simkl-mcp
# simkl-mcp
An [MCP](https://modelcontextprotocol.io) server wrapping the [Simkl](https://simkl.com) API, so
Claude can read your watch history and log what you've watched.
This is the **only place a Simkl token lives**. [`re-com-video`](https://github.com/umsachde/re-com-video)
— the recommendation engine — is read-only and calls these tools rather than holding a
credential itself, so a recommender can never be the thing that also mutated the history it
claims to have excluded.
## Tools
### Reading
| Tool | Does |
| --- | --- |
| `login_status()` | Whether a usable token is stored. Never returns the token. |
| `get_activities()` | Timestamps of the user's most recent list changes. The cheapest call in the API, and the gate for every sync. |
| `get_library(type=None, status=None, date_from=None)` | Watch history and watchlists, normalized. With `date_from`, only what changed since. |
| `get_library_ids(type=None)` | Every Simkl ID and nothing else — the cheap half of deletion reconciliation. |
| `lookup_watched(items)` | Ask Simkl directly whether specific titles are already in the library. |
| `get_ratings(type, rating=None)` | The titles the user rated themselves. |
| `get_title(simkl_id, type)` | Full detail record, including `users_recommendations` — the viewer-based similarity signal. |
| `resolve_id(imdb=…, tmdb=…, type=…, netflix=…, mal=…)` | External ID → Simkl ID. |
| `search(query, type=None, limit=10)` | Search by title. Use `resolve_id` instead whenever an external ID exists. |
### Writing
| Tool | Does |
| --- | --- |
| `mark_watched(items, type)` | Record watch events. |
| `set_status(items, status, type)` | Move titles to `watching` / `plantowatch` / `hold` / `completed` / `dropped`. |
| `rate(items, rating, type)` | Rate 1–10. |
| `remove_from_history(items, type)` | Remove from history. Also clears the item's rating. |
| `logout()` | Delete the local token. |
## Setup
```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
```
Register a free app at [simkl.com/settings/developer](https://simkl.com/settings/developer). The
PIN flow needs no `client_secret` and no redirect URI, so the redirect URI field can be anything
(`urn:ietf:wg:oauth:2.0:oob` is conventional).
```bash
python scripts/setup_auth.py --client-id YOUR_CLIENT_ID
```
It prints a five-character code; enter it at [simkl.com/pin](https://simkl.com/pin). The token is
written to `simkl_auth.json` (mode `600`, gitignored). Simkl issues **no refresh token** — the
token lasts about five years, and a 401 means the app was revoked at
[simkl.com/settings/apps](https://simkl.com/settings/apps).
Register as an MCP server:
```json
{
"mcpServers": {
"simkl": {
"command": "/path/to/simkl-mcp/.venv/bin/python",
"args": ["/path/to/simkl-mcp/server.py"],
"env": { "SIMKL_AUTH_PATH": "/path/to/simkl-mcp/simkl_auth.json" }
}
}
}
```
## Tests
```bash
pip install -e ".[dev]" && python -m pytest tests -q
```
No network and no token required — every HTTP call is faked.
## What this server is careful about
Simkl's API has a handful of behaviours that fail *quietly*, which is the class of bug this
project is built to avoid. Each is handled here and has a test:
- **Anime entries nest under `show`, not `anime`.** Reading the wrong key silently returns an
empty anime library — and an exclusion gap nothing would notice.
- **Browse filters silently widen.** An unrecognised `type` or `status` segment is treated as
"all" and still returns `200`. This server rejects unknown filter values instead of sending
them.
- **Movies have no `watching` or `hold` status.** Refused up front rather than silently ignored.
- **TMDB IDs are not unique across movie and TV.** `resolve_id` refuses a TMDB ID without a type.
- **Slugs are not unique** (three different *Superman* films share one), so links are built from
the Simkl ID.
- **`/redirect` must not be followed.** The `Location` header is the whole answer; the
destination is HTML.
- **The PIN `device_code` is the literal string `"DEVICE_CODE"`** — a placeholder. Polling uses
`user_code`, and stops at the first token (Simkl deletes an approved code, and polling an
unknown one falls through to issuing a *new* code).
- **Deltas never contain deletions.** `get_library(date_from=…)` says so in its response.
- **History rows with no Simkl ID are counted, not dropped.** `skipped_without_simkl_id` is part
of every library response.
## Simkl's terms
Free for non-commercial and personal projects. Every request sends `client_id`, `app-name`,
`app-version` and a `User-Agent`, as required, and every title this server returns carries a link
back to its Simkl page. See [api.simkl.org/api-rules](https://api.simkl.org/api-rules).
## License
MIT
TDQS
Scored across 14 tools
Each tool has a clearly distinct purpose: auth (logout, login_status), library retrieval (get_library, get_library_ids, lookup_watched), title info (get_title, search, resolve_id), write actions (mark_watched, set_status, rate, remove_from_history), and sync support (get_activities, get_ratings). Even similar tools like get_library vs get_library_ids are explicitly differentiated by intent (full data vs ID-only for deletion reconciliation).
All tool names follow a consistent snake_case verb-noun pattern (e.g., get_activities, mark_watched, resolve_id). Verbs are uniform across CRUD-like operations, and even standalone verbs like 'search' and 'rate' fit the pattern. No mixed conventions or ambiguous naming.
14 tools is well-scoped for a media library sync and recommendation server. Each tool fills a clear role—auth, reading, writing, ID resolution, and search—without redundancy. The count is neither bloated nor insufficient, covering the full integration surface.
The tool surface covers the entire lifecycle: login status, library synchronization with delta support, deletion reconciliation, direct watched-check, ratings, title details, ID resolution, search, and all write operations (mark, status, rate, remove). No obvious gaps for the stated purpose of powering a recommendation system.