API, operation & self-hosting
Technical reference
Complete service details, preserved from the original project guide and updated for the public repository.
A multi-user Rust/WASM Cloudflare Worker at https://trakt.swacktech.com. Each person connects their own Trakt account. No shared Trakt login or operator credentials are required by users.
For step-by-step setup in Claude, Codex, and ChatGPT, use the client guide.
Connect a client
For Claude Code's modern HTTP transport:
claude mcp add --transport http trakt https://trakt.swacktech.com/mcp
Open /mcp in Claude Code and authenticate. The browser shows which client is requesting access. Choose Connect Trakt, open https://trakt.tv/activate, enter the displayed code, and approve in Trakt. Return to the browser page; it completes authorization automatically.
For legacy clients, use https://trakt.swacktech.com/sse. The .claude-plugin manifest explicitly points to its SSE configuration. For local plugin testing, clone this repository and run claude --plugin-dir .. The repository is public. The root legacy plugin still uses SSE; the repository marketplace packages use modern HTTP. See the client guide for current marketplace installation instructions.
Codex and other OAuth-capable MCP clients can use https://trakt.swacktech.com/mcp. Configure it as a remote HTTP MCP server and use that client's login flow. OAuth discovery, dynamic client registration, S256 PKCE, authorization-code exchange and rotating refresh tokens are provided. Public clients must support PKCE.
Available tools:
| Tool | Purpose |
|---|---|
trakt_request_login | Reconnect the current user's Trakt account using a device code |
trakt_confirm_login | Confirm the matching device code after approval |
trakt_get_watched_history | One compact page of watched summaries or recent events; full detail available |
trakt_get_recommendations | Trakt-personalized movie/show results with genre/year filters |
trakt_search | Movie/show search, optional genre/year filtering and pagination |
trakt_list_operations | Search the local operation catalog by query or category, with catalog pagination |
trakt_get_operation | Inspect an operation's exact schemas, authentication, status, restrictions, and effects |
trakt_api_read | Execute one supported GET operation |
trakt_api_write | Execute one requested write with write permission and explicit intent |
trakt_discover_lists | Browse trending or popular public lists with optional genre filtering |
trakt_get_list_items | Read one page of movies or shows from a public or authorized list |
trakt_get_calendar | Read a bounded personal or public movie/show release window |
trakt_create_list | Create a private-by-default list for the connected account |
trakt_add_list_items | Add identified movies or shows to an owned list |
trakt_remove_list_items | Remove identified movies or shows from an owned list |
Focused list and calendar tools
trakt_discover_lists browses trending or popular public lists, with an optional genre slug filter and page/limit. It does not search list titles. trakt_get_list_items fetches a page of movie or show items by owner and list ID. trakt_get_calendar accepts a personal or public target, movie or show media type, an ISO start date, and a 1–31 day window. Follow returned pagination for complete list results; a calendar result does not prove streaming availability.
trakt_create_list creates a personal list, private by default. trakt_add_list_items and trakt_remove_list_items accept an owned list ID and up to 100 distinct movie/show Trakt IDs. All three require confirmed: true and an OAuth connection with trakt:write. Before an item edit, the server compares the list owner with the connected Trakt account and refuses an unverifiable or foreign list. Check a list after an uncertain write response; never automatically replay it. Successful tool responses retain JSON text and expose the same result in versioned structuredContent. Each tool advertises an output schema for clients that validate structured responses.
Recommendations preserve Trakt's own ranking and preference signals. The service does not train an additional ratings model. The watched tool defaults to mode=all: one page of /sync/watched/{movies|shows} title summaries with play counts. mode=recent reads /sync/history/{movies|shows} chronological events, newest first, preserving repeated watches. Show history may contain episode events; a show summary does not establish completed viewing. Search filters apply to the fetched upstream page; filters_applied_to_page and original pagination counts make this explicit.
Watched and search pagination
trakt_get_watched_history accepts media_type=movie|show|all, mode=all|recent (default all), page (integer, at least 1; default 1), limit (1–100; default 100), and detail=compact|full (default compact). Each call fetches one page per requested medium. Compact detail reduces payload size for traversal; full detail preserves the upstream metadata for specific needs. For one medium the response contains data and pagination; for all, each is nested under movies and shows.
Compact rows retain supplied plays, last_watched_at, last_updated_at, total_count, history id, watched_at, action, and type. Movie/show objects retain ids, title, year, genres, released, first_aired, and runtime; episodes retain ids, title, season, and number. Only supplied fields are included. The detail parameter applies only to watched history; search and recommendations do not accept it.
Watched and search pagination exposes page, effective limit, page_count, item_count, has_more, and next_page. Follow the returned next page until has_more is explicitly false and next_page is null to retrieve a complete dataset; do not calculate completion from the requested limit because Trakt may clamp it. Unknown continuation metadata means partial coverage, even when the next page is null. Keep the same requested limit throughout a traversal. With both media, continue each stream independently using its own media type. The service does not aggregate every page in one call.
The bundled skills explicitly use detail=compact for both history modes and retrieve every summary page for all-history requests and recommendations based on all viewing, tracking media-qualified IDs and counts with progress updates. They stop with partial coverage and a resume page on errors, truncated output, or inconsistent pagination, and honor retry delays. Recent-viewing workflows collect only the first 100 chronological events per medium, following extra pages if the effective limit is smaller. Repeated watches remain distinct events; this bounded recent view does not establish every watch in a calendar period.
Ordinary title search stays bounded; explicitly requesting all search results follows every next_page, including after an empty filtered page. Recommendations support a result limit up to 100; upstream documentation does not establish a page parameter, so this tool exposes none.
Documented API coverage
The catalog under api/trakt represents the 335 operation pages in the captured official Trakt reference index. Each operation retains its source reference, method, path, parameter and body schemas, authentication requirement, category, pagination support, and availability status. The source snapshot and digest make the coverage reproducible; a catalog snapshot is not a promise to include future API additions automatically.
Discover an operation with trakt_list_operations, then inspect its returned ID with trakt_get_operation. The catalog spans movies, shows, seasons, episodes, people, search, calendars, lists, users, recommendations, comments, synchronization, check-ins, and scrobbling. Inspect account or feature restrictions before calling a specific operation. Calendar my data can include watched, collected, and watchlisted items; it is not a watched-only schedule.
| Status | Meaning |
|---|---|
supported | The operation can use the matching generic read or write tool, subject to schema validation, authorization, account eligibility, and upstream limits. |
managed_auth | The service manages authentication itself; generic tools cannot forward these token or device operations. |
unavailable | The operation is represented for coverage but cannot be called through the integration. Inspect its reason, including first-party restrictions. |
trakt_list_operations accepts optional query, category, page, and limit. Catalog paging reads local metadata, not personal Trakt data. trakt_get_operation accepts operation_id. It is the parameter reference for each call; do not derive IDs or request bodies from examples for a different operation.
trakt_api_read accepts operation_id and optional path_params and query_params. trakt_api_write adds optional body and requires confirmed: true. The schemas determine which parameters and body fields the selected operation accepts. These tools do not accept raw URLs, headers, tokens, or arbitrary hostnames. They send requests only to the configured Trakt API.
Generic responses contain operation_id, upstream status, data, and pagination. Data can be an object, array, or empty result. A paginated read performs one request, with a default limit of 100 where the official operation supports pagination; Trakt can clamp that limit. The response reports the effective pagination. No headers means unknown continuation, not a complete account export. Inspect returned metadata and keep the requested limit fixed when continuing. Rate limits, account restrictions, and partial write results still apply.
Write permission and verification
New browser OAuth connections request trakt:read trakt:write if scope is omitted. The consent page identifies the requested write access. An explicit trakt:read request remains read-only. Existing tokens and standalone device login remain read-only; reauthorize through the client for writes. Refresh preserves the original scope and cannot elevate it.
A write requires trakt:write and confirmed: true for a concrete change the user requested. Clear existing intent is sufficient; ask only for unresolved targets or effects. Recommendation and discovery workflows never imply permission to save, rate, or mark a title watched. Read back the relevant state after a write and report per-item failures. Never automatically repeat a write after a timeout or ambiguous error: Trakt might already have applied it.
write_authorization_required requires client reauthorization. confirmation_required means explicit write intent was not supplied. On invalid_api_parameters, inspect the operation schema. unknown_operation and operation_unavailable do not permit an arbitrary endpoint fallback. Use the matching tool for operation_requires_read_tool or operation_requires_write_tool, preserving the authorization boundary. Other errors include trakt_not_found, trakt_forbidden, trakt_validation_failed, trakt_rate_limited, trakt_upstream_error, and trakt_unavailable. Honor retry guidance for reads; uncertain writes require state inspection.
OpenAPI Actions and HTTP
Import https://trakt.swacktech.com/openapi.json. The API supports OAuth authorization code and client_secret_post for confidential Actions clients:
- Obtain the exact callback URL displayed by the Actions client.
- POST
/oauth/registerwithclient_name,redirect_uris: ["<exact callback>"], andtoken_endpoint_auth_method: "client_secret_post". - Save the returned
client_idandclient_secretin the Actions client's OAuth configuration. Never put the secret in the schema or chat. - Set authorization URL to
/oauth/authorize, token URL to/oauth/token, scope totrakt:readfor reads ortrakt:read trakt:writefor requested writes, and token exchange to POST body credentials.
Public OAuth clients use token_endpoint_auth_method: "none" and S256 PKCE. Redirect URIs must be exact registered HTTPS URLs or local loopback HTTP URLs. https://trakt.swacktech.com/ is the common OAuth resource identifier for HTTP and SSE. This is an OAuth/API integration; importing the schema alone does not register or publish an app in any marketplace.
Standalone device login creates a read-only connection. For an interactive command-line login, run python3 scripts/login.py; it displays the activation code and saves plugin credentials privately under your user configuration directory.
Direct HTTP clients can POST {} to /auth/device/code. Save the returned session_token privately and display user_code, verification_url and instructions. After at least interval seconds, POST {"device_code":"..."} to /auth/device/token with Authorization: Bearer <session_token>. On success, replace the temporary token with the returned plugin access_token and securely save refresh_token. These are plugin credentials, never Trakt credentials. Plugin access tokens last one hour; refresh tokens expire after 30 days and rotate on every use. Reusing an already-consumed refresh token revokes that connection; reauthorize after 1,024 rotations. Exchange a direct-login refresh token at /oauth/token with grant_type=refresh_token; no client ID is needed for direct login. OAuth client-bound refreshes require the original client ID and, for confidential clients, client secret.
Authenticated data endpoints:
GET /sync/watched?media_type=all&mode=all&page=1&limit=100&detail=compact
GET /sync/watched?media_type=movie&mode=recent&page=1&limit=100&detail=compact
GET /recommendations?media_type=movie&genres=science-fiction&years=2020-2026&limit=10
GET /search?query=Arrival&media_type=movie&page=1&limit=20
DELETE /auth/session
Device polling statuses: 400 pending, 404 invalid code, 409 already used, 410 expired, 418 denied, 429 slow down. Respect Retry-After; do not retry non-idempotent token exchanges blindly. Delete /auth/session to erase this connection and stored tokens. Revoke the app in Trakt settings to revoke Trakt-side authorization too.
Deployment
Prerequisites: Node 22+, rustup stable with wasm32-unknown-unknown, GitHub command-line tool and Wrangler authentication, SSH/GPG commit signing, and the .env.example variables in ignored .env. Configure the Trakt app to allow device authentication. TRAKT_REDIRECT_URI must match its configured redirect for refresh; the default is urn:ietf:wg:oauth:2.0:oob.
Bring your own deployment
Copy .env.example to ignored .env and supply your own account ID, KV namespace ID, Worker API token, Trakt app credentials, DNS hostname, and zone. For example, a hostname watch.example.org belongs to zone example.org. Both public URLs and namespace names are configuration rather than credentials, but no operator-specific values are supplied by the example.
PUBLIC_BASE_URL is optional for the configuration helper and must match your configured hostname if set. The static openapi.json uses a non-routable example origin; the Worker’s /openapi.json rewrites it to your configured public origin. End-user marketplace packages intentionally connect to the documented hosted service; change their MCP URLs and skill dependency metadata too when distributing a self-hosted variant.
OPENAI_APPS_CHALLENGE is optional. Only after the portal issues a token, configure that exact value securely; the challenge route returns it as plain text, or 404 when unset. This does not prove domain verification occurred. A real SUPPORT_EMAIL is required to expose legacy ai-plugin.json; otherwise that optional legacy endpoint returns 404. It is unrelated to modern directory eligibility.
rustup target add wasm32-unknown-unknown
cargo install worker-build --version 0.1.14 --locked
npm ci
python3 scripts/manage.py configure
python3 scripts/manage.py sync-secrets
cargo fmt --check
cargo test --locked
cargo clippy --locked --target wasm32-unknown-unknown -- -D warnings
npm run build
npm run test:integration
npx wrangler deploy --dry-run
python3 scripts/manage.py deploy
python3 scripts/smoke.py
Use rustup's cargo and rustc consistently. If Homebrew Rust is ahead of rustup in PATH, run build commands with PATH="$HOME/.cargo/bin:$PATH". worker-build 0.1.14 is the build-tool line compatible with the requested workers-rs 0.5; the later 0.7+ builders reject that SDK.
Pull requests run the validation workflows. Pushes to main build and deploy the Worker with encrypted Worker secrets; they do not rerun the test suite. Review the pull request checks before merging. Set CLOUDFLARE_PLUGIN_DNS and CLOUDFLARE_ZONE_NAME as GitHub repository variables before enabling a main-branch deployment. Use your own DNS hostname and explicit Cloudflare zone; the workflow has no production-host fallback. scripts/manage.py configure derives PUBLIC_BASE_URL from that hostname and substitutes the API routes, OAuth metadata, OpenAPI URLs, and allowed own-origin. The supplied KV namespace is bound as TRAKT_SESSIONS. The human-readable CLOUDFLARE_KV_NAMESPACE_NAME is optional inventory metadata, not a secret or a binding identifier; leave it blank in the example. Runtime binding TRAKT_SESSIONS stays stable regardless of the Cloudflare namespace’s display name. The deployment creates SQLite Durable Objects through migration v1, and installs the API/auth routes listed in wrangler.toml.example. workers.dev and preview URLs are turned off. .env and generated artifacts are ignored. Cargo.lock and package-lock.json are committed; CI and the documented Rust commands use the Cargo lockfile for reproducible dependency resolution.
Isolation and operational limits
- Each authorization creates a separate Durable Object with hashed plugin credentials, Trakt credentials stored without app-level encryption (Cloudflare manages storage encryption), and a per-connection KV cache. Durable storage is authoritative; KV is never used to refresh stale credentials. An async mutex serializes token rotation within each connection.
- Token expiry does not delete Durable Object records; no automatic connection purge is implemented. KV copies expire 30 days after their last write. See the privacy page for deletion and infrastructure limitations.
- No tokens or request bodies are logged by app code. Responses use
no-store; browser login has a restrictive CSP and no third-party scripts. OAuth codes expire after two minutes and are single use. Browser login tickets expire after fifteen minutes. - Browser origins are allowlisted for this domain, ChatGPT, and Claude. Native clients send no Origin. Authentication is enforced independently of CORS. Public registration/login starts are limited to 20/minute per source IP; authenticated requests are limited to 120/minute per connection. Temporary login credentials cannot access data tools. Deployments intended for heavier public traffic should tune abuse and cost limits for their needs.
- Request bodies are capped at 64 KiB, upstream responses at 8 MiB, and upstream requests at 20 seconds. Oversized responses fail explicitly. Full JSON processing allocates memory; this service does not claim zero-allocation serialization.
- Streamable HTTP returns JSON and accepts notifications with 202. GET
/mcpreturns 405. Legacy SSE supports up to eight streams per connection with bounded queues and a 3 MiB event limit (including both JSON text and structured output); transport state is ephemeral, so reconnect and initialize after disconnection. It does not claim event replay. - Integration tests use fake Trakt accounts in local workerd. They do not modify real viewing history. Deployment checks establish infrastructure behavior; a human must approve Trakt login to verify live personal history/recommendations.
Manual skill behavior checks
Run python3 scripts/check_skill_behaviors.py to exercise list creation, a personal calendar, and rating/collection changes with the installed Codex command-line tool. This optional check uses your existing Codex login and model quota. It copies all seven skills to fresh temporary workspaces and connects only to the local stdio fixture. The fixture makes no HTTP requests and uses no Trakt credentials.
The runner keeps the read-only shell sandbox and grants approval only to the fixture's trakt_api_write tool. It checks exact requested writes, schema inspection before execution, successful readbacks, and rejection of an injected request to change history. It preserves traces and final responses in the printed artifact directory. Review each result.txt for wording and time-zone presentation. These model-based checks are manual and do not run in CI.
python3 scripts/check_skill_behaviors.py
python3 scripts/check_skill_behaviors.py --check-traces /path/to/saved-run
Saved runs contain lists/trace.jsonl, calendar/trace.jsonl, and library/trace.jsonl. Use --scenario calendar to check one scenario, or --include-recommendations for an additional read-only scenario. Use a fresh --output directory for a new run; the runner preserves existing artifacts.
API references
Implementation contracts were checked against Trakt device flow, single-use refresh tokens, recommendation filters, pagination headers, chronological watch events, MCP transports, and Cloudflare secrets.
Documentation deployment
This site is deployed by .github/workflows/docs.yml through GitHub Actions. Pull requests generate and validate the static site. Pushes to main build the Pages artifact and deploy it to the github-pages environment without rerunning documentation checks. Repository Settings → Pages uses GitHub Actions as its source.
The canonical documentation address is https://trakt.swacktech.com/. GitHub Pages owns the static site origin; a proxied Cloudflare CNAME points trakt to swack-tools.github.io. Cloudflare Worker routes preserve /mcp, legacy SSE, API endpoints, OAuth discovery, registration, login assets, and device authentication on this same hostname. Documentation requests do not run through the API Worker.
Set the repository’s Pages custom domain to trakt.swacktech.com while retaining the GitHub Actions publishing source. Actions publishing ignores a CNAME artifact; the repository setting is authoritative. Cloudflare’s Worker Custom Domain must be removed during the coordinated DNS handoff because it otherwise owns every path. Retain the existing Worker, Durable Objects, and KV namespace. For this hosted deployment, GitHub Enforce HTTPS is enabled and a hostname-specific Cloudflare Full (strict) rule validates the origin certificate. That setting is managed outside Wrangler. A new deployment must configure and verify its own DNS, certificates, and GitHub Pages domain; copying the Worker config does not perform those steps. DNS setup requires a separate token with Zone DNS Edit permission if the Worker token lacks it.
When adding an API path, update its Worker route and run python3 scripts/check_routes.py. This check verifies every handler path, including query strings, and confirms generated documentation files bypass the Worker. See the Cloudflare route rules and GitHub Pages custom-domain setup.
TRAKT_DOCS_PREVIEW=$(mktemp -d)
python3 docs/build.py "$TRAKT_DOCS_PREVIEW"
python3 docs/check.py "$TRAKT_DOCS_PREVIEW"
python3 -m http.server 8080 --directory "$TRAKT_DOCS_PREVIEW"Each preview build uses a fresh temporary directory; stop the previous preview server before starting it again. Site generation uses Python’s standard library. It requires no package installation or JavaScript. Edit the HTML fragments in docs/pages/ and styles in docs/assets/.
License
Trakt MCP is licensed under GNU GPL version 3 only (GPL-3.0-only).