From e4a3b9f1fe68585ec844da2bdf26b42cdc37adb5 Mon Sep 17 00:00:00 2001 From: Jack Arturo Date: Sat, 6 Jun 2026 16:21:45 +0200 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20adopt=20house=20standard=20?= =?UTF-8?q?=E2=80=94=20CLAUDE.md=20imports=20AGENTS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote CLAUDE.md's content to AGENTS.md (canonical, read by all agents) and reduce CLAUDE.md to the bare `@AGENTS.md` import. Mechanical only; the freshness pass follows in the next commit. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 93 +------------------------------------------------------ 2 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2c04a57 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +MCP server providing direct USB control of Elgato Stream Deck devices. Bypasses Elgato software entirely using `python-elgato-streamdeck`. + +## Development Commands + +```bash +# Setup +uv venv && uv pip install -e ".[dev]" + +# Run server (for Claude Desktop) +uv run server.py + +# Lint +uv run ruff check . +uv run ruff check --fix . + +# Format +uv run ruff format . + +# Run tests (no hardware required) +uv run pytest tests/ -v + +# Test without deck (fails gracefully if no hardware) +python -c "from server import state; print(state.get_deck_info())" +``` + +## Claude Desktop Config + +Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: +```json +{ + "mcpServers": { + "streamdeck": { + "command": "uv", + "args": ["--directory", "/path/to/streamdeck-mcp", "run", "server.py"] + } + } +} +``` + +## Architecture + +Single-file server (`server.py`) using MCP's `Server` class with stdio transport. + +``` +server.py +├── StreamDeckState # Connection, pages, buttons, callbacks +│ ├── connect() # Find and open first USB deck +│ ├── set_button_*() # Image/action configuration +│ ├── *_page() # Page CRUD and switching +│ └── _key_callback() # Physical button press handler +├── @server.list_tools() # Tool definitions +└── @server.call_tool() # Tool handlers +``` + +**State persistence**: `~/.streamdeck-mcp/pages.json` (appearance) and `buttons.json` (actions). + +## Key Concepts + +- **Button indexing**: 0-based, left-to-right, top-to-bottom. 5x3 deck = keys 0-14. + +``` +Stream Deck Original (15 keys): +┌────┬────┬────┬────┬────┐ +│ 0 │ 1 │ 2 │ 3 │ 4 │ +├────┼────┼────┼────┼────┤ +│ 5 │ 6 │ 7 │ 8 │ 9 │ +├────┼────┼────┼────┼────┤ +│ 10 │ 11 │ 12 │ 13 │ 14 │ +└────┴────┴────┴────┴────┘ +``` + +- **Pages**: Named profiles. "main" is default and undeletable. +- **Actions**: `page:name` for page switching, or shell commands (requires running server). +- **Image generation**: Pillow renders button images. Accepts an MDI icon name (e.g. `mdi:cpu-64-bit` — ~7400 glyphs bundled in `streamdeck_assets/`) or freeform text, plus `icon_color` / `bg_color`. `icon` and `text` are mutually exclusive; use the button's `title` field on `streamdeck_write_page` for labels so they don't collide with Elgato's title overlay. + +## USB Permissions + +- **macOS**: `brew install hidapi` (usually works) +- **Linux**: Needs udev rule — see README +- **Windows**: May need Zadig driver + +## Troubleshooting + +1. **"No Stream Deck found"** — Check USB, permissions, hidapi installed +2. **Button images blank** — Pillow not installed or font not found +3. **State not persisting** — Check write permissions on `~/.streamdeck-mcp/` diff --git a/CLAUDE.md b/CLAUDE.md index 2c04a57..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,92 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -MCP server providing direct USB control of Elgato Stream Deck devices. Bypasses Elgato software entirely using `python-elgato-streamdeck`. - -## Development Commands - -```bash -# Setup -uv venv && uv pip install -e ".[dev]" - -# Run server (for Claude Desktop) -uv run server.py - -# Lint -uv run ruff check . -uv run ruff check --fix . - -# Format -uv run ruff format . - -# Run tests (no hardware required) -uv run pytest tests/ -v - -# Test without deck (fails gracefully if no hardware) -python -c "from server import state; print(state.get_deck_info())" -``` - -## Claude Desktop Config - -Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: -```json -{ - "mcpServers": { - "streamdeck": { - "command": "uv", - "args": ["--directory", "/path/to/streamdeck-mcp", "run", "server.py"] - } - } -} -``` - -## Architecture - -Single-file server (`server.py`) using MCP's `Server` class with stdio transport. - -``` -server.py -├── StreamDeckState # Connection, pages, buttons, callbacks -│ ├── connect() # Find and open first USB deck -│ ├── set_button_*() # Image/action configuration -│ ├── *_page() # Page CRUD and switching -│ └── _key_callback() # Physical button press handler -├── @server.list_tools() # Tool definitions -└── @server.call_tool() # Tool handlers -``` - -**State persistence**: `~/.streamdeck-mcp/pages.json` (appearance) and `buttons.json` (actions). - -## Key Concepts - -- **Button indexing**: 0-based, left-to-right, top-to-bottom. 5x3 deck = keys 0-14. - -``` -Stream Deck Original (15 keys): -┌────┬────┬────┬────┬────┐ -│ 0 │ 1 │ 2 │ 3 │ 4 │ -├────┼────┼────┼────┼────┤ -│ 5 │ 6 │ 7 │ 8 │ 9 │ -├────┼────┼────┼────┼────┤ -│ 10 │ 11 │ 12 │ 13 │ 14 │ -└────┴────┴────┴────┴────┘ -``` - -- **Pages**: Named profiles. "main" is default and undeletable. -- **Actions**: `page:name` for page switching, or shell commands (requires running server). -- **Image generation**: Pillow renders button images. Accepts an MDI icon name (e.g. `mdi:cpu-64-bit` — ~7400 glyphs bundled in `streamdeck_assets/`) or freeform text, plus `icon_color` / `bg_color`. `icon` and `text` are mutually exclusive; use the button's `title` field on `streamdeck_write_page` for labels so they don't collide with Elgato's title overlay. - -## USB Permissions - -- **macOS**: `brew install hidapi` (usually works) -- **Linux**: Needs udev rule — see README -- **Windows**: May need Zadig driver - -## Troubleshooting - -1. **"No Stream Deck found"** — Check USB, permissions, hidapi installed -2. **Button images blank** — Pillow not installed or font not found -3. **State not persisting** — Check write permissions on `~/.streamdeck-mcp/` +@AGENTS.md From 1dfd4597364069dc845fd7b0a0fd261e3ff4f1e9 Mon Sep 17 00:00:00 2001 From: Jack Arturo Date: Sat, 6 Jun 2026 16:28:55 +0200 Subject: [PATCH 2/3] docs: freshen AGENTS.md against current code - Document the dual-server architecture: default profile-writer (profile_server.py, 7 tools + design_streamdeck_deck prompt) plus legacy USB server (server.py, 14 tools); old doc described only the USB server as the whole project. - Correct entry points (uv/uvx, not Node), real env var STREAMDECK_APP_PATH, and verified filesystem/state paths. - Generalize intro to coding agents (Claude Code, Cursor, Codex, others). Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 146 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2c04a57..52a2276 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,22 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to coding agents (Claude Code, Cursor, Codex, and others) when working with code in this repository. ## Overview -MCP server providing direct USB control of Elgato Stream Deck devices. Bypasses Elgato software entirely using `python-elgato-streamdeck`. +MCP server for authoring real Elgato Stream Deck profiles. The default server (`profile_server.py`) writes directly to the Elgato Stream Deck desktop app's profile manifests (`ProfilesV3`, with `ProfilesV2` fallback) instead of taking exclusive USB control. A legacy USB-direct server (`server.py`, built on `python-elgato-streamdeck`) is preserved for Linux/headless setups or when the MCP server should own the hardware itself. + +This is a Python project managed with `uv` (see `pyproject.toml`); there is no `package.json`. + +## Entry Points + +`pyproject.toml` defines three console scripts: + +- `streamdeck-mcp` → `profile_server:run` — **default** profile-writer server (`profile_server.py:782`). +- `streamdeck-mcp-usb` → `server:run` — legacy USB-direct server (`server.py:1332`). +- `streamdeck-mcp-install-skill` → `install_skill:main` — copies the bundled `streamdeck-designer` Agent Skill into `~/.claude/skills/`. + +Both servers use MCP's `Server` class over stdio (`stdio_server`). ## Development Commands @@ -12,7 +24,14 @@ MCP server providing direct USB control of Elgato Stream Deck devices. Bypasses # Setup uv venv && uv pip install -e ".[dev]" -# Run server (for Claude Desktop) +# Run the default profile-writer server +uvx streamdeck-mcp +# or from source: +uv run profile_server.py + +# Run the legacy USB server +uvx --from streamdeck-mcp streamdeck-mcp-usb +# or from source: uv run server.py # Lint @@ -24,45 +43,99 @@ uv run ruff format . # Run tests (no hardware required) uv run pytest tests/ -v - -# Test without deck (fails gracefully if no hardware) -python -c "from server import state; print(state.get_deck_info())" ``` -## Claude Desktop Config +Tests live in `tests/` (`test_profile_manager.py`, `test_server.py`); `pytest` is configured with `asyncio_mode = "auto"` and `testpaths = ["tests"]` in `pyproject.toml`. + +## Client Config + +Run through [`uvx`](https://docs.astral.sh/uv/). Claude Code: -Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: +```bash +claude mcp add streamdeck -- uvx streamdeck-mcp +``` + +Claude Desktop / Cursor / Codex and other stdio MCP clients: ```json { "mcpServers": { "streamdeck": { - "command": "uv", - "args": ["--directory", "/path/to/streamdeck-mcp", "run", "server.py"] + "command": "uvx", + "args": ["streamdeck-mcp"] } } } ``` +For the legacy USB server, use `"args": ["--from", "streamdeck-mcp", "streamdeck-mcp-usb"]`. + ## Architecture -Single-file server (`server.py`) using MCP's `Server` class with stdio transport. +The default profile-writer flow: + +``` +profile_server.py # MCP Server "streamdeck-profile-mcp" (stdio) +├── @server.list_tools() # 7 tool definitions (profile_server.py:253) +├── @server.list_prompts() # design_streamdeck_deck prompt (profile_server.py:694) +├── @server.call_tool() # Tool handlers +└── @server.get_prompt() # Prompt handler + │ + ▼ +profile_manager.py # ProfileManager: read/write Elgato profile manifests +├── ProfilesV3 / ProfilesV2 resolution (profile_manager.py:228) +├── OS-specific plugins/profile dir resolution +├── icon + script generation +└── safe write cycle (quit → write → relaunch) +``` + +The legacy USB flow (`server.py`, MCP Server `"streamdeck-mcp"`): ``` server.py -├── StreamDeckState # Connection, pages, buttons, callbacks -│ ├── connect() # Find and open first USB deck -│ ├── set_button_*() # Image/action configuration -│ ├── *_page() # Page CRUD and switching -│ └── _key_callback() # Physical button press handler -├── @server.list_tools() # Tool definitions -└── @server.call_tool() # Tool handlers +├── StreamDeckState # Connection, pages, buttons, callbacks +│ ├── connect(serial) # Open a USB deck (by serial, or first enumerated) +│ ├── set_button_*() # Image/action configuration +│ ├── *_page() # Page CRUD and switching +│ └── _key_callback() # Physical button press handler +├── @server.list_tools() # 14 tool definitions (server.py:940) +└── @server.call_tool() # Tool handlers ``` -**State persistence**: `~/.streamdeck-mcp/pages.json` (appearance) and `buttons.json` (actions). +## MCP Tools + +### Default profile-writer server (`profile_server.py`) — 7 tools + +1. **streamdeck_read_profiles** — List desktop profiles, device metadata, and page directories from `ProfilesV3`/`ProfilesV2`. +2. **streamdeck_read_page** — Read a page manifest; returns simplified button details plus raw native action objects. +3. **streamdeck_write_page** — Create or rewrite a page manifest. Reuse copied `button.raw` values to preserve configured third-party plugin actions. +4. **streamdeck_create_icon** — Render button or touch-strip PNGs from ~7,400 bundled Material Design Icons or from short text; unknown icon names return close-match suggestions. +5. **streamdeck_create_action** — Create an executable shell script in `~/StreamDeckScripts/` and return an Open action block. +6. **streamdeck_restart_app** — Restart the macOS Stream Deck desktop app after profile changes. +7. **streamdeck_install_mcp_plugin** — Install the bundled streamdeck-mcp Stream Deck plugin for durable encoder imagery (usually auto-installed by `streamdeck_write_page`). + +**Prompt:** `design_streamdeck_deck` — primes non-skill-aware MCP clients with the streamdeck-designer authoring briefing; optional `intent` argument. + +### Legacy USB server (`server.py`) — 14 tools + +`streamdeck_connect`, `streamdeck_list_devices`, `streamdeck_info`, `streamdeck_set_button`, `streamdeck_set_buttons`, `streamdeck_clear_button`, `streamdeck_get_button`, `streamdeck_clear_all`, `streamdeck_set_brightness`, `streamdeck_create_page`, `streamdeck_switch_page`, `streamdeck_list_pages`, `streamdeck_delete_page`, `streamdeck_disconnect`. + +When multiple decks are attached, call `streamdeck_list_devices` first, then pass the desired `serial` to `streamdeck_connect`; omitting `serial` opens the first enumerated deck. + +## State & Filesystem Paths + +- **Profile-writer:** edits the Elgato app's profile manifests in place under its plugins/profiles directories (macOS: `~/Library/Application Support/com.elgato.StreamDeck/`; Linux: `~/.local/share/Elgato/StreamDeck/`; Windows: `%APPDATA%\Elgato\StreamDeck\`). + - Generated icons: `~/.streamdeck-mcp/generated-icons/` (`profile_manager.py:485`). + - Generated shell scripts: `~/StreamDeckScripts/` (`profile_manager.py:480`). +- **Legacy USB server:** persists its own state to `~/.streamdeck-mcp/pages.json` and `buttons.json` (`server.py:55`). + +## Environment Variables + +- **`STREAMDECK_APP_PATH`** — override the Elgato app location if it is not at `/Applications/Elgato Stream Deck.app` (read in `profile_manager.py:332`; default `profile_manager.py:111`). +- `APPDATA` (Windows-only system var) is read to locate the Stream Deck plugins directory on Windows. ## Key Concepts -- **Button indexing**: 0-based, left-to-right, top-to-bottom. 5x3 deck = keys 0-14. +- **Button indexing**: 0-based, left-to-right, top-to-bottom. A 5x3 deck = keys 0-14. ``` Stream Deck Original (15 keys): @@ -75,18 +148,29 @@ Stream Deck Original (15 keys): └────┴────┴────┴────┴────┘ ``` -- **Pages**: Named profiles. "main" is default and undeletable. -- **Actions**: `page:name` for page switching, or shell commands (requires running server). -- **Image generation**: Pillow renders button images. Accepts an MDI icon name (e.g. `mdi:cpu-64-bit` — ~7400 glyphs bundled in `streamdeck_assets/`) or freeform text, plus `icon_color` / `bg_color`. `icon` and `text` are mutually exclusive; use the button's `title` field on `streamdeck_write_page` for labels so they don't collide with Elgato's title overlay. +- **Image generation**: Pillow renders button images. `streamdeck_create_icon` accepts an MDI icon name (e.g. `mdi:cpu-64-bit` — ~7,400 glyphs bundled in `streamdeck_assets/`) or freeform text, plus `icon_color` / `bg_color`. Use a button's `title` field on `streamdeck_write_page` for labels so they don't collide with rendered icon art. +- **Safe write cycle**: the Elgato desktop app keeps profiles in memory and can overwrite on-disk manifest edits when it quits. `streamdeck_write_page` raises `StreamDeckAppRunningError` when the app is running and `auto_quit_app` is not set. On macOS, pass `auto_quit_app: true` to quit before writing, then call `streamdeck_restart_app`. On Windows, quit and relaunch manually. +- **Profile versions**: `ProfilesV3` is preferred when present. `ProfilesV2` is supported, but existing pages should be targeted by `directory_id` or `page_index` because V2 uses opaque directory names. + +## streamdeck-designer Skill + +The repo bundles an Agent Skill (`streamdeck_assets/skill/streamdeck-designer/`) that teaches agents to plan, theme, and author full decks. Install with: + +```bash +uvx --from streamdeck-mcp streamdeck-mcp-install-skill # adds --force to upgrade +``` + +It is copied to `~/.claude/skills/streamdeck-designer/`. Clients that don't load Claude Code skills can invoke the `design_streamdeck_deck` MCP prompt instead. -## USB Permissions +## USB Permissions (legacy server only) -- **macOS**: `brew install hidapi` (usually works) -- **Linux**: Needs udev rule — see README -- **Windows**: May need Zadig driver +- **macOS**: `brew install hidapi` +- **Linux**: needs a udev rule — see README +- **Windows**: may need the Zadig driver ## Troubleshooting -1. **"No Stream Deck found"** — Check USB, permissions, hidapi installed -2. **Button images blank** — Pillow not installed or font not found -3. **State not persisting** — Check write permissions on `~/.streamdeck-mcp/` +1. **Profile edits not appearing** — the Elgato app may have overwritten manifests on quit; use `auto_quit_app: true` then `streamdeck_restart_app`. +2. **App not found / wrong path** — set `STREAMDECK_APP_PATH`. +3. **Button images blank** — Pillow not installed or font not found. +4. **(Legacy USB) "No Stream Deck found"** — check USB, permissions, and that `hidapi` is installed. From 7c8c35773da137808ad917712eed30d364bfe7fe Mon Sep 17 00:00:00 2001 From: Jack Arturo Date: Sat, 6 Jun 2026 17:21:31 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20describe=20observable=20write=5Fpage=20warning=20be?= =?UTF-8?q?havior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 52a2276..e8f2fca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,7 +149,7 @@ Stream Deck Original (15 keys): ``` - **Image generation**: Pillow renders button images. `streamdeck_create_icon` accepts an MDI icon name (e.g. `mdi:cpu-64-bit` — ~7,400 glyphs bundled in `streamdeck_assets/`) or freeform text, plus `icon_color` / `bg_color`. Use a button's `title` field on `streamdeck_write_page` for labels so they don't collide with rendered icon art. -- **Safe write cycle**: the Elgato desktop app keeps profiles in memory and can overwrite on-disk manifest edits when it quits. `streamdeck_write_page` raises `StreamDeckAppRunningError` when the app is running and `auto_quit_app` is not set. On macOS, pass `auto_quit_app: true` to quit before writing, then call `streamdeck_restart_app`. On Windows, quit and relaunch manually. +- **Safe write cycle**: the Elgato desktop app keeps profiles in memory and can overwrite on-disk manifest edits when it quits. When the app is running and `auto_quit_app` is not set, `streamdeck_write_page` returns a `⚠️` warning text result (not a raised exception) and does not write — inspect the returned text to detect this. On macOS, pass `auto_quit_app: true` to quit before writing, then call `streamdeck_restart_app`. On Windows, quit and relaunch manually. - **Profile versions**: `ProfilesV3` is preferred when present. `ProfilesV2` is supported, but existing pages should be targeted by `directory_id` or `page_index` because V2 uses opaque directory names. ## streamdeck-designer Skill