mcp-server-harmony 0.1.0__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 (48) hide show
  1. mcp_server_harmony-0.1.0/LICENSE +21 -0
  2. mcp_server_harmony-0.1.0/PKG-INFO +356 -0
  3. mcp_server_harmony-0.1.0/README.md +320 -0
  4. mcp_server_harmony-0.1.0/pyproject.toml +77 -0
  5. mcp_server_harmony-0.1.0/setup.cfg +4 -0
  6. mcp_server_harmony-0.1.0/src/harmony_mcp/__init__.py +3 -0
  7. mcp_server_harmony-0.1.0/src/harmony_mcp/assumptions.py +157 -0
  8. mcp_server_harmony-0.1.0/src/harmony_mcp/catalog.py +181 -0
  9. mcp_server_harmony-0.1.0/src/harmony_mcp/cli.py +302 -0
  10. mcp_server_harmony-0.1.0/src/harmony_mcp/client.py +428 -0
  11. mcp_server_harmony-0.1.0/src/harmony_mcp/config.py +211 -0
  12. mcp_server_harmony-0.1.0/src/harmony_mcp/discovery.py +103 -0
  13. mcp_server_harmony-0.1.0/src/harmony_mcp/doctor.py +278 -0
  14. mcp_server_harmony-0.1.0/src/harmony_mcp/hubs.py +243 -0
  15. mcp_server_harmony-0.1.0/src/harmony_mcp/limits.py +25 -0
  16. mcp_server_harmony-0.1.0/src/harmony_mcp/logsafe.py +156 -0
  17. mcp_server_harmony-0.1.0/src/harmony_mcp/ratelimit.py +213 -0
  18. mcp_server_harmony-0.1.0/src/harmony_mcp/registry.py +137 -0
  19. mcp_server_harmony-0.1.0/src/harmony_mcp/remote.py +417 -0
  20. mcp_server_harmony-0.1.0/src/harmony_mcp/server.py +348 -0
  21. mcp_server_harmony-0.1.0/src/harmony_mcp/sim/__init__.py +1 -0
  22. mcp_server_harmony-0.1.0/src/harmony_mcp/sim/fake_hub.py +346 -0
  23. mcp_server_harmony-0.1.0/src/harmony_mcp/sim/fixtures/den.json +62 -0
  24. mcp_server_harmony-0.1.0/src/harmony_mcp/sim/fixtures/living_room.json +133 -0
  25. mcp_server_harmony-0.1.0/src/harmony_mcp/tasks.py +131 -0
  26. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/PKG-INFO +356 -0
  27. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/SOURCES.txt +46 -0
  28. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/dependency_links.txt +1 -0
  29. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/entry_points.txt +2 -0
  30. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/requires.txt +13 -0
  31. mcp_server_harmony-0.1.0/src/mcp_server_harmony.egg-info/top_level.txt +1 -0
  32. mcp_server_harmony-0.1.0/tests/test_assumptions.py +54 -0
  33. mcp_server_harmony-0.1.0/tests/test_catalog.py +75 -0
  34. mcp_server_harmony-0.1.0/tests/test_cli.py +54 -0
  35. mcp_server_harmony-0.1.0/tests/test_client.py +235 -0
  36. mcp_server_harmony-0.1.0/tests/test_config.py +56 -0
  37. mcp_server_harmony-0.1.0/tests/test_contract.py +303 -0
  38. mcp_server_harmony-0.1.0/tests/test_logsafe.py +237 -0
  39. mcp_server_harmony-0.1.0/tests/test_multi_hub.py +124 -0
  40. mcp_server_harmony-0.1.0/tests/test_ratelimit.py +344 -0
  41. mcp_server_harmony-0.1.0/tests/test_recorded.py +73 -0
  42. mcp_server_harmony-0.1.0/tests/test_registry.py +152 -0
  43. mcp_server_harmony-0.1.0/tests/test_remote.py +359 -0
  44. mcp_server_harmony-0.1.0/tests/test_safety.py +220 -0
  45. mcp_server_harmony-0.1.0/tests/test_stdio.py +31 -0
  46. mcp_server_harmony-0.1.0/tests/test_tasks.py +304 -0
  47. mcp_server_harmony-0.1.0/tests/test_tooling.py +238 -0
  48. mcp_server_harmony-0.1.0/tests/test_tools.py +226 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicholas E. Andrade
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,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-harmony
3
+ Version: 0.1.0
4
+ Summary: MCP server for the Logitech Harmony Hub (local websocket API), activity-first and allow-listed by the hub's own config
5
+ Author: Nicholas E. Andrade
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SDNick484/mcp-server-harmony
8
+ Project-URL: Issues, https://github.com/SDNick484/mcp-server-harmony/issues
9
+ Project-URL: Changelog, https://github.com/SDNick484/mcp-server-harmony/blob/main/CHANGELOG.md
10
+ Keywords: mcp,mcp-server,harmony,logitech-harmony,harmony-hub,home-automation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Home Automation
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp<3,>=2.3
24
+ Requires-Dist: aioharmony<2,>=1.0.10
25
+ Requires-Dist: aiohttp>=3.9
26
+ Requires-Dist: pyjwt[crypto]>=2.10
27
+ Requires-Dist: uvicorn>=0.31
28
+ Requires-Dist: typing_extensions>=4.7
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: anyio>=4; extra == "dev"
32
+ Requires-Dist: ruff>=0.6; extra == "dev"
33
+ Requires-Dist: mypy>=1.11; extra == "dev"
34
+ Requires-Dist: httpx2; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # mcp-server-harmony
38
+
39
+ [![CI](https://github.com/SDNick484/mcp-server-harmony/actions/workflows/ci.yml/badge.svg)](https://github.com/SDNick484/mcp-server-harmony/actions/workflows/ci.yml)
40
+
41
+ An [MCP](https://modelcontextprotocol.io) server for **Logitech Harmony Hubs**, one or many
42
+ (say, one per TV). It talks to each hub directly over its local websocket API, with no Home
43
+ Assistant and no Logitech cloud. It lets an MCP client such as Claude start and stop
44
+ activities, press buttons, and read what's on. It works with any remote paired to a hub (Elite,
45
+ Companion, 950), since the server talks to the hub, not the remote.
46
+
47
+ > **Status: first contact with real hubs done (2026-10-14).** Two hubs connected, reported their
48
+ > state and names, and powered off on request. Starting activities, button presses, reconnects
49
+ > and config pushes are still verified against the simulator only: the tests run the real
50
+ > aioharmony library against a wire-level fake hub. Every protocol detail that hasn't been
51
+ > confirmed on hardware is a named assumption (see
52
+ > [Verification status](#verification-status)), and
53
+ > [HARDWARE_VALIDATION.md](HARDWARE_VALIDATION.md) is the checklist for confirming them.
54
+
55
+ Not affiliated with Logitech. Built on [aioharmony](https://github.com/Harmony-Libs/aioharmony),
56
+ the library behind Home Assistant's Harmony integration.
57
+
58
+ ## Design: activity-first
59
+
60
+ A Harmony is built around *activities* ("Watch Shield", "Listen to Music"): starting one
61
+ powers the right devices and sets their inputs. That's also the right level for a model:
62
+
63
+ - `start_activity("Watch Shield")` is one call; the hub handles the device choreography.
64
+ - `send_command("VolumeUp")` with no device goes **through the running activity**, so volume
65
+ reaches the receiver and Pause reaches the Shield without the model knowing your wiring.
66
+ - `send_command(device="Onkyo AV Receiver", ...)` is there for the cases activities don't cover.
67
+
68
+ ## Several hubs
69
+
70
+ One server drives all your hubs, each by its own name (the one set in the Harmony app). Tools
71
+ take an optional `hub`, and the model only needs it when a name is ambiguous:
72
+
73
+ - **Unique names just work.** `start_activity("Watch TV")` finds the one hub that has it.
74
+ - **A repeated name asks.** If both hubs have "Listen to Music", the tool returns an error
75
+ naming both hubs, and the model asks you or retries with `hub`.
76
+ - **Hub-qualified refs.** The list tools return a `ref` such as `"Den/Listen to Music"`, which
77
+ works anywhere a name does, so the model can copy one string.
78
+ - **Buttons follow the running activity**, when exactly one hub has one running. With both
79
+ TVs on, `send_command("VolumeUp")` asks which.
80
+ - **`power_off`** turns off the one hub that's on, asks if several are, and takes `hub="all"`
81
+ for every one. With `all`, each hub is reported separately, so one unreachable hub doesn't
82
+ stop the others.
83
+
84
+ The server never guesses between hubs: each one is a different TV, and a guess turns off the
85
+ wrong room.
86
+
87
+ ## Install
88
+
89
+ ```sh
90
+ python -m venv ~/.venvs/harmony && . ~/.venvs/harmony/bin/activate
91
+ pip install mcp-server-harmony
92
+ ```
93
+
94
+ From source instead (for the newest code, or to run the tests):
95
+
96
+ ```sh
97
+ git clone https://github.com/SDNick484/mcp-server-harmony.git && cd mcp-server-harmony
98
+ python -m venv .venv && . .venv/bin/activate
99
+ pip install -e ".[dev]" && pytest
100
+ ```
101
+
102
+ Requires Python 3.11+.
103
+
104
+ ## First contact
105
+
106
+ There's no pairing: a hub's local API answers anything on the LAN at **TCP 8088**.
107
+
108
+ ```sh
109
+ mcp-server-harmony discover # SSDP: lists hubs that answer
110
+ mcp-server-harmony check --discover # ...and adds them all
111
+ mcp-server-harmony check --host 192.168.1.60 # or one at a time, by IP
112
+ mcp-server-harmony doctor # each hub, layer by layer, with hints
113
+ mcp-server-harmony call start_activity activity="Den/Watch TV" # one tool, as the model calls it
114
+ ```
115
+
116
+ `check` connects, prints the hub's name, activities and devices (the exact names the model
117
+ will use), and adds the hub to `~/.config/mcp-server-harmony/config.json` with its name. Plain
118
+ `check` rechecks every configured hub. SSDP is multicast, so it doesn't cross VLANs or guest
119
+ networks; `--host` always works.
120
+
121
+ `doctor` checks TCP, provisioning, the websocket and the catalog for each hub, and warns about
122
+ activity names that more than one hub has. `doctor --dump DIR` saves a redacted copy of each
123
+ hub's config for `tests/fixtures/recorded/`.
124
+
125
+ ## Use it with an MCP client
126
+
127
+ Local (stdio): Claude Code and Claude Desktop launch it themselves.
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "harmony": { "command": "/path/to/mcp-server-harmony/.venv/bin/mcp-server-harmony" }
133
+ }
134
+ }
135
+ ```
136
+
137
+ For Claude Code: `claude mcp add harmony -- /path/to/.venv/bin/mcp-server-harmony`.
138
+
139
+ As a service (Streamable HTTP), to share one instance between Claude Code, Claude Desktop and
140
+ the mobile app through a Cloudflare Tunnel with Cloudflare Access in front:
141
+
142
+ ```sh
143
+ CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com CF_ACCESS_AUD=<aud tag> \
144
+ mcp-server-harmony serve --http --public-host mcp.example.com # 127.0.0.1:8713/harmony/mcp
145
+ ```
146
+
147
+ Every request must carry a valid Access JWT (`Cf-Access-Jwt-Assertion`). The checks and the
148
+ flags are documented in `src/harmony_mcp/remote.py`. The Host-header (DNS-rebinding) checks stay on
149
+ even with `--bind 0.0.0.0`: for LAN clients, also pass `--public-host <this machine's LAN IP>`.
150
+
151
+ ### As an OpenRC service on Alpine (e.g. a Proxmox LXC)
152
+
153
+ `deploy/alpine/` installs it the same way as mcp-server-webos, so the two can share an LXC: a venv
154
+ in `/opt/mcp-server-harmony`, a `mcp-harmony` system user, settings in
155
+ `/etc/mcp-server-harmony/env`, the hub list in `/var/lib/mcp-server-harmony/config.json`, and
156
+ `supervise-daemon` restarting it if it exits. The LXC needs TCP 8088 to each hub.
157
+
158
+ ```sh
159
+ git clone https://github.com/SDNick484/mcp-server-harmony.git && cd mcp-server-harmony
160
+ sh deploy/alpine/install.sh # as root; re-run to upgrade
161
+ su -s /bin/sh mcp-harmony -c 'HARMONY_CONFIG_DIR=/var/lib/mcp-server-harmony \
162
+ /opt/mcp-server-harmony/venv/bin/mcp-server-harmony check --host <hub ip>' # once per hub
163
+ vi /etc/mcp-server-harmony/env # public host and Access settings
164
+ rc-update add mcp-server-harmony default && rc-service mcp-server-harmony start
165
+ ```
166
+
167
+ Logs are in `/var/log/mcp-server-harmony/server.log`. CI runs `deploy/alpine/smoke-test.sh` in a
168
+ stock `python:3.12-alpine` container: install, the test suite on musl, the service under OpenRC,
169
+ and an MCP call over HTTP reaching a simulated hub.
170
+
171
+ ## Configuration
172
+
173
+ | Setting | Default | What it does |
174
+ | ------------------------ | ------------------------------------- | --------------------------------------------------------------- |
175
+ | `HARMONY_HOSTS` | *(from `config.json`)* | Comma-separated hub addresses; replaces the file's list |
176
+ | `HARMONY_CONFIG_DIR` | `$XDG_CONFIG_HOME/mcp-server-harmony` | Where `config.json` lives |
177
+ | `HARMONY_DRY_RUN` | off | Read the hubs, send nothing that changes anything (`serve --dry-run` too) |
178
+ | `HARMONY_LOG_UNREDACTED` | off | Don't mask IPs and MACs in logs (`--no-redact`); secrets stay masked |
179
+
180
+ `config.json` (written by `check`, safe to edit by hand):
181
+
182
+ ```json
183
+ {
184
+ "hubs": [
185
+ { "host": "192.168.1.60", "name": "Living Room" },
186
+ { "host": "192.168.1.61", "name": "Den" }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ `name` is optional: without it, the hub's own name is used once it connects. Write your own to
192
+ call a hub something else; `check` fills in missing names but never replaces one. A hub also
193
+ answers to its address. Optionally add `"protocol": "WEBSOCKETS"` (or `"XMPP"`) to skip the
194
+ transport race. `HARMONY_HOST` (one hub) and the older `{"host": ...}` form still work.
195
+ Problems (a host with a port, duplicate hosts or names, invalid JSON) are reported by name at
196
+ startup and by `doctor`.
197
+
198
+ ## Tools
199
+
200
+ | Tool | What it does |
201
+ | ----------------- | ------------------------------------------------------------------------------------------------ |
202
+ | `get_status` | Start here: every hub's name, reachable, firmware, on/off, running activity, and any `transition` |
203
+ | `list_activities` | Activities on each hub, with `ref`, marking the running ones |
204
+ | `list_devices` | Devices on each hub, with `ref`, manufacturer, model and command count |
205
+ | `list_commands` | Commands for an activity or device (default: the running activity), and which device gets each |
206
+ | `start_activity` | Start an activity and wait until its hub finishes; `unchanged` if it's already running |
207
+ | `power_off` | Run a hub's power-off (or `hub="all"`); `unchanged` if it's already off |
208
+ | `send_command` | Press a command 1-10 times (`repeat`), optionally held (`hold_ms`) or spaced (`delay_ms`) |
209
+
210
+ Every tool except `get_status` takes an optional `hub`. Actions return
211
+ `{hub, outcome: done | unchanged | dry_run | error, detail, sent}`, where `sent` lists the frames
212
+ that went to the hub. The server also offers resources (`harmony://hubs`,
213
+ `harmony://hubs/{hub}`) with each hub's catalog, and a `fix_stuck_remote` prompt for the
214
+ Shield's Bluetooth keyboard getting stuck.
215
+
216
+ ### A session against the simulator
217
+
218
+ Two hubs that both have "Listen to Music". (Output from `simulate`, trimmed.)
219
+
220
+ ```
221
+ > get_status
222
+ {"dry_run": false, "hubs": [
223
+ {"hub": "Living Room", "reachable": true, "power": "off", "current_activity": null, "transition": null, ...},
224
+ {"hub": "Den", "reachable": true, "power": "off", "current_activity": null, "transition": null, ...}]}
225
+
226
+ > start_activity {"activity": "Listen to Music"}
227
+ ERROR: Activity 'Listen to Music' exists on Living Room, Den; pass hub to choose.
228
+
229
+ > list_activities {"hub": "Den"}
230
+ [{"hub": "Den", "name": "Watch TV", "ref": "Den/Watch TV", "running": false},
231
+ {"hub": "Den", "name": "Listen to Music", "ref": "Den/Listen to Music", "running": false}]
232
+
233
+ > start_activity {"activity": "Den/Listen to Music"}
234
+ {"hub": "Den", "outcome": "done", "detail": "Start Listen to Music on Den",
235
+ "sent": ["runactivity activityId=39000002 (Listen to Music)"]}
236
+
237
+ > start_activity {"activity": "Watch Shield"}
238
+ {"hub": "Living Room", "outcome": "done", "detail": "Start Watch Shield on Living Room", ...}
239
+
240
+ > send_command {"command": "VolumeUp", "repeat": 3}
241
+ ERROR: Activities are running on Living Room (Watch Shield), Den (Listen to Music); pass hub to choose.
242
+
243
+ > send_command {"command": "VolumeUp", "hub": "Living Room", "repeat": 3}
244
+ {"hub": "Living Room", "outcome": "done", "detail": "Send VolumeUp x3 to Onkyo AV Receiver on Living Room",
245
+ "sent": ["holdAction press+release Onkyo AV Receiver/VolumeUp", ...]}
246
+
247
+ > power_off
248
+ ERROR: Several hubs are on (Living Room, Den); pass hub, or hub='all' for every one.
249
+
250
+ > power_off {"hub": "all"}
251
+ {"results": [{"hub": "Living Room", "outcome": "done", "detail": "Power off Living Room", ...},
252
+ {"hub": "Den", "outcome": "done", "detail": "Power off Den", ...}]}
253
+ ```
254
+
255
+ ## Safety design
256
+
257
+ - **The hub's own config is the allow-list.** Activity, device and command names are resolved
258
+ against what the hub already knows. There's no way to send a raw IR code, an unknown device
259
+ id, or a command from one device to another.
260
+ - **Left out on purpose:** `sync` (talks to Logitech's cloud), `change_channel`, and anything
261
+ that edits the hub's configuration.
262
+ - **No interleaving with activity sequences.** While a hub is starting an activity or powering
263
+ off, its commands are refused with "busy, try again", since a press mid-sequence can land on
264
+ a device that's still warming up.
265
+ - **Rate limits and caps, per hub.** Per call: at most 10 repeats, held up to 3 s, 100-2000 ms
266
+ apart, 15 s in total. Across calls (a model in a loop): 20 presses in a burst, then 4 a
267
+ second; 4 activity changes in a burst, then one per 15 s. A call over the limit is refused
268
+ whole, never half-sent.
269
+ - **Dry run.** `serve --dry-run` (or `call --dry-run`) reads the hubs and reports what it
270
+ would send.
271
+ - **Resilient.** Each hub reconnects with backoff, and re-reads its state after a reconnect.
272
+ An offline hub doesn't block the others, and shutdown is bounded per hub.
273
+ - **Logs are redacted**: IP addresses and MACs (unless `--no-redact`), and always secrets (JWTs,
274
+ bearer tokens, keys, URL passwords). They go to stderr; stdout is the MCP transport.
275
+ - Every tool carries a title and MCP annotations. Nothing is marked destructive (activities and
276
+ button presses change what's on, not data). `send_command` is the one non-idempotent action,
277
+ since VolumeUp twice is +2.
278
+
279
+ ## Troubleshooting
280
+
281
+ | Message | Meaning |
282
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
283
+ | `No Harmony hub configured.` | Run `check --host <ip>` once per hub, or set `HARMONY_HOSTS` (in the client's config) |
284
+ | `Can't reach the Harmony hub 'Den' at ...` | Offline, its IP changed, or TCP 8088 is blocked (a VLAN in between). Other hubs keep working |
285
+ | `... exists on Living Room, Den; pass hub to choose.` | The name is on more than one hub: the model should ask which TV, or use a ref |
286
+ | `Den is busy starting ...` | An activity is mid-sequence. Wait until `get_status` shows `transition: null` for that hub |
287
+ | `Rate limit: refusing to ...` | Many presses in a short time. Wait and retry if it was deliberate |
288
+
289
+ `doctor` says which layer failed for each hub and what to check.
290
+
291
+ ## Development
292
+
293
+ ```sh
294
+ pip install -e ".[dev]"
295
+ pytest -q # everything, no hardware: real aioharmony against fake hubs over real sockets
296
+ ruff check . && ruff format --check . && mypy
297
+ ```
298
+
299
+ The simulator runs the same fake hubs as real services on 127.0.0.1 and 127.0.0.2:
300
+
301
+ ```sh
302
+ mcp-server-harmony simulate --write-config /tmp/hcfg # two hubs; --hubs 1 for one, --flaky N to drop connections
303
+ HARMONY_CONFIG_DIR=/tmp/hcfg mcp-server-harmony doctor
304
+ ```
305
+
306
+ (On macOS, add the second loopback address first: `sudo ifconfig lo0 alias 127.0.0.2`.)
307
+
308
+ What the tests cover:
309
+
310
+ | File | Covers |
311
+ | --------------------- | ------------------------------------------------------------------------------------------------ |
312
+ | `test_contract.py` | The real aioharmony against the fake hub over a socket: the wire protocol, faults, reconnects |
313
+ | `test_catalog.py` | Parsing a hub's config into the allow-list |
314
+ | `test_client.py` | One hub: start, power off, presses, the busy guard |
315
+ | `test_multi_hub.py` | Several hubs: ambiguity, refs, an offline hub, partial failures |
316
+ | `test_tools.py` | The MCP contract the model sees (schemas, annotations, structured results) |
317
+ | `test_safety.py` | Dry run, rate limits, caps, config validation, shutdown |
318
+ | `test_tooling.py` | `doctor`, `discover`, `call`, `simulate`, log redaction |
319
+ | `test_recorded.py` | Replays configs captured from real hubs (`doctor --dump`) |
320
+ | `test_assumptions.py` | Every assumption appears here and in HARDWARE_VALIDATION.md |
321
+
322
+ ## Verification status
323
+
324
+ These are the protocol assumptions the code depends on (claim, source, and for verified ones the
325
+ evidence, in `src/harmony_mcp/assumptions.py`). `hardware-verified` means seen working on a real hub;
326
+ the rest are verified against the simulator only:
327
+
328
+ | Assumption | Confidence | Status |
329
+ | ------------------ | ---------- | -------------- |
330
+ | `H-PORT` | high | hardware-verified |
331
+ | `H-PROVISION` | high | hardware-verified |
332
+ | `H-FRAMES` | high | hardware-verified |
333
+ | `H-CONFIG-SHAPE` | high | simulator-only |
334
+ | `H-POWEROFF` | high | hardware-verified |
335
+ | `H-NAME` | high | hardware-verified |
336
+ | `H-START-SEQUENCE` | medium | simulator-only |
337
+ | `H-PRESS-SILENT` | medium | simulator-only |
338
+ | `H-PRESS-ERROR` | low | simulator-only |
339
+ | `H-HOLD` | low | simulator-only |
340
+ | `H-CONFIG-PUSH` | medium | simulator-only |
341
+ | `H-RECONNECT` | high | simulator-only |
342
+ | `H-SSDP` | medium | simulator-only |
343
+ | `H-TIMING` | low | simulator-only |
344
+
345
+ "High" means aioharmony itself depends on it, and Home Assistant's Harmony integration runs on
346
+ aioharmony. `test_assumptions.py` fails if this table and `assumptions.py` disagree.
347
+
348
+ ## Roadmap
349
+
350
+ - Validate on hardware ([HARDWARE_VALIDATION.md](HARDWARE_VALIDATION.md))
351
+ - Surface activity changes as MCP resource updates, once the Claude apps support subscriptions
352
+ - Publish to the MCP registry
353
+
354
+ ## License
355
+
356
+ MIT. See [LICENSE](LICENSE).