fix(stremio_mcp): report ADB failures as actionable, redacted categories - #22
Merged
Merged
Conversation
added 4 commits
July 20, 2026 23:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Implement the captain-approved bounded response to GitHub issue #21 in stremio-mcp. Correct the macOS ADB Local Network documentation to identify adb, document the permitted GUI-terminal ADB server plus localhost-client pattern, and forbid automated kill-server/start-server lifecycle management. Add typed, bounded, redacted ADB failure categories with actionable caller-visible messages for unreachable/ambiguous network failures, unauthorized/offline devices, timeouts, and transport failures; preserve the native ADB transport and defer the raw-TCP differential probe. Sanitize endpoint/raw stderr/credential/URL/command-payload logging, fix set_volume false success, invalidate stale device state after failed operations, and bound reconnect attempts. Validate with mocked regression tests, locked repository checks, documentation/changelog updates, and CI.
What Changed
AdbFailureCategory/AdbFailurepair andclassify_adb_failure()that maps ADB output to bounded categories (unreachable, ambiguous network, unauthorized, offline, timeout, transport, command).tv_controlandplaytool responses now return actionable guidance via_adb_failure_text()instead of a bare "Failed", and controller logs record only the category — device endpoints, raw ADB stderr, intent URIs, and command payloads are no longer logged or returned.connect()is serialized behind a lock with a reconnect cooldown, andset_volumenow derives success from a new per-call_run_shell()helper so a failing shell command can no longer report success (the earlier shared-flag approach was replaced after review flagged a concurrency race).adbbinary, the shared server should be started from a permitted GUI terminal with other tools acting as localhost clients, and automated tooling must not runadb kill-server/start-server. Covered by newNativeAdbControllerTestsandAdbToolFailureTestscases; the full locked unit-test suite (99 tests) and compileall pass.Risk Assessment
✅ Low: The follow-up commit is a tightly scoped fix of the two prior findings — removing the shared shell-success flag in favor of a per-call return and reordering connect-scoped network markers ahead of the generic timeout branch — with mocked regression tests for both and no collateral impact on the other failure categories or on any acceptance criterion.
Testing
Ran the repo's locked setup, full unit suite, and source compilation (all clean), then went beyond tests by driving the real MCP tool handlers against a fake
adbbinary throughADB_PATHso the actual subprocess, classification, and response path executed for five realistic ADB failure modes; the captured transcripts show actionable category-tagged guidance with no endpoint, port, or secret leaking into user messages or logs, the previously false "Volume set to 8" success now correctly reported as a failure, five concurrent calls collapsing to a single boundedadbconnect attempt, and automatic recovery once the device returned. A base-commit run of the identical driver documents the prior leaky, unactionable behavior. This change is CLI/MCP-text and documentation only with no rendered UI surface, so the evidence is command transcripts rather than screenshots.Evidence: After: MCP tool responses and logs for five ADB failure modes (changed code)
=== scenario: local_network_denied — macOS Local Network denial / silently dropped packets MCP call: tv_control({'category': 'volume', 'action': 'set', 'value': 8}) user sees > ADB failure (category=ambiguous_network): network connection failed; verify the TV is online and use its current connection port, and on macOS check that Local Network access is granted to adb. server logs: ERROR:stremio-mcp:ADB connect failed: category=ambiguous_network leak check (endpoint/port/secret in message or logs): none === scenario: no_route — TV on another LAN (no route to host) user sees > ADB failure (category=unreachable): network reachability is ambiguous; verify the TV is on the same LAN and that macOS Local Network access is granted to adb. leak check: none === scenario: unauthorized — TV has not authorized this computer (stderr carries a secret) user sees > ADB failure (category=unauthorized): the TV has not authorized this computer; accept the debugging prompt on the TV. server logs: ERROR:stremio-mcp:ADB connect failed: category=unauthorized leak check: none === scenario: offline — TV connected but device offline MCP call: play({'source': 'direct', 'imdb_id': 'tt0111161'}) user sees > Failed to play: ADB failure (category=offline): the TV is offline; wake it and reconnect using the current connection port. leak check: none === scenario: shell_transport — transport dies mid-session user sees > ADB failure (category=transport): the ADB transport failed; verify the TV connection and try again. leak check: noneEvidence: Before (base commit 45c269d): same scenarios leaked endpoint/secret and reported false success
=== scenario: local_network_denied user sees > Volume set to 8 <-- false success on a total failure server logs: ERROR:stremio-mcp:Failed to connect to Android TV: failed to connect to '10.0.0.8:37139': Operation timed out leak check: LEAKED 10.0.0.8,37139 === scenario: unauthorized user sees > Failed <-- no guidance server logs: ERROR:stremio-mcp:Failed to connect to Android TV: failed to connect to '10.0.0.8:37139': device unauthorized. bearer=secret-token-12345678 leak check: LEAKED 10.0.0.8,37139,secret-token-12345678 === scenario: offline user sees > Failed to play: movie leak check: LEAKED 10.0.0.8,37139 === scenario: shell_transport user sees > Failed leak check: LEAKED 10.0.0.8,37139Evidence: Bounded reconnect and stale-state recovery through the MCP tool path
--- 5 concurrent tv_control calls while the TV is unreachable user sees > ADB failure (category=ambiguous_network): network connection failed; verify the TV is online and use its current connection port, and on macOS check that Local Network access is granted to adb. (x5) actualadbsubprocess invocations for 5 tool calls: 1 (serialized + cooldown-bounded, not one connect per call) --- TV comes back; next tool call reconnects without a restart user sees > Navigate: home controller device handle restored: '10.0.0.8:37139', last_failure cleared: TrueEvidence: Evidence driver scripts and fake adb binary used for the manual end-to-end runs
Evidence: README macOS Local Network guidance (identifies adb, GUI-terminal server pattern, forbids automated lifecycle management)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed ✅
src/stremio_mcp.py:469-set_volumedetermines success by reading the controller-instance flagself._last_shell_succeededthatsend_shell_commandmutates (lines 435/446) rather than from a value returned by the call it made. MCP tool calls can be served concurrently on the same sharedcontrollerobject, so two overlapping shell operations interleave on this single flag: ifset_volume's ownmedia volume --setreturns non-zero (flag -> False) while a concurrentget_tv_state/get_playback_statusshell command completes successfully afterwards (flag -> True),set_volumereturns True and the tool replies "Volume set to N" — reintroducing exactly the false-success this change set out to remove. It also makes theself._last_shell_succeeded = Noneline at 469 dead in production (send_shell_command overwrites it to False immediately), existing only so the mocked test at tests/test_stremio_mcp.py:1645 passes. Prefer a private helper returning(ok, stdout)(e.g._run_shell(command) -> tuple[bool, str]) thatsend_shell_commandandset_volumeboth consume, so success is carried per-call instead of on shared state.src/stremio_mcp.py:272-classify_adb_failuretests"timed out" in output or "timeout" in outputbefore any of the network markers, sofailed to connect to '<tv>:<port>': Operation timed out— the other common macOS Local Network denial symptom, alongside theNo route to hostseen in issue ADB: actionable preflight for macOS Local Network denial, and document the working pattern #21, since the denied packets are silently blackholed rather than rejected — is classified TIMEOUT and answered with "the operation timed out; verify the TV is online and try again", which omits the Local Network guidance that is the point of the issue. The generic timeout branch also cannot distinguish this from the internal 20sasyncio.wait_fortimeout in_run_adb. Consider testing the connect-scoped network markers (failed to connect,no route to host, ...) before the generic timeout, or adding the Local Network hint to the TIMEOUT guidance.src/stremio_mcp.py:233- The two network categories carry guidance that reads inverted against their names:UNREACHABLEsays "network reachability is ambiguous..." whileAMBIGUOUS_NETWORKsays "network connection failed...". Per the comment at line 279 this is deliberate (aNo route to hostreally is ambiguous between a route failure and a macOS denial), but a future maintainer editing these strings is likely to "fix" them backwards. Noting only; renaming or a short comment on the mapping would remove the trap.🔧 Fix: fix ADB shell success race and connect-timeout classification
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
uv sync --lockeduv run --locked python -m unittest discover -s tests -v(99 tests, all pass, including the newNativeAdbControllerTestsclassification/redaction/cooldown cases andAdbToolFailureTests)uv run --locked python -m compileall -q src testsManual end-to-end: drove realstremio_mcp.call_tool(tv_controlvolume/playback/navigate,play) withADB_PATHpointed at a fakeadbemitting real ADB stderr for local-network-denial, no-route, unauthorized (with an embedded sentinel secret), offline, and mid-session transport failure; captured user-visible tool text plus all server log records and asserted no host/port/secret leakageManual before/after: ran the same driver against base commit45c269dsource to contrast messages and log leakageManual bounded-reconnect/recovery: 5 concurrenttv_controlcalls against an unreachable TV while counting actualadbsubprocess invocations, then healed the fake adb and confirmed the next call reconnected and cleared stale failure stateDoc check:grep -rn "kill-server|start-server" README.md AGENTS.md CLAUDE.md docs/and read of the README macOS troubleshooting section🔧 **Document** - 1 issue found → auto-fixed ✅
src/stremio_mcp.py:231- The user-facing guidance strings for AdbFailureCategory.UNREACHABLE and AMBIGUOUS_NETWORK appear transposed: UNREACHABLE (matched on explicit "no route to host"/"network is unreachable") says "network reachability is ambiguous", while AMBIGUOUS_NETWORK (matched on connect-scoped failures) says "network connection failed". Both are safe and actionable, so this is cosmetic, but a caller reading category=unreachable alongside "ambiguous" text gets a confusing signal. Left unchanged because these strings are asserted by mocked regression tests and editing them would change caller-visible behavior.🔧 Fix: align ADB failure messages with their categories
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.