Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
619 changes: 602 additions & 17 deletions profile_manager.py

Large diffs are not rendered by default.

124 changes: 114 additions & 10 deletions profile_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,29 +206,68 @@ async def list_tools() -> list[Tool]:
"plugin_uuid": {
"type": "string",
"description": (
"For advanced actions, the plugin UUID used "
"when building a native action object."
"For a third-party (or advanced native) action, the plugin's "
"UUID, e.g. 'com.romancin.wiim' or 'one.blueshift.streamdeck.toggl'. "
"Pair with action_uuid + settings. Get it from "
"streamdeck_read_plugins (plugin_uuid). The write is validated "
"against installed plugins."
),
},
"plugin_name": {
"type": "string",
"description": (
"Optional Plugin display name. Defaults to the installed "
"plugin's manifest Name when omitted."
),
},
"plugin_version": {
"type": "string",
"description": (
"Optional Plugin version. Defaults to the installed plugin's "
"manifest Version when omitted."
),
},
"action_uuid": {
"type": "string",
"description": (
"For advanced actions, the native action UUID, "
"for example com.elgato.streamdeck.page.next."
"The native action UUID to write. Built-in examples: "
"com.elgato.streamdeck.page.next. Third-party example: "
"'com.romancin.wiim.preset'. Must be one of the plugin's "
"declared action UUIDs (see streamdeck_read_plugins) or the "
"write is rejected."
),
},
"action_name": {
"type": "string",
"description": ("Optional action Name. Defaults to the action's manifest Name."),
},
"settings": {
"type": "object",
"description": "Native action settings object.",
"description": (
"Native action Settings object. For third-party actions, use "
"the keys from that action's settings_fields "
"(streamdeck_read_plugins). Missing declared fields are reported "
"in the write result's 'warnings'."
),
},
"states": {
"type": "array",
"items": {"type": "object"},
"description": (
"Optional explicit per-instance States array for a plugin "
"action. Omit to default to the plugin manifest's declared "
"state count (empty state objects)."
),
},
"action_id": {
"type": "string",
"description": (
"Optional stable ActionID. A random UUID is generated when omitted."
),
},
"linked_title": {
"type": "boolean",
"description": "Optional LinkedTitle flag for the native action.",
},
"state": {
"type": "integer",
Expand All @@ -251,6 +290,52 @@ async def list_tools() -> list[Tool]:
}

return [
Tool(
name="streamdeck_read_plugins",
description=(
"List installed Stream Deck plugins and their declared actions from "
"the user's Elgato Plugins directory, including each action's UUID, "
"Name, States, and — best-effort — the Settings fields its Property "
"Inspector expects (parsed from the action's PI HTML/JS). Use this "
"before authoring a third-party plugin action with "
"streamdeck_write_page: it gives you the plugin_uuid, action_uuid, "
"state_count, and settings_fields you need. Notes: some first-party "
"Elgato plugins (OBS, Home Assistant, Hue, Spotify, Zoom, …) ship "
"ELGATO-protected/binary manifests and are reported with "
"parse_status instead of action metadata — for those, copy a "
"configured button via streamdeck_read_page's raw action. "
"settings_fields are heuristic: a missing field is not proof it is "
"unused, and a listed field is not guaranteed required."
),
inputSchema={
"type": "object",
"properties": {
"plugin_id": {
"type": "string",
"description": (
"Optional exact match against manifest UUID, folder stem, "
"folder name, or plugin display name."
),
},
"include_raw_manifest": {
**_scalar_or_string("boolean"),
"description": (
"If true, include raw_manifest for readable JSON manifests. "
"Defaults to false."
),
},
"include_settings_schema": {
**_scalar_or_string("boolean"),
"description": (
"If true (default), enrich each readable action with "
"state_count and a best-effort settings_fields list "
"inferred from its Property Inspector. Set false to skip "
"PI parsing for a faster, manifest-only listing."
),
},
},
},
),
Tool(
name="streamdeck_read_profiles",
description=(
Expand Down Expand Up @@ -300,11 +385,22 @@ async def list_tools() -> list[Tool]:
name="streamdeck_write_page",
description=(
"Create a new page or replace/update an existing Stream Deck desktop "
"page manifest. IMPORTANT: the Elgato desktop app overwrites profile "
"manifests from its in-memory state on quit, so writes made while the "
"app is running are lost. This tool refuses to write when the app is "
"running unless auto_quit_app=True is passed. Call streamdeck_restart_app "
"once your edits are complete to make the changes visible on the device."
"page manifest. Buttons can be page-nav (action_type), script-backed "
"Open actions (path, from streamdeck_create_action), the bundled MCP "
"dial, OR a native action for ANY installed third-party plugin — pass "
"plugin_uuid + action_uuid + settings (discover these with "
"streamdeck_read_plugins) and the tool writes the same native action "
"shape Stream Deck itself uses (Plugin metadata, State, States, "
"Settings), defaulting the States count and Plugin name/version from "
"the installed manifest so the button survives Elgato's "
"overwrite-on-quit. Validation: an unknown plugin_uuid/action_uuid "
"raises a clear error listing what is installed; missing settings "
"fields are reported non-fatally in the result's 'warnings'. "
"IMPORTANT: the Elgato desktop app overwrites profile manifests from "
"its in-memory state on quit, so writes made while the app is running "
"are lost. This tool refuses to write when the app is running unless "
"auto_quit_app=True is passed. Call streamdeck_restart_app once your "
"edits are complete to make the changes visible on the device."
),
inputSchema={
"type": "object",
Expand Down Expand Up @@ -568,11 +664,19 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
"transparent_bg",
"force",
"show_title",
"include_raw_manifest",
),
arrays=("buttons", "icons"),
)

try:
if name == "streamdeck_read_plugins":
plugins = manager.list_plugins(
plugin_id=arguments.get("plugin_id"),
include_raw_manifest=arguments.get("include_raw_manifest", False),
)
Comment on lines +674 to +677

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass through the plugin schema opt-out flag

The tool schema exposes include_settings_schema=false to skip Property Inspector parsing, but this handler neither coerces nor forwards that argument, so every streamdeck_read_plugins call still uses ProfileManager.list_plugins' default include_settings_schema=True. On installations with several PI files this defeats the advertised faster manifest-only mode and returns fields callers explicitly opted out of; add this argument to boolean coercion and pass it into list_plugins.

Useful? React with 👍 / 👎.

return [TextContent(type="text", text=json.dumps(plugins, indent=2))]

if name == "streamdeck_read_profiles":
profiles = manager.list_profiles()
return [TextContent(type="text", text=json.dumps(profiles, indent=2))]
Expand Down
9 changes: 5 additions & 4 deletions skills/streamdeck-profile/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ Use this skill when you need to configure Elgato Stream Deck desktop profiles wi

1. Call `streamdeck_read_profiles` to discover the active profiles root and page `directory_id` values.
2. Call `streamdeck_read_page` before editing an existing page so you can inspect the current native action objects.
3. Use `streamdeck_create_icon` for button art. Pass `icon` with a Material Design Icons name like `mdi:cpu-64-bit`, `mdi:volume-high`, or `mdi:github` (~7400 glyphs bundled) plus `icon_color` and `bg_color` for a glyph; or pass `text` alone for a text-only icon. `icon` and `text` are mutually exclusive — set the button's `title` on `streamdeck_write_page` for labels, since Elgato overlays titles on images.
4. Use `streamdeck_create_action` when you want a shell-command button. It creates an executable script in `~/StreamDeckScripts/` and returns a ready-to-insert Open action block.
5. Call `streamdeck_write_page` with `directory_id` for the safest updates on existing pages.
6. Call `streamdeck_restart_app` on macOS if the Stream Deck desktop app does not pick up changes immediately.
3. Call `streamdeck_read_plugins` before authoring installed plugin actions. It discovers plugin/action UUIDs from readable manifests, but does not infer property inspector settings schemas. Prefer copying configured plugin actions from `streamdeck_read_page` via `button.raw`.
4. Use `streamdeck_create_icon` for button art. Pass `icon` with a Material Design Icons name like `mdi:cpu-64-bit`, `mdi:volume-high`, or `mdi:github` (~7400 glyphs bundled) plus `icon_color` and `bg_color` for a glyph; or pass `text` alone for a text-only icon. `icon` and `text` are mutually exclusive — set the button's `title` on `streamdeck_write_page` for labels, since Elgato overlays titles on images.
5. Use `streamdeck_create_action` when you want a shell-command button. It creates an executable script in `~/StreamDeckScripts/` and returns a ready-to-insert Open action block.
6. Call `streamdeck_write_page` with `directory_id` for the safest updates on existing pages.
7. Call `streamdeck_restart_app` on macOS if the Stream Deck desktop app does not pick up changes immediately.

## Practical Notes

Expand Down
3 changes: 3 additions & 0 deletions streamdeck_assets/skill/streamdeck-designer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ For **Original / MK.2 / Neo / Mini**: fewer slots, so every slot matters more. O

When the user names an integration, check what you have access to **right now** in this session:

- If the user wants to use an **installed Stream Deck plugin action**, call `streamdeck_read_plugins` first to discover installed plugins and action UUIDs. This is a manifest catalog only: it does not infer property-inspector settings schemas. Prefer copying configured actions from `streamdeck_read_page` via `button.raw`; only synthesize new third-party actions when you already have a known-good `plugin_uuid`, `action_uuid`, and `settings` shape.
- If a matching MCP is available (Hue MCP, Home Assistant MCP, Spotify MCP, OBS MCP), **use it** to enumerate the user's real devices, scenes, rooms, playlists. The goal is that when the user presses "Chill Scene" on the deck, their actual living-room Hue scene named "Chill" activates — not a placeholder.
- If no matching MCP is available, **ask the user** for the endpoints, credentials, bridge IPs, auth tokens you need. Tell them plainly what you're going to do with each (e.g. "I'll store your Hue API key in ~/StreamDeckScripts/.env — nothing outside your machine").

Expand Down Expand Up @@ -118,6 +119,8 @@ For page navigation between pages in the same profile, use `action_type: "next_p

For switching to a **different profile** on press, that's not exposed as a convenience field — build the action object explicitly with `plugin_uuid: "com.elgato.streamdeck.profile"` and the profile-switch action UUID. Most decks don't need this; if you need it, check the Elgato SDK reference.

For installed third-party plugins, discover action UUIDs with `streamdeck_read_plugins`. If the user already configured the action somewhere in the Stream Deck app, use `streamdeck_read_page` on that page and copy the button's `raw` action object into the new location; this preserves plugin-specific `Settings` without guessing. If you only have the plugin manifest, you still need the correct settings shape from the user, the plugin docs, or an existing configured action.

No other action types exist standalone in Phase 1. In particular: there is no "call back to Claude" action yet — pressing a key fires a shell script or switches a page, nothing else. See Phase 2 in the repo roadmap if the user asks about live/dynamic behavior.

### 8. Pick encoder layouts with the Phase 1 constraint in mind
Expand Down
Loading
Loading