Skip to main content
Glama
umsachde

simkl-mcp

by umsachde
README.md
# 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

A4/5.0

Scored across 14 tools

Disambiguation5/5

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).

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues