mcp-server-shieldtv 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 (47) hide show
  1. mcp_server_shieldtv-0.1.0/LICENSE +21 -0
  2. mcp_server_shieldtv-0.1.0/PKG-INFO +573 -0
  3. mcp_server_shieldtv-0.1.0/README.md +536 -0
  4. mcp_server_shieldtv-0.1.0/pyproject.toml +70 -0
  5. mcp_server_shieldtv-0.1.0/setup.cfg +4 -0
  6. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/PKG-INFO +573 -0
  7. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/SOURCES.txt +45 -0
  8. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/dependency_links.txt +1 -0
  9. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/entry_points.txt +2 -0
  10. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/requires.txt +14 -0
  11. mcp_server_shieldtv-0.1.0/src/mcp_server_shieldtv.egg-info/top_level.txt +1 -0
  12. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/__init__.py +5 -0
  13. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/adb.py +478 -0
  14. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/assumptions.py +190 -0
  15. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/cli.py +442 -0
  16. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/client.py +542 -0
  17. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/config.py +258 -0
  18. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/discovery.py +36 -0
  19. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/doctor.py +276 -0
  20. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/logsafe.py +309 -0
  21. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/lookup.py +60 -0
  22. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/ratelimit.py +226 -0
  23. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/registry.py +141 -0
  24. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/remote.py +516 -0
  25. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/server.py +376 -0
  26. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/sim/__init__.py +1 -0
  27. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/sim/fake_shield.py +553 -0
  28. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/tasks.py +174 -0
  29. mcp_server_shieldtv-0.1.0/src/shieldtv_mcp/tools.py +29 -0
  30. mcp_server_shieldtv-0.1.0/tests/test_adb.py +388 -0
  31. mcp_server_shieldtv-0.1.0/tests/test_assumptions.py +85 -0
  32. mcp_server_shieldtv-0.1.0/tests/test_cli.py +38 -0
  33. mcp_server_shieldtv-0.1.0/tests/test_client.py +228 -0
  34. mcp_server_shieldtv-0.1.0/tests/test_config.py +139 -0
  35. mcp_server_shieldtv-0.1.0/tests/test_http_tools.py +98 -0
  36. mcp_server_shieldtv-0.1.0/tests/test_logsafe.py +521 -0
  37. mcp_server_shieldtv-0.1.0/tests/test_lookup.py +41 -0
  38. mcp_server_shieldtv-0.1.0/tests/test_ratelimit.py +382 -0
  39. mcp_server_shieldtv-0.1.0/tests/test_registry.py +175 -0
  40. mcp_server_shieldtv-0.1.0/tests/test_remote.py +571 -0
  41. mcp_server_shieldtv-0.1.0/tests/test_resources.py +73 -0
  42. mcp_server_shieldtv-0.1.0/tests/test_stdio.py +45 -0
  43. mcp_server_shieldtv-0.1.0/tests/test_tasks.py +425 -0
  44. mcp_server_shieldtv-0.1.0/tests/test_tool_annotations.py +18 -0
  45. mcp_server_shieldtv-0.1.0/tests/test_tooling.py +101 -0
  46. mcp_server_shieldtv-0.1.0/tests/test_tools.py +286 -0
  47. mcp_server_shieldtv-0.1.0/tests/test_wire.py +341 -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,573 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-shieldtv
3
+ Version: 0.1.0
4
+ Summary: MCP server for NVIDIA Shield TV (Android TV Remote protocol v2) with a one-time pairing CLI
5
+ Author: Nicholas E. Andrade
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SDNick484/mcp-server-shieldtv
8
+ Project-URL: Issues, https://github.com/SDNick484/mcp-server-shieldtv/issues
9
+ Project-URL: Changelog, https://github.com/SDNick484/mcp-server-shieldtv/blob/main/CHANGELOG.md
10
+ Keywords: mcp,mcp-server,nvidia-shield,shield-tv,android-tv,home-automation
11
+ Classifier: Development Status :: 2 - Pre-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: androidtvremote2>=0.3.2
25
+ Requires-Dist: zeroconf>=0.130
26
+ Requires-Dist: typing_extensions>=4.7
27
+ Requires-Dist: adb-shell[async]>=0.4.4
28
+ Requires-Dist: pyjwt[crypto]>=2.10
29
+ Requires-Dist: uvicorn>=0.31
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8; extra == "dev"
32
+ Requires-Dist: anyio>=4; extra == "dev"
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Requires-Dist: mypy>=1.11; extra == "dev"
35
+ Requires-Dist: httpx2; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # mcp-server-shieldtv
39
+
40
+ [![CI](https://github.com/SDNick484/mcp-server-shieldtv/actions/workflows/ci.yml/badge.svg)](https://github.com/SDNick484/mcp-server-shieldtv/actions/workflows/ci.yml)
41
+
42
+ An [MCP](https://modelcontextprotocol.io) server for the **NVIDIA Shield TV**, built on the
43
+ Android TV Remote protocol v2 (the same protocol the Google TV phone app uses). It lets an
44
+ MCP client such as Claude press remote keys, launch apps, wake or sleep the Shield, and
45
+ read its power state and foreground app.
46
+
47
+ > **Status.** Pairing, `get_status`, keys, `launch_app`, `set_power` and the ADB tools were
48
+ > verified on a real Shield (remote service 7.00) in 2026-10. The features added since
49
+ > (pairing retries, reconnect and rediscovery hardening, dry run, `doctor`, resources and
50
+ > prompts, HTTP, the Alpine service) are **verified against the simulator only**, a
51
+ > wire-level fake that the real protocol library pairs and connects with.
52
+ > [Verification status](#verification-status) lists every protocol assumption and whether a
53
+ > Shield has confirmed it; [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/HARDWARE_VALIDATION.md) is the checklist.
54
+
55
+ It's one of four sibling servers with the same conventions (dry run, `doctor`, typed results,
56
+ Streamable HTTP behind Cloudflare Access):
57
+ [mcp-server-onkyo](https://github.com/SDNick484/mcp-server-onkyo),
58
+ [mcp-server-harmony](https://github.com/SDNick484/mcp-server-harmony) and
59
+ [mcp-server-sofabaton](https://github.com/SDNick484/mcp-server-sofabaton).
60
+
61
+ This is not an official NVIDIA project, and NVIDIA publishes no MCP server for the Shield.
62
+
63
+ ## Why this protocol
64
+
65
+ | | Remote protocol v2 (this project) | ADB |
66
+ |---|---|---|
67
+ | Setup | One-time pairing with an on-screen code | Enable *Network debugging* on the Shield |
68
+ | Good for | D-pad, media keys, app launch, power/app state | Deep device inspection |
69
+ | Risk if exposed to an LLM | Bounded: it can only press keys | High: `adb shell` is arbitrary code execution |
70
+
71
+ Three optional ADB tools do what the remote protocol can't: `get_now_playing`,
72
+ `get_remotes` and `reboot_shield`. They are off until you run `adb-setup`, and they run
73
+ only fixed commands; the server never exposes a raw shell (see
74
+ [ADB tools](#adb-tools-optional)).
75
+
76
+ ## Install
77
+
78
+ ```sh
79
+ python -m venv ~/.venvs/shieldtv && . ~/.venvs/shieldtv/bin/activate
80
+ pip install mcp-server-shieldtv
81
+ ```
82
+
83
+ From source instead (for the newest code, or to run the tests):
84
+
85
+ ```sh
86
+ git clone https://github.com/SDNick484/mcp-server-shieldtv.git && cd mcp-server-shieldtv
87
+ python -m venv .venv && . .venv/bin/activate
88
+ pip install -e ".[dev]" && pytest
89
+ ```
90
+
91
+ Requires Python 3.11+.
92
+
93
+ ## Pair with your Shield (once)
94
+
95
+ The Shield and the machine running the server must be on the same network, and the
96
+ machine must be able to reach the Shield on these ports:
97
+
98
+ | Port | Used for |
99
+ |---|---|
100
+ | TCP 6466 | Remote control (every tool call) |
101
+ | TCP 6467 | Pairing (only during `pair`) |
102
+ | UDP 5353 (mDNS) | `discover`, and `pair` without `--host` |
103
+ | TCP 5555 | ADB, only if you enable the ADB tools with `adb-setup` |
104
+
105
+ A firewall, a guest network, or a separate IoT VLAN between the two will show up as
106
+ "can't reach the Shield".
107
+
108
+ ```sh
109
+ mcp-server-shieldtv discover # optional: list Android TV devices via mDNS
110
+ mcp-server-shieldtv pair # finds the Shield, or: pair --host 192.168.1.50
111
+ ```
112
+
113
+ A code appears on the TV; type it into the terminal. A typo, a rejected code or Cancel on
114
+ the TV starts over with a new code (three tries). Pairing generates a client certificate
115
+ and key under `~/.config/mcp-server-shieldtv/` (override with `SHIELDTV_CONFIG_DIR`) and
116
+ saves the Shield's address, name and MAC in `config.json`. The MAC is how the server finds
117
+ the Shield again if its address changes. Unusable old credentials are set aside
118
+ (`cert.pem.broken`) and replaced. A running server notices a new pairing and reconnects
119
+ with it; it doesn't need a restart.
120
+
121
+ Then check everything between this machine and the Shield:
122
+
123
+ ```sh
124
+ mcp-server-shieldtv doctor
125
+ ```
126
+
127
+ ```
128
+ Shield 192.168.1.50
129
+ ok config paired with SHIELD Android TV (MAC xx:xx:xx:xx:B2:C3) at x.x.x.50; credentials load and are private
130
+ ok tcp ports 6466 and 6467 accept connections
131
+ ok identity SHIELD Android TV, MAC xx:xx:xx:xx:B2:C3
132
+ ok session NVIDIA SHIELD Android TV (remote service <version>); on, com.google.android.tvlauncher in front, ...
133
+ ```
134
+
135
+ `doctor` checks each layer in order and stops at the first failure, saying what it means and
136
+ which [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/HARDWARE_VALIDATION.md) step covers it. It never presses a
137
+ key, launches anything or pairs. Output is redacted (addresses, MACs) unless `--no-redact`.
138
+
139
+ **Treat `cert.pem` and `key.pem` like a password.** Anyone who has them can control your
140
+ Shield. The directory is created `0700` and the files `0600`, and `.gitignore` excludes
141
+ them. To revoke access, remove the device from the Shield's settings and re-pair.
142
+
143
+ WSL2 note: mDNS discovery needs mirrored networking (`networkingMode=mirrored` in
144
+ `%UserProfile%\.wslconfig`, then `wsl --shutdown`). Otherwise pass `--host`.
145
+
146
+ ### ADB tools (optional)
147
+
148
+ The remote protocol knows which app is in front, but not what it's playing, whether your
149
+ Bluetooth remotes work, or how to reboot. ADB can do all three. To enable the ADB tools:
150
+
151
+ 1. On the Shield: Settings > Device Preferences > About, select **Build** seven times, then
152
+ Developer options > **Network debugging** on.
153
+ 2. Run `mcp-server-shieldtv adb-setup` (after `pair`). The TV asks "Allow USB debugging?":
154
+ tick "Always allow from this computer" and choose Allow.
155
+
156
+ `adb-setup` creates its own ADB key (`adbkey`, `adbkey.pub`, `0600`) next to the pairing
157
+ files and sets `"adb": true` in `config.json`. **The ADB key grants a shell on the Shield;
158
+ guard it more carefully than the pairing files.** To revoke it, use Developer options >
159
+ Revoke USB debugging authorizations.
160
+
161
+ **`get_now_playing`** reports, checked on a real Shield: the app, title, subtitle (the artist for music,
162
+ the channel for live TV), play state, and position. Live TV (YouTube TV, Sling) reports no
163
+ position, since its "position" is an offset into the stream. Music played from YouTube Music
164
+ shows up as the YouTube app. Duration and album aren't available this way. A title that
165
+ contains ", " can rarely split wrong (the system prints title and artist joined by ", ").
166
+
167
+ **`get_remotes`** lists Bluetooth remotes (a Harmony hub shows up as "Harmony Keyboard")
168
+ (every paired input device, even ones that haven't connected since the Shield started)
169
+ as `working`, `disconnected` (normal when not in use), or `stuck`: Bluetooth says it is
170
+ connected, but Android never created its input device, so its buttons do nothing. This
171
+ happened to both remotes on a real Shield after a reboot. The fix for a Harmony hub is to
172
+ press Off and start the activity again.
173
+
174
+ **`reboot_shield`** notes which remotes work, restarts the Shield, and waits until it has
175
+ booted (about 35 seconds on a real Shield) plus 20 seconds for remotes that reconnect on
176
+ their own. It reports any remote that came back stuck, with the fix, and any that worked
177
+ before but hasn't reconnected yet. A Harmony hub doesn't reconnect until a button is
178
+ pressed, so expect it to be listed that way; if it then does nothing, `get_remotes` says
179
+ whether it's stuck. The whole call takes about a minute. It is marked destructive, so MCP
180
+ clients ask before running it.
181
+
182
+ ## Use it with an MCP client
183
+
184
+ ```json
185
+ {
186
+ "mcpServers": {
187
+ "shieldtv": { "command": "/path/to/mcp-server-shieldtv/.venv/bin/mcp-server-shieldtv" }
188
+ }
189
+ }
190
+ ```
191
+
192
+ For Claude Code: `claude mcp add shieldtv -- /path/to/.venv/bin/mcp-server-shieldtv`.
193
+
194
+ ### HTTP and Cloudflare Access
195
+
196
+ To share one server between Claude Code, Claude Desktop and the mobile app, run it as a
197
+ service and reach it through a Cloudflare Tunnel with Access in front:
198
+
199
+ ```sh
200
+ CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com CF_ACCESS_AUD=<aud tag> \
201
+ mcp-server-shieldtv --http --public-host mcp.example.com # 127.0.0.1:8712/shieldtv/mcp
202
+ ```
203
+
204
+ | Flag | Variable | Default |
205
+ |---|---|---|
206
+ | `--bind` | `MCP_HTTP_BIND` | `127.0.0.1` |
207
+ | `--port` | `MCP_HTTP_PORT` | `8712` (Onkyo 8711, Harmony 8713) |
208
+ | `--path` | `MCP_HTTP_PATH` | `/shieldtv/mcp` |
209
+ | `--public-host` | `MCP_PUBLIC_HOSTS` | none: only loopback Host headers pass |
210
+ | | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | none: no Access checks |
211
+ | | `MCP_ALLOWED_EMAILS` | anyone Access lets in |
212
+
213
+ With Access configured, every request needs a valid `Cf-Access-Jwt-Assertion`: the signature
214
+ (the team's published keys), audience, issuer, expiry, and the email allow-list if set.
215
+ Anything else gets a plain 403. The server **refuses to start** on a non-loopback address
216
+ without Access unless you pass `--insecure-no-auth` (for trusted-LAN testing only). It also
217
+ checks `Host` and `Origin` (DNS-rebinding protection), including on `--bind 0.0.0.0`.
218
+ `GET /healthz` answers without auth. `src/shieldtv_mcp/remote.py` is identical in all four
219
+ servers.
220
+
221
+ ### As a service on Alpine (Proxmox LXC)
222
+
223
+ ```sh
224
+ sh deploy/alpine/install.sh # as root; or: install.sh /path/to/checkout
225
+ su -s /bin/sh mcp-shieldtv -c 'SHIELDTV_CONFIG_DIR=/var/lib/mcp-server-shieldtv \
226
+ /opt/mcp-server-shieldtv/venv/bin/mcp-server-shieldtv pair --host <shield ip>'
227
+ vi /etc/mcp-server-shieldtv/env # HTTP and Access settings
228
+ rc-update add mcp-server-shieldtv default && rc-service mcp-server-shieldtv start
229
+ ```
230
+
231
+ | Path | Owner, mode | Holds |
232
+ |---|---|---|
233
+ | `/opt/mcp-server-shieldtv/venv` | root, 0755 | The code |
234
+ | `/etc/mcp-server-shieldtv/env` | root:mcp-shieldtv, 0640 | HTTP and Access settings |
235
+ | `/var/lib/mcp-server-shieldtv/` | mcp-shieldtv, 0700 (files 0600) | `cert.pem`, `key.pem`, `adbkey`, `config.json` |
236
+ | `/var/log/mcp-server-shieldtv/server.log` | mcp-shieldtv, 0750 | Logs, redacted |
237
+ | `/etc/init.d/mcp-server-shieldtv` | root, 0755 | OpenRC script (`supervise-daemon`, restarts on exit) |
238
+
239
+ The credentials live in `/var/lib`, owned by the unprivileged `mcp-shieldtv` user, because
240
+ they're the service's own: `pair` runs as that user, and the server rewrites `config.json`
241
+ when the Shield moves. Every dependency has a musl wheel, so nothing compiles.
242
+ `deploy/alpine/smoke-test.sh` runs the whole install in a `python:3.12-alpine` container
243
+ against the simulator, and CI runs it on every push.
244
+ To point it at a particular Shield without editing `config.json`, pass the host as an
245
+ environment variable: `claude mcp add shieldtv -e SHIELDTV_HOST=192.168.1.50 -- ...`.
246
+ That Shield must already be paired with the same credentials (`pair --host <ip>` once per
247
+ Shield; note that `pair` also saves its host as the default in `config.json`).
248
+
249
+ ## Configuration
250
+
251
+ | Setting | Default | What it does |
252
+ |---|---|---|
253
+ | `SHIELDTV_HOST` | *(from `config.json`)* | The Shield's IP address or hostname |
254
+ | `SHIELDTV_CONFIG_DIR` | `$XDG_CONFIG_HOME/mcp-server-shieldtv` | Where the certificate, key and `config.json` live |
255
+ | `XDG_CONFIG_HOME` | `~/.config` | Standard base directory, used when `SHIELDTV_CONFIG_DIR` is unset |
256
+ | `SHIELDTV_DRY_RUN` | off | Read the Shield, send nothing that changes anything (`serve --dry-run`) |
257
+ | `SHIELDTV_DEBUG` | off | Log the protocol traffic to stderr (`--debug`) |
258
+ | `SHIELDTV_LOG_UNREDACTED` | off | Show LAN addresses and MACs in logs (`--no-redact`); keys and tokens stay hidden |
259
+
260
+ `config.json` (written by `pair`, safe to edit by hand):
261
+
262
+ | Key | What it does |
263
+ |---|---|
264
+ | `host` | The Shield's address, saved by `pair` (and updated if the Shield moves) |
265
+ | `name`, `mac` | The Shield's name and MAC, read from its certificate by `pair` |
266
+ | `apps` | Extra or overriding app names for `launch_app` (see below) |
267
+ | `adb` | `true` once `adb-setup` succeeds; enables the ADB tools |
268
+ | `port`, `pairing_port` | `6466` and `6467`; other values are for the simulator |
269
+
270
+ Problems in `config.json` (invalid JSON, a bad app entry) are logged at startup and shown by
271
+ `doctor`, and only the bad part is ignored.
272
+
273
+ When the host is set in more than one place, the most specific wins: `pair --host`, then
274
+ `SHIELDTV_HOST`, then `config.json`.
275
+
276
+ ## Tools
277
+
278
+ | Tool | What it does |
279
+ |---|---|
280
+ | `get_status` | Reachability, power (`on`/`standby`), foreground app, volume (`null` when not reported), device info; while unreachable, `stale: true` with `as_of` and `error` |
281
+ | `list_apps` | The app names `launch_app` accepts |
282
+ | `send_key` | Press an allow-listed remote key, optionally repeated 1-10 times (see below) |
283
+ | `launch_app` | Launch an allow-listed app by friendly name, and confirm it reached the foreground |
284
+ | `set_power` | Wake (`on`) or sleep (`off`) using `WAKEUP`/`SLEEP`, not the `POWER` toggle |
285
+ | `get_now_playing` | App, title, subtitle (artist or channel), play state and position. Needs `adb-setup` |
286
+ | `get_remotes` | Each Bluetooth remote and whether it works, with a fix for stuck ones. Needs `adb-setup` |
287
+ | `reboot_shield` | Restart the Shield, wait until it's back, then check the remotes. Needs `adb-setup` |
288
+
289
+ Every tool returns structured content with a published output schema. The actions
290
+ (`send_key`, `launch_app`, `set_power`, and `reboot_shield`'s first fields) return the same
291
+ **ActionResult** shape as the sibling servers:
292
+
293
+ ```json
294
+ {"shield": "SHIELD Android TV", "outcome": "done",
295
+ "detail": "Launched netflix (com.netflix.ninja is in the foreground)",
296
+ "sent": ["app_link https://www.netflix.com/title"], "warnings": []}
297
+ ```
298
+
299
+ `outcome` is `done`, `unchanged` (e.g. `set_power off` while already in standby: nothing is
300
+ sent) or `dry_run`. Failures are MCP errors with a sentence that says what to do.
301
+
302
+ **Dry run** (`serve --dry-run`, `call --dry-run` or `SHIELDTV_DRY_RUN=1`) reads the Shield and
303
+ validates every call as usual, but sends nothing; results say what would have been sent.
304
+
305
+ ### Resources and prompts
306
+
307
+ Resources are context a client attaches (in Claude Code: `@shieldtv:shieldtv://apps`). Reading
308
+ them sends nothing to the Shield.
309
+
310
+ | Resource | What it holds |
311
+ |---|---|
312
+ | `shieldtv://apps` | The apps `launch_app` accepts: link, package, and whether each is a default or from `config.json` |
313
+ | `shieldtv://keys` | The keys `send_key` accepts, and the ones left out on purpose, with why |
314
+
315
+ Prompts are workflows you pick (in Claude Code: `/mcp__shieldtv__watch netflix`).
316
+
317
+ | Prompt | Arguments | Does |
318
+ |---|---|---|
319
+ | `watch` | `app`, `what` | Wakes the Shield, opens the app, navigates with the D-pad while asking you what's on screen |
320
+ | `remotes_not_working` | | Runs `get_remotes` and repeats its advice; doesn't reboot unless you ask |
321
+
322
+ ### Movie night across servers
323
+
324
+ The servers don't know about each other: each works alone, and a client with several
325
+ connected composes them. With all four connected, *"movie night in the theater: Plex on the
326
+ Shield, receiver in Dolby Surround at 45"* becomes (tool names as of these versions):
327
+
328
+ ```
329
+ harmony.start_activity activity="Watch Shield" # TV and receiver on, inputs switched
330
+ shieldtv.launch_app app="plex"
331
+ onkyo.set_listening_mode receiver="Theater" mode="dolby-surround"
332
+ onkyo.set_volume receiver="Theater" level=45 # capped server-side if above the cap
333
+ ```
334
+
335
+ What makes this safe to hand to a model is that limits live in each server, not in the
336
+ prompt: only allow-listed keys and apps here, and the volume cap and never-guess-a-receiver
337
+ rule in Onkyo.
338
+
339
+ Allowed keys: `HOME`, `BACK`, `MENU`, `DPAD_UP`/`DOWN`/`LEFT`/`RIGHT`/`CENTER`,
340
+ `MEDIA_PLAY_PAUSE`, `MEDIA_PLAY`, `MEDIA_PAUSE`, `MEDIA_STOP`, `MEDIA_NEXT`,
341
+ `MEDIA_PREVIOUS`, `MEDIA_REWIND`, `MEDIA_FAST_FORWARD`, `VOLUME_UP`, `VOLUME_DOWN`,
342
+ `VOLUME_MUTE`.
343
+
344
+ ### Apps
345
+
346
+ Default apps, each checked on a real Shield: `youtube`, `youtube-tv`, `netflix`,
347
+ `prime-video`, `disney+`, `hulu`, `plex`, `spotify`.
348
+
349
+ Apps launch through **deep links**: https links, or an app's own scheme (`plex://`,
350
+ `spotify:`) where the https link would go to a browser instead. On the Shield (remote service 7.x), a bare
351
+ package name is sent as `market://launch?id=<package>`, which the Shield rejects and then
352
+ drops the connection. `launch_app` waits for the app to reach the foreground (up to 10s)
353
+ and returns an error that says what happened if it doesn't: the request was rejected, or it
354
+ was accepted but nothing opened (the app isn't installed, or no app handles the link; the
355
+ TV shows "You don't have an app that can do this").
356
+
357
+ ### Add your own apps
358
+
359
+ Edit `~/.config/mcp-server-shieldtv/config.json`:
360
+
361
+ ```json
362
+ {
363
+ "host": "192.168.1.50",
364
+ "apps": {
365
+ "crunchyroll": { "link": "https://www.crunchyroll.com", "package": "com.crunchyroll.crunchyroid" },
366
+ "example": "https://example.com/tv"
367
+ }
368
+ }
369
+ ```
370
+
371
+ An entry is either a link, or `{ "link", "package" }`. Adding the package is recommended:
372
+ `launch_app` then confirms that exact app opened, and `get_status` shows the friendly name.
373
+ Without it, any app other than the home screen coming to the front counts as success.
374
+ Names are case-insensitive, and an entry with the same name as a default replaces it.
375
+ Restart the server (or your MCP client) to pick up changes.
376
+
377
+ **Finding the link and package:** open the app on the Shield, then call `get_status` (or ask
378
+ "what app is open on the Shield?"); `current_app_package` is the package. For the link, try
379
+ the service's website address (`https://www.<service>.com`). With ADB enabled, Android can
380
+ tell you which app a link opens without launching anything:
381
+
382
+ ```sh
383
+ adb shell cmd package query-activities --brief -a android.intent.action.VIEW \
384
+ -c android.intent.category.BROWSABLE -d 'https://tv.youtube.com'
385
+ ```
386
+
387
+ A result of `com.google.android.tv.frameworkpackagestubs/.Stubs$BrowserStub` means "no app"
388
+ (the TV shows "You don't have an app that can do this"). If the stub is listed alongside the
389
+ app, the app usually still opens, but its own scheme (if it has one) avoids the ambiguity.
390
+
391
+ ## Safety design
392
+
393
+ - **Keys are an allow-list.** The tool schema is an enum, and the client re-checks it.
394
+ `POWER`, `SEARCH` (starts voice capture), `SETTINGS`, `MUTE` (Android's *microphone*
395
+ mute), raw numeric key codes, and the library's `text:` typing are not available.
396
+ - **Apps are an allow-list.** Only names in `list_apps` can be launched.
397
+ - **No shell and no arbitrary key codes**, so a prompt-injected model has a small blast
398
+ radius. ADB is off by default. When enabled, the ADB tools take no arguments and run only
399
+ constant commands (`dumpsys media_session`, `dumpsys bluetooth_manager`, `dumpsys input`,
400
+ `getprop sys.boot_completed`, `/proc/uptime`), plus ADB's own reboot service. Code that
401
+ tries any other command is refused, so nothing the model writes reaches the Shield's shell.
402
+ - **`reboot_shield` is the only tool marked destructive**: it interrupts playback, so
403
+ clients should confirm with you first.
404
+ - **Credentials are private** (`0600`) and never logged. Logs go to stderr because stdout
405
+ belongs to the MCP transport.
406
+ - Tools carry titles and MCP annotations (`readOnlyHint`, `destructiveHint`,
407
+ `idempotentHint`, `openWorldHint`) so clients can decide what needs confirmation.
408
+
409
+ ## Behavior notes
410
+
411
+ - The server keeps **one long-lived connection** and caches pushed state. If the Shield is
412
+ asleep or offline at startup it retries in the background (1 s doubling to 60 s), and tool
413
+ calls return a clear "can't reach the Shield" message in the meantime. Each attempt is
414
+ bounded (15 s), so a Shield that accepts the connection but never starts a session can't
415
+ hang the server.
416
+ - **While disconnected, `get_status` still shows the last known values**, marked
417
+ `stale: true`, with `as_of` (when they arrived) and `error` (why it isn't reachable).
418
+ - **If the Shield moves to another address** (DHCP), after 60 s unreachable the server looks
419
+ for it over mDNS and checks each candidate's certificate for the MAC that `pair` saved. Only
420
+ an exact match is adopted, and saved to `config.json`. Any other device is never guessed at.
421
+ - **If the Shield stops trusting the certificate** (unpaired on the TV, factory reset), the
422
+ error says to re-run `pair`, and the running server picks up the new pairing by itself.
423
+ - Volume keys act on whatever the Shield is configured to control (the Shield itself, HDMI-CEC,
424
+ or IR), so results depend on your setup. When volume goes to a TV or receiver over CEC, the
425
+ Shield doesn't report a level, and `get_status` returns `volume: null`.
426
+
427
+ ## Troubleshooting
428
+
429
+ The server's errors are written to be actionable, so the model will usually relay one of
430
+ these:
431
+
432
+ **"Not paired with a Shield yet."** No host or credentials were found. Run
433
+ `mcp-server-shieldtv pair`. If you did pair, check that the server and `pair` use the same
434
+ `SHIELDTV_CONFIG_DIR` (an MCP client may launch the server with a different environment).
435
+
436
+ **"The Shield rejected our pairing."** The Shield no longer trusts this certificate,
437
+ typically after a factory reset or after removing the device under the Shield's
438
+ remote/connected-device settings. Run `pair` again; a running server reconnects by itself.
439
+
440
+ **"Can't connect: the pairing files ... can't be used."** `cert.pem` or `key.pem` is
441
+ damaged. Run `pair` again; it sets the old files aside.
442
+
443
+ **"Can't reach the Shield at ..."** The server is paired but has no connection. The Shield
444
+ may be asleep, rebooting, or off the network, its IP may have changed (a DHCP reservation
445
+ helps), or TCP 6466 may be blocked (see the port table above). The server keeps retrying in
446
+ the background, so the next call may succeed without a restart. If the address changed and
447
+ `config.json` has the Shield's `mac` (saved by `pair` since this version), the server finds
448
+ it again by itself; otherwise re-pair with the new address. `doctor` tells these cases apart.
449
+
450
+ **"The connection to the Shield dropped; try again in a moment."** The connection closed
451
+ during the command. The library reconnects on its own; retry.
452
+
453
+ **"The Shield rejected the launch request ..."** The app entry is a bare package name (or a
454
+ link the Shield refuses). Use an https link; see [Apps](#apps).
455
+
456
+ **"The Shield accepted ..., but the foreground app didn't change"** The app is probably not
457
+ installed, or nothing on the Shield handles that link. A very slow cold start can also do
458
+ this; the next call then reports the app as already open.
459
+
460
+ **`pair` says "No input to read the code from".** It was run without a terminal (for
461
+ example through a tool that doesn't attach one). Run it in a regular terminal.
462
+
463
+ **`discover` (or `pair` without `--host`) finds nothing.** mDNS doesn't cross most VLANs or
464
+ guest networks, and on WSL2 it needs mirrored networking (see above). Pass `--host <ip>`;
465
+ the Shield shows its IP in its network/about settings, and your router's client list has
466
+ it too.
467
+
468
+ Server logs (connection attempts, retries, auth failures) go to stderr, which most MCP
469
+ clients save in their own logs. Running `mcp-server-shieldtv` in a terminal shows them
470
+ directly; it waits for MCP messages on stdin, so stop it with Ctrl+C.
471
+
472
+ ## A session against the simulator
473
+
474
+ `simulate` runs a **simulated Shield**: TLS on two ports, speaking Android TV Remote v2 the way
475
+ androidtvremote2 does. The real library pairs and connects with it. Pair with it exactly as
476
+ with a real one (the code "on the TV" is printed by the simulator), or pass `--paired`:
477
+
478
+ ```
479
+ $ mcp-server-shieldtv simulate --config-dir /tmp/sim # terminal 1
480
+ ...
481
+ The TV shows the pairing code: 3B26B3
482
+
483
+ $ export SHIELDTV_CONFIG_DIR=/tmp/sim # terminal 2
484
+ $ mcp-server-shieldtv pair --host 127.0.0.1
485
+ Pairing with SHIELD Android TV (00:04:4B:A1:B2:C3). A code will appear on the TV screen.
486
+ Enter the code shown on the TV: 3B26B3
487
+ Paired and connected: NVIDIA SHIELD Android TV
488
+
489
+ $ mcp-server-shieldtv call launch_app app=netflix
490
+ { "shield": "SHIELD Android TV", "outcome": "done",
491
+ "detail": "Launched netflix (com.netflix.ninja is in the foreground)",
492
+ "sent": ["app_link https://www.netflix.com/title"], "warnings": [] }
493
+
494
+ $ mcp-server-shieldtv call set_power state=off
495
+ { ..., "outcome": "done", "detail": "The Shield is in standby", "sent": ["KEYCODE_SLEEP"] }
496
+
497
+ $ mcp-server-shieldtv call set_power state=off
498
+ { ..., "outcome": "unchanged", "detail": "The Shield is already in standby", "sent": [] }
499
+
500
+ $ mcp-server-shieldtv call launch_app app=hulu
501
+ Error executing tool launch_app: The Shield accepted https://www.hulu.com/welcome, but the
502
+ foreground app didn't change within 10s (still com.netflix.ninja). The app may not be
503
+ installed, or no installed app handles that link; the TV may be showing an error. The Shield
504
+ is in standby, which may be why: wake it with set_power first.
505
+ ```
506
+
507
+ (Lines wrapped, some fields elided with `...`.) What the simulator does and where each behavior
508
+ comes from (the library's client code, the owner's Shield, or invented for a test) is listed
509
+ at the top of `src/shieldtv_mcp/sim/fake_shield.py`.
510
+
511
+ ## Development
512
+
513
+ ```sh
514
+ pip install -e ".[dev]"
515
+ pytest -q # ~200 tests, ~15 s, no Shield needed
516
+ ruff check . && ruff format --check .
517
+ mypy # strict type checking of src/
518
+ ```
519
+
520
+ | Tests | Cover |
521
+ |---|---|
522
+ | `test_config.py` | Allow-lists (checked against the protocol's own key enum), settings, config problems |
523
+ | `test_client.py` | Connection lifecycle and state against `FakeRemote`, a stand-in for the library |
524
+ | `test_wire.py` | The real library against the simulated Shield: pairing (typo, rejection, Cancel, Shield gone), session, drops, garbled frames, sleep, stale state, unpairing, re-pair pickup, an address change (127.0.0.2 to .3) |
525
+ | `test_tools.py`, `test_resources.py` | The MCP contract: schemas, annotations, ActionResults, errors, dry run, resources, prompts |
526
+ | `test_adb.py` | ADB parsers on real dumpsys output; only constant commands reach the shell |
527
+ | `test_tooling.py`, `test_cli.py` | `doctor`, `simulate`, `call`, argument parsing |
528
+ | `test_http_tools.py`, `test_remote.py` | Streamable HTTP end to end; Access JWTs (valid, expired, wrong audience, wrong issuer, missing) with locally generated keys |
529
+ | `test_stdio.py` | The installed entry point over stdio |
530
+ | `test_assumptions.py` | Every assumption is cited by code, and listed here and in HARDWARE_VALIDATION.md |
531
+
532
+ CI runs it all on Python 3.11-3.14, plus the Alpine smoke test.
533
+ `npx @modelcontextprotocol/inspector mcp-server-shieldtv` browses tools, resources and prompts
534
+ by hand. [docs/ADB_TOOLS.md](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/docs/ADB_TOOLS.md) reviews the ADB tools and proposes read-only
535
+ additions (design only).
536
+
537
+ ## Verification status
538
+
539
+ Hardware-verified means seen on the owner's Shield (what was seen is in each assumption's
540
+ `note` in `src/shieldtv_mcp/assumptions.py`). Simulator-only means the simulator implements
541
+ it and nothing has confirmed it.
542
+
543
+ | Assumption | Confidence | Status |
544
+ |---|---|---|
545
+ | `S-PAIRING` | high | hardware-verified |
546
+ | `S-REMOTE-HANDSHAKE` | high | hardware-verified |
547
+ | `S-TLS-REJECT` | medium | simulator-only |
548
+ | `S-CERT-MAC` | medium | simulator-only |
549
+ | `S-MDNS` | high | simulator-only |
550
+ | `S-RECONNECT` | high | hardware-verified |
551
+ | `S-SLEEP-CONNECTION` | medium | simulator-only |
552
+ | `S-MARKET-REJECT` | high | hardware-verified |
553
+ | `S-LINK-UNHANDLED` | high | hardware-verified |
554
+ | `S-WAKE-ANY-KEY` | high | hardware-verified |
555
+ | `S-CEC-VOLUME` | high | hardware-verified |
556
+ | `S-APP-LINKS` | high | hardware-verified |
557
+ | `S-DUMPSYS-MEDIA` | high | hardware-verified |
558
+ | `S-LIVE-TV-PACKAGES` | medium | simulator-only |
559
+ | `S-STUCK-REMOTES` | high | hardware-verified |
560
+ | `S-REBOOT-TIME` | high | hardware-verified |
561
+
562
+ `test_assumptions.py` fails if this table and `assumptions.py` disagree.
563
+
564
+ ## Roadmap
565
+
566
+ - Hardware validation of this branch ([HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/HARDWARE_VALIDATION.md))
567
+ - The ADB proposals in [docs/ADB_TOOLS.md](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/docs/ADB_TOOLS.md), once their outputs are captured
568
+ - Several Shields from one server, by name (as the sibling servers do)
569
+ - Publish to PyPI and the MCP registry
570
+
571
+ ## License
572
+
573
+ MIT. See [LICENSE](https://github.com/SDNick484/mcp-server-shieldtv/blob/main/LICENSE).