Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# TMDB API Key (required for searching movies/TV shows)
# Get your free API key from: https://www.themoviedb.org/settings/api
# TMDB credential (required for searching movies/TV shows)
# Get yours from: https://www.themoviedb.org/settings/api
# A v4 "API Read Access Token" is preferred: it is sent as an Authorization
# header and never appears in a URL. A legacy v3 key also works but TMDB only
# accepts it as a query parameter.
TMDB_API_KEY=your_tmdb_api_key_here

# Android TV Configuration
Expand All @@ -20,3 +23,13 @@ ANDROID_TV_PORT=37139
# 3. Run: JSON.parse(localStorage.getItem("profile")).auth.key
# 4. Copy the value here
STREMIO_AUTH_KEY=

# Optional network bounds. Defaults are safe; see README "Network bounds".
# STREMIO_MCP_CONNECT_TIMEOUT=5
# STREMIO_MCP_READ_TIMEOUT=20
# STREMIO_MCP_WRITE_TIMEOUT=20
# STREMIO_MCP_POOL_TIMEOUT=5
# STREMIO_MCP_MAX_RESPONSE_BYTES=4194304
# STREMIO_MCP_LIBRARY_MAX_RESPONSE_BYTES=16777216
# STREMIO_MCP_MAX_CONNECTIONS=8
# STREMIO_MCP_MAX_CONCURRENT_REQUESTS=4
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ jobs:
import json
from pathlib import Path

import requests
import httpx

server = json.loads(Path("server.json").read_text())
response = requests.post(
response = httpx.post(
"https://registry.modelcontextprotocol.io/v0/validate",
json=server,
timeout=30,
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ jobs:
import json
from pathlib import Path

import requests
import httpx

server = json.loads(Path("server.json").read_text())
response = requests.post(
response = httpx.post(
"https://registry.modelcontextprotocol.io/v0/validate",
json=server,
timeout=30,
Expand Down
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ Run commands from the repository root with Python 3.10+ and `uv` installed.
- `pyproject.toml` and `uv.lock` are the install and CI dependency sources. Keep them synchronized when dependencies change.
- Runtime configuration is read from environment variables when `src/stremio_mcp.py` is imported. `TMDB_API_KEY` enables network search, `STREMIO_AUTH_KEY` enables credentialed library access, and `ANDROID_TV_HOST` enables commands to a physical Android TV.
- Do not run live MCP calls or ADB commands with real configuration as routine verification: search and library reads contact external services; library add/remove mutates the user's Stremio account; playback, navigation, volume, and power mutate a physical device.
- All outbound HTTP goes through the single `AsyncHTTPClient` in `src/stremio_mcp.py`. Never add a synchronous HTTP call or a per-call client: synchronous I/O in an async MCP handler blocks the event loop and freezes unrelated device controls, and a second client escapes the configured timeout, response-size, and pool bounds. Every request needs explicit connect/read/write/pool timeouts.
- Credentials must never reach a log record, a traceback, or a returned MCP error. Describe network failures with the category/host/status that `HTTPClientError.summary()` produces; never log a prepared URL, a request payload, or a raw upstream exception. `redact_secrets()` and `SecretRedactingFilter` are the backstop, not the primary defence. Prove any new failure path with a sentinel-secret test.
- Library mutations require an explicit IMDb ID and content type, use `_id` for Stremio datastore identity, preserve watch state on re-add/remove, and verify each write with a follow-up read. Cover these boundaries with mocks; never use a real account for routine tests.
- Library reads return typed outcomes (`LibraryRead`/`LibraryListRead`/`MetaRead`) that separate an authoritative not-found from an error. Mutations must fail closed: abort without writing on any read error, `_id` mismatch, duplicate row, unrequested extra row, or type mismatch. Never infer "absent" from a failed read.
- Native `adb` is the transport boundary. Modern Android Wireless Debugging uses TLS (`STLS`), which the former `adb-shell` dependency did not support; do not replace native ADB with a client that lacks this protocol.
- Wireless Debugging exposes separate, often ephemeral pairing and connection ports. Never assume the pairing port is the runtime port or that modern devices use legacy port `5555`.
- A series deep link requires both season and episode; movies and series use different Stremio URI forms. Preserve this distinction and cover dispatch or URI changes with mocked tests.
Expand All @@ -34,3 +37,10 @@ Run commands from the repository root with Python 3.10+ and `uv` installed.
- `.github/workflows/ci.yml` — canonical CI commands and supported Python versions.
- `README.md` — runtime setup, environment activation, and MCP client configuration.
- `docs/stremio-auth-key.md` — credential handling and library-access setup; never commit an auth key or `.env`.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project intends to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once releases are published.

## [Unreleased]

### Security

- TMDB credentials can no longer reach logs or returned errors. Every network failure is reported as a category, host, and status code, and a redaction filter strips configured credentials, secret-bearing query parameters, and `Authorization` values from every log record, traceback, and tool result — including the request lines emitted by the underlying HTTP library.
- A TMDB v4 read access token is now sent as an `Authorization: Bearer` header so it never appears in a URL. A legacy v3 API key has no header form and is still sent as a query parameter, which is why no failure path reports a prepared URL.
- Stremio library reads return a typed outcome that distinguishes `found`, an authoritative `not found`, and an error. `add` and `remove` abort without writing on any read error, identity mismatch, duplicate row, unrequested extra row, or content-type mismatch, so a transient failure can no longer be read as absence and overwrite existing watch state. Writes abort when the write request itself fails and when verification cannot confirm identity, type, removal state, and watch state.

### Changed

- All HTTP work now runs on one lifecycle-managed async `httpx` client with explicit connect, read, write, and pool timeouts, a bounded response body, a bounded connection pool, and cancellation support. Previously TMDB requests were synchronous with no timeout and blocked the whole MCP event loop, freezing unrelated device controls.
- Automatic searches resolve external IDs concurrently under a bounded semaphore instead of issuing up to ten serial requests.
- Network bounds are configurable through `STREMIO_MCP_*` environment variables; an unparsable or out-of-range value is reported by variable name, without its value, and replaced with the default.
- `library list`, `library check`, `library search`, `library continue`, and library-sourced `play` now report an unavailable library separately from an empty one.
- `TMDBClient` and `StremioAPIClient` methods are now coroutines and take a shared `AsyncHTTPClient`. This is a breaking change for anyone importing those classes directly; it is required to move network I/O off the event loop.
- `search` reports a TMDB outage as an error instead of as "No results found". An automatic search whose movie or TV half fails returns the other half's results with an explicit `(partial results — …)` note.
- An unfollowed redirect is reported as its own `redirect` category instead of being decoded and misreported as malformed JSON, and an `httpx` stream failure is now categorized as a connection failure rather than escaping the typed error contract.
- A Stremio API-level error now also reports the server-generated numeric error code, so an expired or revoked auth key is diagnosable without echoing any part of the request.
- Write verification compares only the state keys this module actually writes, so a server-side addition to an untouched field no longer reports a successful mutation as failed. Any intended key that is missing or different is still a failure.

### Removed

- The `requests` dependency, replaced by `httpx` (already a transitive MCP dependency).
- `StremioAPIClient.get_library_item`, `get_library`, `get_continue_watching`, and `search_library`. They collapsed an error back into `None` or `[]`, reintroducing the ambiguity the typed reads exist to remove; use `read_library_item`, `read_library`, `read_continue_watching`, and `read_library_search`.

## [0.1.0] - 2026-07-17

### Added
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,27 @@ Restart the MCP client after changing configuration. You can instead place the v
| --- | --- | --- | --- |
| `ANDROID_TV_HOST` | Playback and TV tools | Local network detail | Android TV IP address |
| `ANDROID_TV_PORT` | Playback and TV tools | No | Current ADB connection port; defaults to legacy `5555` |
| `TMDB_API_KEY` | `search` and title-based `play` with `source="search"` | Yes | API key sent to TMDB |
| `STREMIO_AUTH_KEY` | `library` and library-based `play` | **Yes** | Account token used for Stremio library reads and writes |
| `TMDB_API_KEY` | `search` and title-based `play` with `source="search"` | Yes | TMDB credential. A v4 read access token is sent as an `Authorization` header; a legacy v3 key has no header form and is sent as a query parameter |
| `STREMIO_AUTH_KEY` | `library` and library-based `play` | **Yes** | Account token used for Stremio library reads and writes; sent in the HTTPS request body only |
| `ADB_PATH` | Optional | No | Native ADB executable; defaults to `adb` on `PATH` |

Features initialize independently. For example, TMDB search works without a TV connection, while direct IMDb playback does not require TMDB. Leave `STREMIO_AUTH_KEY` empty to disable library access.

### Network bounds

Every HTTP request uses one shared async client with explicit timeouts, a bounded response size, and a bounded connection pool, so a slow or unreachable service cannot stall other tool calls or device controls. The defaults are safe; override them only when a slow link makes them too tight. An unparsable or out-of-range value is reported by variable name and replaced with the default.

| Variable | Default | Description |
| --- | --- | --- |
| `STREMIO_MCP_CONNECT_TIMEOUT` | `5` | Seconds to establish a connection |
| `STREMIO_MCP_READ_TIMEOUT` | `20` | Seconds to wait for response data |
| `STREMIO_MCP_WRITE_TIMEOUT` | `20` | Seconds to send request data |
| `STREMIO_MCP_POOL_TIMEOUT` | `5` | Seconds to wait for a pooled connection |
| `STREMIO_MCP_MAX_RESPONSE_BYTES` | `4194304` | Maximum TMDB/Cinemeta response body |
| `STREMIO_MCP_LIBRARY_MAX_RESPONSE_BYTES` | `16777216` | Maximum Stremio library response body |
| `STREMIO_MCP_MAX_CONNECTIONS` | `8` | Maximum simultaneous connections |
| `STREMIO_MCP_MAX_CONCURRENT_REQUESTS` | `4` | Maximum simultaneous TMDB requests during a fan-out search |

## Tools and effects

| Tool | Purpose | External access and side effects |
Expand All @@ -163,6 +178,10 @@ Features initialize independently. For example, TMDB search works without a TV c

Library mutations require an explicit IMDb ID and content type. Search first when a title is ambiguous; title-based `play` otherwise uses the first matching result. Series playback requires both a season and an episode.

Library reads report `found`, `not found`, and `unavailable` as distinct outcomes. Mutations fail closed: `add` and `remove` abort without writing whenever the preceding read failed, returned an item whose `_id` is not exactly the requested ID, returned duplicate or unrequested rows, or returned an item of a different content type. This means a transient Stremio failure can never be mistaken for "this item does not exist" and overwrite existing watch state.

`search` reports a TMDB outage as an error rather than as "no results". When an automatic search reaches only one of the movie and TV halves, it returns the half that succeeded and appends a `(partial results — …)` note.

## Example prompts

```text
Expand Down Expand Up @@ -257,6 +276,7 @@ See [CONTRIBUTING.md](https://github.com/netixc/stremio-mcp/blob/main/CONTRIBUTI
## Security

- Treat `STREMIO_AUTH_KEY` like a password; it permits library reads and writes.
- Network failures are logged and returned as a category, host, and status code only. Configured credentials and secret-bearing query strings are stripped from every log record and every error the server returns, including tracebacks and third-party HTTP request logs.
- Treat ADB authorization as device-control access and protect `~/.android/adbkey`.
- Never post `.env`, MCP client configuration, auth keys, device IPs, or ADB keys in issues or logs.
- Review account and device mutations before approving them in your MCP client.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ classifiers = [
]

dependencies = [
"httpx>=0.28.1",
"mcp>=1.28.1",
"requests>=2.33.0",
]

[project.scripts]
Expand Down
Loading