opendeck-mcp 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. opendeck_mcp-0.3.1/.gitattributes +2 -0
  2. opendeck_mcp-0.3.1/.github/workflows/publish.yml +57 -0
  3. opendeck_mcp-0.3.1/.github/workflows/tests.yml +34 -0
  4. opendeck_mcp-0.3.1/.gitignore +8 -0
  5. opendeck_mcp-0.3.1/CHANGELOG.md +66 -0
  6. opendeck_mcp-0.3.1/LICENSE +21 -0
  7. opendeck_mcp-0.3.1/PKG-INFO +347 -0
  8. opendeck_mcp-0.3.1/README.md +319 -0
  9. opendeck_mcp-0.3.1/ROADMAP.md +243 -0
  10. opendeck_mcp-0.3.1/pyproject.toml +57 -0
  11. opendeck_mcp-0.3.1/scripts/scrub_fixture.py +41 -0
  12. opendeck_mcp-0.3.1/server.json +22 -0
  13. opendeck_mcp-0.3.1/src/opendeck_mcp/__init__.py +3 -0
  14. opendeck_mcp-0.3.1/src/opendeck_mcp/__main__.py +74 -0
  15. opendeck_mcp-0.3.1/src/opendeck_mcp/bridge_plugin/dev.kastro5.opendeck-mcp-bridge.sdPlugin/bridge.js +78 -0
  16. opendeck_mcp-0.3.1/src/opendeck_mcp/bridge_plugin/dev.kastro5.opendeck-mcp-bridge.sdPlugin/icon.png +0 -0
  17. opendeck_mcp-0.3.1/src/opendeck_mcp/bridge_plugin/dev.kastro5.opendeck-mcp-bridge.sdPlugin/icon@2x.png +0 -0
  18. opendeck_mcp-0.3.1/src/opendeck_mcp/bridge_plugin/dev.kastro5.opendeck-mcp-bridge.sdPlugin/index.html +9 -0
  19. opendeck_mcp-0.3.1/src/opendeck_mcp/bridge_plugin/dev.kastro5.opendeck-mcp-bridge.sdPlugin/manifest.json +14 -0
  20. opendeck_mcp-0.3.1/src/opendeck_mcp/core/__init__.py +0 -0
  21. opendeck_mcp-0.3.1/src/opendeck_mcp/core/apps.py +99 -0
  22. opendeck_mcp-0.3.1/src/opendeck_mcp/core/catalog.py +103 -0
  23. opendeck_mcp-0.3.1/src/opendeck_mcp/core/devices.py +183 -0
  24. opendeck_mcp-0.3.1/src/opendeck_mcp/core/fonts.py +104 -0
  25. opendeck_mcp-0.3.1/src/opendeck_mcp/core/homeassistant.py +224 -0
  26. opendeck_mcp-0.3.1/src/opendeck_mcp/core/icons.py +202 -0
  27. opendeck_mcp-0.3.1/src/opendeck_mcp/core/images.py +95 -0
  28. opendeck_mcp-0.3.1/src/opendeck_mcp/core/jsonio.py +52 -0
  29. opendeck_mcp-0.3.1/src/opendeck_mcp/core/knowledge.py +135 -0
  30. opendeck_mcp-0.3.1/src/opendeck_mcp/core/manifest.py +285 -0
  31. opendeck_mcp-0.3.1/src/opendeck_mcp/core/paths.py +119 -0
  32. opendeck_mcp-0.3.1/src/opendeck_mcp/core/pi_scrape.py +104 -0
  33. opendeck_mcp-0.3.1/src/opendeck_mcp/core/preview.py +267 -0
  34. opendeck_mcp-0.3.1/src/opendeck_mcp/core/profile.py +147 -0
  35. opendeck_mcp-0.3.1/src/opendeck_mcp/core/render.py +113 -0
  36. opendeck_mcp-0.3.1/src/opendeck_mcp/core/secrets.py +77 -0
  37. opendeck_mcp-0.3.1/src/opendeck_mcp/core/validate.py +125 -0
  38. opendeck_mcp-0.3.1/src/opendeck_mcp/http.py +186 -0
  39. opendeck_mcp-0.3.1/src/opendeck_mcp/plugins/__init__.py +0 -0
  40. opendeck_mcp-0.3.1/src/opendeck_mcp/plugins/catalogue.py +127 -0
  41. opendeck_mcp-0.3.1/src/opendeck_mcp/plugins/installer.py +124 -0
  42. opendeck_mcp-0.3.1/src/opendeck_mcp/runtime/__init__.py +0 -0
  43. opendeck_mcp-0.3.1/src/opendeck_mcp/runtime/applier.py +267 -0
  44. opendeck_mcp-0.3.1/src/opendeck_mcp/runtime/backups.py +120 -0
  45. opendeck_mcp-0.3.1/src/opendeck_mcp/runtime/live.py +171 -0
  46. opendeck_mcp-0.3.1/src/opendeck_mcp/runtime/process.py +144 -0
  47. opendeck_mcp-0.3.1/src/opendeck_mcp/server.py +437 -0
  48. opendeck_mcp-0.3.1/src/opendeck_mcp/staging/__init__.py +0 -0
  49. opendeck_mcp-0.3.1/src/opendeck_mcp/staging/diff.py +113 -0
  50. opendeck_mcp-0.3.1/src/opendeck_mcp/staging/ops.py +528 -0
  51. opendeck_mcp-0.3.1/src/opendeck_mcp/staging/plan.py +74 -0
  52. opendeck_mcp-0.3.1/src/opendeck_mcp/staging/workspace.py +167 -0
  53. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/__init__.py +0 -0
  54. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/bridge_hook.py +48 -0
  55. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/edit_tools.py +508 -0
  56. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/ha_tools.py +95 -0
  57. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/icon_tools.py +30 -0
  58. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/plugin_tools.py +108 -0
  59. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/preview_tools.py +40 -0
  60. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/read_tools.py +211 -0
  61. opendeck_mcp-0.3.1/src/opendeck_mcp/tools/view.py +77 -0
  62. opendeck_mcp-0.3.1/tests/conftest.py +31 -0
  63. opendeck_mcp-0.3.1/tests/fixtures/config/applications.json +5 -0
  64. opendeck_mcp-0.3.1/tests/fixtures/config/images/ulanzi-d200h/Default/Keypad.12.0/0.png +0 -0
  65. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/deviceBrightness.png +0 -0
  66. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/inputSimulation.png +0 -0
  67. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/openUrl.png +0 -0
  68. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/plugin.png +0 -0
  69. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/runCommand.png +0 -0
  70. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/icons/switchProfile.png +0 -0
  71. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/manifest.json +108 -0
  72. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.amansprojects.starterpack.sdPlugin/propertyInspector/check.svg +0 -0
  73. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.ulanzi.d200x.sdPlugin/assets/icon.png +0 -0
  74. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.ulanzi.d200x.sdPlugin/assets/widescreen.png +0 -0
  75. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/com.ulanzi.d200x.sdPlugin/manifest.json +37 -0
  76. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/actionImage.png +0 -0
  77. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/actionImage@2x.png +0 -0
  78. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/category.png +0 -0
  79. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/category@2x.png +0 -0
  80. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button-off.png +0 -0
  81. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button-off@2x.png +0 -0
  82. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button-on.png +0 -0
  83. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button-on@2x.png +0 -0
  84. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button.png +0 -0
  85. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/ha-button@2x.png +0 -0
  86. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/plugin/plugin.png +0 -0
  87. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/images/plugin/plugin@2x.png +0 -0
  88. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/manifest.json +62 -0
  89. opendeck_mcp-0.3.1/tests/fixtures/config/plugins/de.perdoctus.streamdeck.homeassistant.sdPlugin/previews/1-preview.png +0 -0
  90. opendeck_mcp-0.3.1/tests/fixtures/config/profiles/ulanzi-d200h/Default.json +688 -0
  91. opendeck_mcp-0.3.1/tests/fixtures/config/profiles/ulanzi-d200h.json +3 -0
  92. opendeck_mcp-0.3.1/tests/fixtures/config/profiles/ulanzi-d200x/Default.json +145 -0
  93. opendeck_mcp-0.3.1/tests/fixtures/config/profiles/ulanzi-d200x.json +3 -0
  94. opendeck_mcp-0.3.1/tests/fixtures/config/settings/de.perdoctus.streamdeck.homeassistant.sdPlugin.json +1 -0
  95. opendeck_mcp-0.3.1/tests/fixtures/config/settings/dev.kastro5.opendeck-mcp-bridge.sdPlugin.json +1 -0
  96. opendeck_mcp-0.3.1/tests/fixtures/config/settings.json +4 -0
  97. opendeck_mcp-0.3.1/tests/test_apps.py +20 -0
  98. opendeck_mcp-0.3.1/tests/test_core.py +139 -0
  99. opendeck_mcp-0.3.1/tests/test_homeassistant.py +95 -0
  100. opendeck_mcp-0.3.1/tests/test_http.py +72 -0
  101. opendeck_mcp-0.3.1/tests/test_icons.py +103 -0
  102. opendeck_mcp-0.3.1/tests/test_live.py +111 -0
  103. opendeck_mcp-0.3.1/tests/test_preview.py +84 -0
  104. opendeck_mcp-0.3.1/tests/test_server_and_plugins.py +53 -0
  105. opendeck_mcp-0.3.1/tests/test_staging.py +129 -0
  106. opendeck_mcp-0.3.1/uv.lock +1158 -0
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ *.png binary
@@ -0,0 +1,57 @@
1
+ # On a version tag: test, publish the package to PyPI, then list the release in the MCP Registry.
2
+ #
3
+ # git tag -a v1.2.3 -m "opendeck-mcp 1.2.3" && git push origin v1.2.3
4
+ #
5
+ # The tag must match pyproject.toml's version. Both publishes use the workflow's OIDC identity, so
6
+ # no tokens are stored: PyPI needs this repo + workflow + the "pypi" environment registered once as a
7
+ # trusted publisher for the opendeck-mcp project, and the registry accepts io.github.kastro5/* from it.
8
+ name: Publish
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ publish:
19
+ runs-on: ubuntu-latest
20
+ environment: pypi
21
+ permissions:
22
+ contents: read
23
+ id-token: write # OIDC for PyPI trusted publishing and the MCP Registry
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
+
27
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
28
+
29
+ - name: Check the tag matches the version
30
+ run: |
31
+ version=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
32
+ test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != version $version"; exit 1; }
33
+ test "$(jq -r .version server.json)" = "$version" || { echo "server.json version != $version"; exit 1; }
34
+
35
+ - name: Test
36
+ run: |
37
+ uv sync --locked
38
+ uv run pytest -q -m "not live"
39
+
40
+ - name: Build
41
+ run: uv build
42
+
43
+ - name: Publish to PyPI
44
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
45
+
46
+ - name: Publish to the MCP Registry
47
+ run: |
48
+ curl -fsSL -o mcp-publisher.tar.gz \
49
+ https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz
50
+ echo "a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc mcp-publisher.tar.gz" | sha256sum -c -
51
+ tar xzf mcp-publisher.tar.gz mcp-publisher
52
+ # PyPI can take a moment to serve a new release, and the registry checks it exists.
53
+ for i in 1 2 3 4 5 6; do
54
+ ./mcp-publisher login github-oidc && ./mcp-publisher publish && exit 0
55
+ sleep 20
56
+ done
57
+ exit 1
@@ -0,0 +1,34 @@
1
+ # Lint and unit tests on every push and pull request, on Windows (where it is used) and Linux
2
+ # (where the platform code is otherwise untested). The live tests need a real OpenDeck and are
3
+ # skipped here.
4
+ name: Tests
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [windows-latest, ubuntu-latest]
21
+ runs-on: ${{ matrix.os }}
22
+ steps:
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
+
25
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
26
+
27
+ - name: Install
28
+ run: uv sync --locked
29
+
30
+ - name: Lint
31
+ run: uv run ruff check src tests
32
+
33
+ - name: Test
34
+ run: uv run pytest -q -m "not live"
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ .claude/
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1 — 2026-10-15
4
+
5
+ - Published on PyPI (`uvx opendeck-mcp`) and listed in the MCP Registry as
6
+ `io.github.kastro5/opendeck-mcp`; releases are published by GitHub Actions on version tags.
7
+
8
+ Upgrading: re-register with `uvx opendeck-mcp@0.3.1` (see the README).
9
+
10
+ ## 0.3.0 — 2026-10-15
11
+
12
+ - **Icons.** `make_icon` makes a key image from text, an emoji (Twemoji), a brand logo (Simple
13
+ Icons, in the brand's colour) or a symbol (Material Design Icons), and saves it as a PNG to pass
14
+ as `image`. `find_icons` searches the icon sets by name, alias and tag.
15
+ - **Folders.** `make_folder` creates a sub-profile, a key that opens it and a Back key inside, with
16
+ titles placed under the icons.
17
+ - `get_app_rules` lists the exact names of the apps you have open, as OpenDeck matches them (the
18
+ executable's description on Windows), so app rules no longer need a trip through OpenDeck's UI.
19
+ - Staged moves to another profile no longer show a stray space in their description.
20
+
21
+ Upgrading: re-register with `@v0.3.0` (see the README).
22
+
23
+ ## 0.2.0 — 2026-10-15
24
+
25
+ - **Instant edits.** `update_key` now changes a key's title, image and settings in the running
26
+ OpenDeck without restarting it, when the key is on the profile a connected device is showing.
27
+ Settings reach the plugin the same way OpenDeck's own settings panel sends them. Everything else
28
+ is still staged, and the result says which happened. A new `mode` parameter (`auto`, `live`,
29
+ `staged`) overrides the choice.
30
+ - New `undo_live` tool: puts back the titles, images and settings changed by instant edits. Images
31
+ are saved as bytes before each edit, so a custom key image can always be restored.
32
+ - **Home Assistant awareness.** `ha_search` finds entities by name, domain or area,
33
+ `ha_describe_entity` shows an entity's state and services and prints ready-made settings for a
34
+ Home Assistant key, and `ha_call_service` calls a service on request (after a confirmation step).
35
+ The server uses the login already saved in the Home Assistant plugin, and the token never appears
36
+ in any output.
37
+ - `describe_action` now explains the Home Assistant plugin's key settings.
38
+ - **Pictures of the deck.** `render_profile` draws a profile the way OpenDeck draws it (icons,
39
+ titles with their font, colour, stroke and alignment, background colours, the wide key, dials and
40
+ touchpoints), with staged keys outlined, or before and after side by side. OpenDeck's title fonts
41
+ are downloaded once from Google Fonts; offline, a system font with the same metrics is used.
42
+ - **HTTP transport.** `opendeck-mcp serve --host <ip>` serves over streamable HTTP for clients on
43
+ other machines, with a bearer token on every request and Host-header checks, and it refuses to
44
+ listen on every interface unless told to. `install-autostart` starts it hidden at each Windows
45
+ logon (from the Startup folder; no admin rights), `token` prints the client's `claude mcp add`
46
+ command, and `uninstall-autostart` removes it. Running `opendeck-mcp` with no arguments still
47
+ serves stdio.
48
+ - README: upgrading, other MCP clients, network use and stored files, troubleshooting.
49
+ - New dependencies: `websockets`, `resvg-py` (SVG icons in previews).
50
+
51
+ Upgrading: re-register with `@v0.2.0` (see the README). The bridge plugin updates itself on the next
52
+ apply.
53
+
54
+ ## 0.1.0 — 2026-10-14
55
+
56
+ First release.
57
+
58
+ - Read devices, profiles, keys, plugins, plugin settings (redacted), logs and app rules.
59
+ - Stage key edits (set, update, clear, move/swap/copy), Multi/Toggle Action children and delays,
60
+ profile create/duplicate/rename/delete/select, app switching rules and plugin settings.
61
+ - Preview staged changes as a per-key diff with warnings; apply them with OpenDeck stopped, a backup,
62
+ verification and an automatic rollback; undo with `restore_backup`.
63
+ - Search the OpenAction catalogue and the Elgato archive; install, update and remove plugins after
64
+ inspecting them.
65
+ - Companion *OpenDeck MCP Bridge* plugin that records device geometry.
66
+ - Tested on Windows 11 with OpenDeck 2.14.1.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kastro5
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.5
2
+ Name: opendeck-mcp
3
+ Version: 0.3.1
4
+ Summary: MCP server for configuring OpenDeck (open-source Stream Deck software) in natural language
5
+ Project-URL: Homepage, https://github.com/kastro5/opendeck-mcp
6
+ Project-URL: Changelog, https://github.com/kastro5/opendeck-mcp/blob/main/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/kastro5/opendeck-mcp/issues
8
+ Author: kastro5
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,mcp,model-context-protocol,opendeck,stream-deck,streamdeck
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Desktop Environment
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpx==0.28.1
21
+ Requires-Dist: mcp==2.2.0
22
+ Requires-Dist: pillow==12.3.0
23
+ Requires-Dist: platformdirs==4.12.2
24
+ Requires-Dist: psutil==7.2.2
25
+ Requires-Dist: resvg-py==0.5.0
26
+ Requires-Dist: websockets==15.0.1
27
+ Description-Content-Type: text/markdown
28
+
29
+ # opendeck-mcp
30
+
31
+ <!-- mcp-name: io.github.kastro5/opendeck-mcp -->
32
+
33
+ Set up your [OpenDeck](https://github.com/nekename/OpenDeck) stream controller by asking an AI agent:
34
+
35
+ > "Put a Multi Action on the top-left key that mutes my mic and opens Discord, make a 'Gaming'
36
+ > profile from my default one, and switch to it whenever Steam is focused."
37
+
38
+ `opendeck-mcp` is an [MCP](https://modelcontextprotocol.io) server. It lets Claude Code (or any MCP client)
39
+ read your decks, profiles and plugins, **stage** changes, show you a diff, and then apply them, with a
40
+ backup you can undo. It works with any device OpenDeck drives: Elgato Stream Decks and plugin-provided
41
+ devices such as the [Ulanzi D200H/D200X](https://github.com/kastro5/opendeck-ulanzi-d200).
42
+
43
+ ## Contents
44
+
45
+ - [What it can do](#what-it-can-do)
46
+ - [Install](#install)
47
+ - [Upgrading](#upgrading)
48
+ - [Other MCP clients](#other-mcp-clients)
49
+ - [From another computer](#from-another-computer)
50
+ - [Using it](#using-it)
51
+ - [How it works](#how-it-works)
52
+ - [Safety](#safety)
53
+ - [Tools](#tools)
54
+ - [Limitations](#limitations)
55
+ - [Troubleshooting](#troubleshooting)
56
+ - [Development](#development)
57
+
58
+ ## What it can do
59
+
60
+ - **See everything:** devices with their grid size, dials and touchpoints; profiles; every key's action,
61
+ title and settings; and installed plugins with their actions.
62
+ - **Edit keys:** place, update (title, font, colour, image, settings), clear, move, swap or copy keys, dials
63
+ and touchpoints, across profiles and devices.
64
+ - **Multi and Toggle Actions:** add and remove the actions inside them, and set the delays between them.
65
+ - **Profiles:** create, duplicate, rename (references to the old name are updated too) and delete
66
+ profiles, including `Folder/Name` profiles, and choose which one each device starts on.
67
+ - **App switching:** switch a device to a profile automatically when an app is focused.
68
+ - **Plugins:**
69
+ - search the same catalogue as OpenDeck's Plugin Manager, plus the Elgato plugin archive;
70
+ - install, update and remove plugins;
71
+ - read and write plugin settings, with secrets redacted;
72
+ - read plugin logs.
73
+ - **Instant edits:** title, image and settings tweaks to a key on the profile a device is showing
74
+ apply at once, with no restart; `undo_live` takes them back.
75
+ - **Icons and folders:** make key images from text, emoji, ~3,400 brand logos or ~7,400 symbols, and
76
+ folder keys that open a sub-profile with a Back key.
77
+ - **Pictures:** `render_profile` shows the deck as OpenDeck draws it, with staged changes outlined or
78
+ before and after side by side, so you can check a layout before applying it.
79
+ - **Home Assistant:** find entities by name, area or domain, and get ready-made settings for a key
80
+ that controls one, using the login already saved in the Home Assistant plugin.
81
+ - **Undo:** every apply takes a backup, and `restore_backup` puts things back.
82
+
83
+ ## Install
84
+
85
+ You need OpenDeck 2.14, [uv](https://docs.astral.sh/uv/) and Claude Code (or another MCP client, see
86
+ below). uv fetches a suitable Python by itself.
87
+
88
+ ```sh
89
+ claude mcp add opendeck --scope user -- uvx opendeck-mcp@0.3.1
90
+ ```
91
+
92
+ Then restart Claude Code and ask it something like *"what's on my OpenDeck?"*.
93
+
94
+ It's on [PyPI](https://pypi.org/project/opendeck-mcp/) and in the
95
+ [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.kastro5/opendeck-mcp`. To run
96
+ straight from GitHub instead: `uvx --from git+https://github.com/kastro5/opendeck-mcp@v0.3.1 opendeck-mcp`.
97
+
98
+ OpenDeck's folders are found automatically. If yours live somewhere unusual, override them with
99
+ environment variables (`claude mcp add … -e NAME=value`):
100
+
101
+ | Variable | Default (Windows) |
102
+ |---|---|
103
+ | `OPENDECK_CONFIG_DIR` | `%APPDATA%\opendeck` |
104
+ | `OPENDECK_EXE` | `%LOCALAPPDATA%\OpenDeck\opendeck.exe` |
105
+ | `OPENDECK_LOG_DIR` | `%LOCALAPPDATA%\opendeck\logs` |
106
+ | `OPENDECK_MCP_DATA_DIR` | `%LOCALAPPDATA%\opendeck-mcp` (the server's own plan, backups and cache) |
107
+ | `OPENDECK_MCP_HA_VERIFY_TLS` | `1`; set `0` for a Home Assistant on https with a self-signed certificate |
108
+ | `OPENDECK_MCP_TOKEN` | the HTTP token (default: generated once, see below) |
109
+
110
+ ### Upgrading
111
+
112
+ The version is pinned in the command, so replace the registration with the new tag:
113
+
114
+ ```sh
115
+ claude mcp remove opendeck --scope user
116
+ claude mcp add opendeck --scope user -- uvx opendeck-mcp@0.3.1
117
+ ```
118
+
119
+ The first apply after an upgrade also updates the companion bridge plugin (see
120
+ [How it works](#how-it-works)), in the same restart.
121
+
122
+ ### Other MCP clients
123
+
124
+ Any client that can start a stdio server works. The usual JSON configuration (Claude Desktop,
125
+ Cursor, VS Code and others) is:
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "opendeck": {
131
+ "command": "uvx",
132
+ "args": ["opendeck-mcp@0.3.1"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ### From another computer
139
+
140
+ The server has to run on the PC that runs OpenDeck, inside your logged-in desktop session: it
141
+ restarts OpenDeck and talks to it through its command line, which doesn't work from a service or an
142
+ SSH session. To use it from another machine (a laptop, a dev VM), serve it over HTTP there and
143
+ connect to it. [Tailscale](https://tailscale.com) or another private network is the recommended way
144
+ in; every request also needs a token.
145
+
146
+ On the OpenDeck PC (Windows):
147
+
148
+ ```sh
149
+ uvx opendeck-mcp@0.3.1 install-autostart --host <tailscale-ip>
150
+ uvx opendeck-mcp@0.3.1 token --host <tailscale-ip>
151
+ ```
152
+
153
+ The first command puts a small script in your Startup folder that starts the server, without a
154
+ window, at every logon, and starts it now. The second prints the command to run on the other
155
+ machine:
156
+
157
+ ```sh
158
+ claude mcp add --transport http opendeck http://<tailscale-ip>:8765/mcp --header "Authorization: Bearer <token>"
159
+ ```
160
+
161
+ - `tailscale ip -4` shows the address. The server refuses `0.0.0.0` (every network) unless you add
162
+ `--allow-all-interfaces`.
163
+ - To connect by name (e.g. a MagicDNS name), add `--allowed-host <name>`; other `Host` headers are
164
+ rejected.
165
+ - `opendeck-mcp token --reset` makes a new token, and `opendeck-mcp uninstall-autostart` removes
166
+ the autostart and stops the server.
167
+ - Windows Firewall must allow inbound TCP 8765 on that network; it asks the first time if it isn't
168
+ already allowed.
169
+ - On Linux or macOS, run `opendeck-mcp serve --host <ip>` from a systemd user service or LaunchAgent.
170
+
171
+ ## Using it
172
+
173
+ Just describe what you want. Under the hood, the agent:
174
+
175
+ 1. calls `get_overview` and `show_profile` to see your devices and keys;
176
+ 2. uses `list_actions` / `describe_action` to find the right action and learn its settings;
177
+ 3. stages edits with `set_key`, `add_child`, `manage_profile` and so on;
178
+ 4. calls `preview_changes` (and `render_profile` for a picture), shows you the result, and waits for your
179
+ OK;
180
+ 5. calls `apply_changes`, which restarts OpenDeck for a few seconds and reports the backup id.
181
+
182
+ Small tweaks skip all that. Changing an existing key's title, image or settings on the profile a
183
+ connected device is showing happens instantly, without a restart, and `undo_live` reverses it. Title
184
+ style, the shown state, other profiles and every structural change still go through staging.
185
+
186
+ For example, *"make a Fusion 360 profile with keys for my most-used sketch and solid tools, put the
187
+ rest in a folder, and switch to it whenever Fusion is in front"* builds a profile of Simulate Input
188
+ keys sending Fusion's shortcuts, with icons from `make_icon`, a "More" folder and the app rule.
189
+
190
+ Keys are addressed the way `show_profile` draws them:
191
+
192
+ ```
193
+ c1 c2 c3 c4 c5
194
+ r1 Multi Action "GitHub" Toggle Action · ·
195
+ r2 · Entity (custo… · Switch Profile ·
196
+ r3 Device Bright… · "rew" · (wide →)
197
+ ```
198
+
199
+ | Slot | Meaning |
200
+ |---|---|
201
+ | `r1c1` | row 1, column 1 |
202
+ | `key13` | raw position 13 (0-based) |
203
+ | `dial1` | first dial |
204
+ | `touch1` | first touchpoint |
205
+ | `r1c1/child2` | second action inside a Multi or Toggle Action |
206
+
207
+ ## How it works
208
+
209
+ OpenDeck has no API for changing its layout. It keeps profiles in memory and overwrites any file edited
210
+ while it runs. So `opendeck-mcp` works on the files, with OpenDeck stopped:
211
+
212
+ 1. **Staging.** Each edit is recorded as an operation in a plan. Nothing is written yet.
213
+ `show_profile` shows staged edits marked with `*`.
214
+ 2. **Applying.**
215
+ - First, OpenDeck is asked to save its in-memory state. This sends a profile switch through its command
216
+ line, which makes it write the selected profile.
217
+ - Then OpenDeck and its plugin processes are stopped.
218
+ - The plan is replayed onto the freshly read files. If something you staged was changed in OpenDeck's
219
+ UI in the meantime, apply reports a conflict and doesn't write anything.
220
+ - The files are backed up, written atomically, read back and re-validated. If anything fails, the
221
+ backup is restored automatically.
222
+ - Finally, OpenDeck is started again, detached and hidden.
223
+ 3. **Faithful files.** Action data is rebuilt from plugin manifests exactly as OpenDeck builds it, and
224
+ JSON is written byte-for-byte in OpenDeck's format. A profile OpenDeck can't read gets replaced by an
225
+ empty one, so every write is validated twice.
226
+ 4. **Device geometry.** OpenDeck doesn't store a device's rows × columns anywhere, so the first apply also
227
+ installs a tiny companion plugin, *OpenDeck MCP Bridge* (no actions, about 60 lines of JavaScript). It
228
+ records the geometry OpenDeck reports to plugins into its own settings file, where the server reads it.
229
+ It's versioned with the server, so a new release updates it on its next apply.
230
+ 5. **Images.** New key images are stored as `data:` URLs. OpenDeck accepts these and converts them to
231
+ files itself on its next save.
232
+
233
+ ### Instant edits
234
+
235
+ Titles and images are sent to the running OpenDeck through its command line (`--process-message`),
236
+ as `setTitle` and `setImage` events. Settings go over OpenDeck's local plugin websocket, posing as
237
+ the key's settings panel, because that is the only path on which OpenDeck tells the plugin its
238
+ settings changed. OpenDeck is then asked to save, and the file is checked. Before each edit the old
239
+ titles, images (as bytes) and settings are written to `live_journal.jsonl` in the server's data
240
+ folder, which `undo_live` replays.
241
+
242
+ ## Safety
243
+
244
+ - **Nothing changes until you confirm.** Staging tools never touch OpenDeck. `apply_changes`,
245
+ `restore_backup` and plugin installs all need a second call with `confirm=true`.
246
+ - **Backups.** Every apply first copies `profiles/`, `images/`, `applications.json` and anything else it
247
+ touches into `%LOCALAPPDATA%\opendeck-mcp\backups`. It keeps the last 30 backups (always at least 5).
248
+ Restoring also backs up the current state first. Backups contain your plugin settings, including
249
+ secrets, so they stay on your machine.
250
+ - **Secrets are redacted.** Anything that looks like a credential (token, password, API key, JWT, Bearer
251
+ value, …) is shown as `«redacted»` in settings, diffs, examples and logs. Passing `«redacted»` back keeps
252
+ the stored value.
253
+ - **Risky keys are flagged.** Run Command keys run commands, Simulate Input keys type into the focused
254
+ window, and Open URL keys with non-web URIs can launch programs. Each is marked ⚠ when staged and in the
255
+ preview.
256
+ - **Plugins are third-party code.** `stage_plugin` first downloads and inspects the plugin: author, version,
257
+ sha256, how it runs, which platforms it supports, and the catalogue's *AI-assisted* flag. It only stages
258
+ the install after you agree. Built-in plugins can't be removed.
259
+ - **Home Assistant.** The Home Assistant tools reuse the server URL and access token saved by the
260
+ Home Assistant plugin. The token is only sent to that server, never shown. `ha_call_service` acts
261
+ on your real home, so it needs a second call with `confirm=true`.
262
+ - **Instant edits aren't backed up.** They don't go through apply, so instead the previous titles,
263
+ images and settings are kept in a journal (the last 200 edits) that `undo_live` replays.
264
+ - **The HTTP token is a key to your deck.** Anyone with it who can reach the port can change keys,
265
+ install plugins and, through Run Command keys, run programs. Keep the server on a private address,
266
+ and `opendeck-mcp token --reset` if the token leaks.
267
+ - **Version check.** Apply refuses to run on OpenDeck versions other than 2.14.x unless you allow it
268
+ explicitly.
269
+
270
+ ### Network and files
271
+
272
+ - **Internet:** only the plugin catalogues and plugin downloads (`find_plugins`, `stage_plugin`), and
273
+ the title fonts for `render_profile` and the icon sets for `make_icon`, fetched once each from
274
+ jsDelivr and cached. Nothing else, and no telemetry.
275
+ - **Local network:** your Home Assistant, only when the Home Assistant tools are used.
276
+ - **Files** in `%LOCALAPPDATA%\opendeck-mcp`: `plan.json` (staged changes), `backups/`,
277
+ `live_journal.jsonl`, `icons/` (made by `make_icon`), `cache/` (catalogues, fonts, icon sets),
278
+ `device_overrides.json`, and for HTTP
279
+ `http_token` and `http.log`. Delete the folder to reset everything.
280
+
281
+ ## Tools
282
+
283
+ | Kind | Tools |
284
+ |---|---|
285
+ | Read | `get_overview`, `show_profile`, `render_profile`, `list_actions`, `describe_action`, `get_plugin_settings`, `read_logs`, `get_app_rules`, `find_plugins`, `find_icons`, `preview_changes`, `list_backups` |
286
+ | Stage | `set_key`, `update_key`, `clear_key`, `move_key`, `add_child`, `remove_child`, `set_multi_delays`, `make_folder`, `manage_profile`, `set_app_rule`, `set_plugin_settings`, `stage_plugin`, `discard_changes`, `set_device_layout` |
287
+ | Icons | `make_icon` (saves a PNG to pass as `image`) |
288
+ | Apply | `apply_changes`, `restore_backup`, `undo_live` |
289
+ | Home Assistant | `ha_search`, `ha_describe_entity`, `ha_call_service` |
290
+
291
+ ## Limitations
292
+
293
+ - **Apply restarts OpenDeck** (about 5 seconds). Unsaved edits in an open settings panel are lost, and
294
+ plugins restart.
295
+ - **Some settings need examples.** Settings are known in detail for OpenDeck's own actions and the Starter
296
+ Pack. For other plugins, the agent works from hints on the plugin's settings page and from your existing
297
+ keys. If those aren't enough, place the action and finish setting it up in OpenDeck's UI.
298
+ - **App rule names.** App rules match the name OpenDeck gives the focused app: on Windows the
299
+ executable's description (e.g. `Autodesk Fusion`, `Visual Studio Code`), otherwise its file name.
300
+ `get_app_rules` lists the exact names of the apps you have open.
301
+ - **Tested platform.** Tested on Windows 11 with OpenDeck 2.14.1, a Ulanzi D200H and Home Assistant.
302
+ The macOS and Linux paths and process handling are written but untested.
303
+ - **Limited live control.** Title, image and settings edits apply instantly; everything else needs
304
+ a restart. It doesn't press keys or switch profiles live.
305
+ - **Pictures are close, not exact.** `render_profile` can't see images a plugin draws while running (a
306
+ clock, a live Home Assistant state) unless OpenDeck has saved them, and OpenDeck's own Multi/Toggle
307
+ Action icons are drawn as look-alikes.
308
+ - **An open settings panel goes quiet.** After an instant settings edit, a settings panel already open
309
+ in OpenDeck's window for that key stops updating until it's reopened.
310
+
311
+ ## Troubleshooting
312
+
313
+ - **"OpenDeck's executable wasn't found" / "config folder wasn't found"**: set `OPENDECK_EXE` /
314
+ `OPENDECK_CONFIG_DIR` (see [Install](#install)).
315
+ - **Apply refuses: untested OpenDeck version.** Check the changelog for a release that supports
316
+ yours; `apply_changes(allow_untested_version=true)` overrides it at your own risk, with a backup.
317
+ - **Apply reports a conflict.** A key you staged was changed in OpenDeck meanwhile. Look at
318
+ `preview_changes`, then discard and re-stage it, or apply with `on_conflict="skip"`.
319
+ - **A device shows "geometry unknown".** It's learned by the bridge plugin, installed with the first
320
+ apply. Until then `set_device_layout` sets rows and columns by hand.
321
+ - **Undo an apply:** `list_backups`, then `restore_backup`.
322
+ - **HTTP client gets 401 / 421.** 401: wrong or missing token (`opendeck-mcp token`). 421: the client
323
+ used a host name the server doesn't know; add it with `--allowed-host`. The server logs to
324
+ `http.log` in its data folder.
325
+
326
+ ## Development
327
+
328
+ ```sh
329
+ git clone https://github.com/kastro5/opendeck-mcp && cd opendeck-mcp
330
+ uv sync
331
+ uv run pytest # unit tests, against a scrubbed fixture config
332
+ uv run pytest -m live # also check snapshots against your real OpenDeck install (read-only)
333
+ claude mcp add opendeck-dev -- uv --directory /path/to/opendeck-mcp run opendeck-mcp
334
+ ```
335
+
336
+ Test fixtures are real OpenDeck profiles with every setting value replaced
337
+ (`scripts/scrub_fixture.py`), plus trimmed plugin manifests with empty icon files.
338
+
339
+ ## License
340
+
341
+ MIT. Not affiliated with OpenDeck or Elgato.
342
+
343
+ Icons and fonts are downloaded at run time, not bundled:
344
+ [Material Design Icons](https://pictogrammers.com/library/mdi/) (Apache 2.0),
345
+ [Simple Icons](https://simpleicons.org) (CC0; brand logos remain their owners' trademarks),
346
+ [Twemoji](https://github.com/jdecked/twemoji) (graphics CC-BY 4.0, © Twitter/X and contributors), and
347
+ OpenDeck's title fonts from [Google Fonts](https://github.com/google/fonts) (OFL).