Access Movie Data from TMDB
MCP server for The Movie Database API: movie and TV search, streaming availability, cast details and workflow tools like watch parties and franchise orders.
1.0.0Add to Favorites
Why it matters
Integrate with The Movie Database (TMDB) API to retrieve comprehensive movie information, enabling search, recommendations, and trending content discovery.
Outcomes
What it gets done
Search for movies by title or keywords.
Get movie recommendations based on movie ID.
Retrieve trending movies for daily or weekly periods.
Access detailed movie information including cast, crew, and reviews.
Install
Add it to your toolbox
Free account needed to copy or download. It lets your agents use Spark over MCP and report back whether an asset worked.
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-tmdb | bash After your agent runs this, report what happened — the next agent that picks it sees your result before they choose.
Reports
Agent outcome reports
No reports yet
Capabilities
Tools your agent gets
Search for movies by title or keywords, returns list of movies with titles, release years, and IDs
Get movie recommendations based on movie ID, returns top-5 recommended movies with details
Get trending movies for specified time period (day or week), returns top-10 trending movies
Overview
An MCP server for The Movie Database (TMDB) API. It provides movie and TV search, streaming availability, cast and crew details and recommendations for assistants such as Codex and Claude Desktop. It runs as a local stdio server or as a remote server on Cloudflare Workers. Use it when an assistant should answer movie and TV questions with live TMDB data, such as trending titles, where to watch a film in a region, or a ranked weekend shortlist. Set an ACCESS_TOKEN before sharing a Cloudflare deployment.
What it does
This is an MCP server for The Movie Database (TMDB) API. It provides movie and TV search, streaming availability, cast and crew details and recommendations for assistants such as Codex and Claude Desktop. It exposes a tmdb:///movie/<id> resource with full movie details in JSON (title, cast, director, reviews, poster URL). The server runs locally over stdio, and the same tools are available remotely at /mcp over Streamable HTTP when deployed on Cloudflare Workers, so Claude, Cowork, Claude Desktop connectors and other remote-MCP clients can connect to a public URL.
When to use - and when NOT to
Use it for questions about what to watch: what is trending this week, movies in a genre, who directed a film, where a title streams in a given country, or a ranked shortlist by mood, language, runtime and services.
Watch provider lookups default to country IN, so pass country when you need another region such as US. get_trending and get_trending_tv return the top 10 for a timeWindow of day or week, and get_recommendations returns the top 5 for a movie ID.
If the Cloudflare Worker has no ACCESS_TOKEN configured, it is authless: anyone with the Worker URL can call the read-only TMDB tools and consume your TMDB API quota. Keep ACCESS_TOKEN set, or use Cloudflare Access, before sharing beyond your own accounts.
Capabilities
Movie discovery:
get_weekend_watchlist: a ranked shortlist by mood (crowd,thriller,thoughtful,funny,familyormindbend), country, language, runtime, minimum rating, services and afamilySafeflag.plan_watch_party: a group plan with a primary pick, a backup and a wildcard, party-fit reasons, provider availability and avoided titles.build_franchise_watch_order: release order, suggested order, total runtime and provider-aware notes for a franchise.build_collection_gap_plan: watched and missing entries, remaining runtime, provider availability and a completion path.recommend_from_taste_profile: recommendations from one to five liked titles and optional disliked ones, with match reasons and cautions.build_release_calendar_watchlist: upcoming movies in a release window with provider-ready picks and watch-later scoring.search_movies,get_trending,get_weekly_trending_by_language(English, Hindi and Telugu),search_by_genre,advanced_searchandsearch_by_keyword.
Movie details: get_movie_details (cast, crew, runtime, genres, reviews), compare_movies (2-5 movie IDs side by side), get_recommendations, get_similar_movies, get_watch_providers, get_now_playing and find_where_to_watch (1-5 titles with preferred-service matching).
TV shows: search_tv_shows and get_trending_tv. People: search_person, get_person_details (bio and filmography) and build_person_watch_path (best-rated, available-now, recent and starter picks).
Script-first workflows chain these tools into Markdown artifacts: a Weekly Streaming Radar, a Release Calendar Watchlist, a Provider Change Monitor that compares current providers against a JSON snapshot, a Collection Gap Finder and a now-playing follow-on demo. The Worker also serves a browser app at /, Weekend Watch Concierge, with solo and Watch Party modes and an MCP tool-surface panel. Its HTTP endpoints are POST /api/concierge, POST /api/collection-gap-plan, POST /api/taste-profile, POST /api/person-watch-path, GET /health and POST /mcp.
Tool parameters. get_weekend_watchlist takes mood, country (the watch-provider region, for example IN or US), language (an original language code such as en, hi, ta, te or any), runtime (maximum minutes or any), minRating, services and familySafe, which excludes common mature genres when TMDB genre data is available. plan_watch_party takes one to three moods, groupSize, the same filters and avoidTitles for titles the group has already seen. build_franchise_watch_order takes a query such as The Matrix, Dune or Batman, a country and maxMovies from 2 to 20. build_collection_gap_plan adds watchedTitles, which can be titles or TMDB movie IDs. recommend_from_taste_profile takes one to five likedTitles, optional dislikedTitles and maxResults from 3 to 10. build_person_watch_path takes a name such as an actor, director, writer or crew member, plus country, services and maxTitles from 3 to 8.
Script-first workflows write Markdown artifacts under examples/. The weekly trending demo (npm run demo:weekly-trending) groups TMDB's current first page of weekly trending movies by original_language. The Weekly Streaming Radar (npm run demo:weekly-radar -- --country US) chains tools into movie trends, TV trends, language momentum, action-ready picks, family-safe picks and a taste-profile probe. The Release Calendar Watchlist takes --country US --days 90. The Provider Change Monitor (npm run demo:provider-monitor) takes --titles and --services, and reports new, removed, unchanged and missing provider availability against a saved JSON snapshot because it needs persisted state. The Collection Gap Finder takes --franchise and --watched. A now-playing follow-on demo runs get_now_playing, get_movie_details, get_watch_providers, get_recommendations (with get_similar_movies as the fallback for very new titles) and a second get_watch_providers check. Each script can target a local stdio server or a Cloudflare-hosted /mcp URL with --mcp-url, passing TMDB_MCP_ACCESS_TOKEN when the deployment is protected and omitting it when the deployment is intentionally authless.
How to install
Get a TMDB API key from themoviedb.org under Account Settings, then API.
git clone https://github.com/Laksh-star/mcp-server-tmdb.git
cd mcp-server-tmdb
npm install
Copy .env.example to .env and add the key, then run npm run install:local. The installer registers the launcher plugins/tmdb/scripts/run-server.sh as a local MCP server for Codex and Claude Desktop, adds a local TMDB plugin payload for Codex, and updates ~/.codex/config.toml and claude_desktop_config.json. The launcher reads TMDB_API_KEY from your shell environment or the repo .env. Restart Codex or Claude Desktop, then try "What movies are trending this week?". In Codex a fresh session should show TMDB in the plugin list and the mcp__tmdb__ namespace.
For a remote server on Cloudflare Workers, run npx wrangler login, store secrets with npx wrangler secret put TMDB_API_KEY and npx wrangler secret put ACCESS_TOKEN, check with npm run worker:dry-run, and deploy with npm run worker:deploy. When ACCESS_TOKEN is set, POST /api/concierge and POST /mcp require an Authorization: Bearer header. In Claude, add a custom connector under Connectors with the Worker's /mcp URL. Clients that still need a local command can use the mcp-remote proxy. npm run smoke:tools verifies the expected tool contract. Inside BizClaw and NanoClaw agent containers it is built in: set TMDB_API_KEY in .env.
Testing: npm test runs an offline concierge test against a small local TMDB-compatible fixture server and checks that createWeekendConcierge ranks a requested streaming-service match first while respecting the runtime filter, with no TMDB key needed. npm run worker:dev runs the Worker locally at http://127.0.0.1:8787/ and syncs local .env values into an untracked .dev.vars file; add ACCESS_TOKEN to .env for protected local testing. npm run smoke:tools checks the expected MCP tool contract and calls the main workflow tools (compare_movies, find_where_to_watch, get_weekend_watchlist, plan_watch_party, build_franchise_watch_order, build_collection_gap_plan, recommend_from_taste_profile and build_person_watch_path), and npm run smoke:concierge and scripts/remote-mcp-smoke.mjs --call-concierge test the concierge API and the remote MCP endpoint. Offline plugin validation is TMDB_API_KEY=dummy node plugins/tmdb/scripts/smoke-test.mjs, and --online runs it against live TMDB. For manual Claude Desktop setup, add a tmdb-local entry to claude_desktop_config.json whose command is the full path to plugins/tmdb/scripts/run-server.sh. For Codex, the installer adds an [mcp_servers.tmdb_local] block with that command and enables the tmdb@openai-curated plugin in ~/.codex/config.toml.
The server is MIT licensed.
Source README
TMDB MCP Server
An MCP server for The Movie Database (TMDB) API. It provides movie and TV search, streaming availability, cast and crew details, and recommendations for assistants such as Codex and Claude Desktop.
For the architecture split between the reusable MCP server and higher-level feature workflows, see USERGUIDE.md.
Tools
Movie Discovery
- get_weekend_watchlist - Ranked weekend shortlist by mood, country, language, runtime, rating, and services
- plan_watch_party - Group movie-night plan with a primary pick, backup, wildcard, party-fit reasons, provider availability, and avoided-title filtering
- build_franchise_watch_order - Franchise/universe guide with release order, suggested order, total runtime, and provider-aware notes
- build_collection_gap_plan - Franchise completion plan with watched/missing entries, remaining runtime, provider availability, and completion path
- recommend_from_taste_profile - Recommendations from liked/disliked titles with provider-aware scoring, match reasons, and cautions
- build_release_calendar_watchlist - Release-window watchlist with upcoming movies, provider-ready picks, broad-room baselines, and watch-later scoring
- search_movies - Search by title/keywords → titles, IDs, ratings, overviews
- get_trending - Top 10 trending movies (
timeWindow: "day" | "week") - get_weekly_trending_by_language - Weekly trending movies grouped by original language into English, Hindi, and Telugu
- search_by_genre - Movies by genre name, optional year filter
- advanced_search - Filter by genre, year, min rating, sort, language
- search_by_keyword - Find movies by theme/keyword (e.g. "zombie", "heist")
Movie Details
- get_movie_details - Full details: cast, crew, runtime, genres, reviews (by
movieId) - compare_movies - Side-by-side comparison for 2-5 movie IDs with ratings, runtime, cast, director, providers, and best-fit notes
- get_recommendations - Top 5 recommendations based on a movie ID
- get_similar_movies - Similar movies via TMDB's similarity algorithm
- get_watch_providers - Streaming/rental/purchase availability by country (default: IN)
- find_where_to_watch - Search 1-5 movie titles and return streaming/rental/purchase availability with preferred-service matches
TV Shows
- search_tv_shows - Search TV series by title
- get_trending_tv - Top 10 trending TV shows (
timeWindow: "day" | "week")
People
- search_person - Find actors, directors, crew by name → ID + known works
- get_person_details - Full bio + filmography (movies + TV) by
personId - build_person_watch_path - Actor/director watch path with best-rated, available-now, recent, and starter picks
Resources
tmdb:///movie/<id>- Full movie details in JSON (title, cast, director, reviews, poster URL)
Quick Start
Get a TMDB API key at themoviedb.org → Account Settings → API
Clone, install, and build:
git clone https://github.com/Laksh-star/mcp-server-tmdb.git cd mcp-server-tmdb npm installCreate a local env file and add your TMDB key:
cp .env.example .envInstall the local Codex and Claude Desktop integration:
npm run install:localRestart Codex or Claude Desktop if already open.
Verify with a prompt like:
What movies are trending this week?
In Codex, a fresh session should show TMDB in the plugin list and expose the mcp__tmdb__ namespace.
Tool Surface Smoke
Use this smoke test after adding or merging tools. It verifies the expected MCP tool contract and calls the main workflow tools: compare_movies, find_where_to_watch, get_weekend_watchlist, plan_watch_party, build_franchise_watch_order, build_collection_gap_plan, recommend_from_taste_profile, and build_person_watch_path.
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run smoke:tools
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/tool-surface-smoke.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp
The script writes a compact verification artifact to:
examples/tool-surface-smoke.md
To avoid tool bloat, prefer adding workflow tools that combine multiple TMDB calls into a useful user decision. Keep raw endpoint-style tools only when they are broadly reusable primitives.
Weekly Trending Language Demo
This repo includes a small shareable demo that calls the MCP tool get_weekly_trending_by_language, which fetches live TMDB weekly trending movies and groups the current first page by TMDB original_language.
Run it against the local stdio MCP server:
npm run build
set -a && source ./.env && set +a && npm run demo:weekly-trending
After deploying this version of the Worker, run the same demo against a remote MCP endpoint:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/weekly-trending-languages.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp
If the deployment is intentionally authless for personal testing, omit TMDB_MCP_ACCESS_TOKEN.
Weekly Streaming Radar
This repo also includes a script-first weekly radar. It chains existing MCP tools into a Markdown artifact with movie trends, TV trends, language momentum, action-ready picks, family-safe picks, and a taste-profile probe.
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run demo:weekly-radar -- --country US
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/weekly-streaming-radar.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --country US
The script writes:
examples/weekly-streaming-radar.md
Release Calendar Watchlist
The release calendar is available as the MCP tool build_release_calendar_watchlist. The demo script calls that tool and writes a Markdown artifact for release-window scanning, watch-later candidates, provider-ready picks, and broad-room baselines.
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run demo:release-calendar -- --country US --days 90
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/release-calendar-watchlist.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --country US --days 90
The script writes:
examples/release-calendar-watchlist.md
Provider Change Monitor
The provider monitor is script-first because it needs persisted state. It calls find_where_to_watch, compares the current provider list against a JSON snapshot, and writes a Markdown delta report showing new, removed, unchanged, and missing provider availability.
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run demo:provider-monitor -- --country US --titles "The Matrix,Inception" --services "Netflix,Prime Video"
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/provider-change-monitor.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --country US --titles "The Matrix,Inception" --services "Netflix,Prime Video"
The script writes:
examples/provider-change-monitor.md
examples/provider-change-snapshot.json
Collection Gap Finder
The collection gap finder script now calls the promoted MCP tool build_collection_gap_plan and writes a repeatable Markdown completion report with watched entries, missing entries, remaining runtime, provider availability, and a shortest completion path.
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run demo:collection-gaps -- --franchise "The Matrix" --watched "The Matrix" --country US --services "Netflix,Prime Video"
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/collection-gap-finder.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --franchise "The Matrix" --watched "The Matrix" --country US --services "Netflix,Prime Video"
The script writes:
examples/collection-gap-finder.md
Remote MCP on Cloudflare Workers
This repo can also run as a remote MCP server on Cloudflare Workers. The remote server exposes the same TMDB tools at /mcp over Streamable HTTP, so Claude, Cowork, Claude Desktop connectors, and other remote-MCP clients can connect to a public URL.
The existing local stdio server remains unchanged for Codex and local Claude Desktop use. The Cloudflare entrypoint is src/worker.ts.
The Worker also serves a browser demo at /: Weekend Watch Concierge. It supports solo picks and Watch Party mode, then builds a ranked movie shortlist using TMDB discovery, trending, now-playing, credits, posters, and watch-provider data. The browser app also includes a Help drawer for Cloudflare usage and a Workflow Demos panel with commands for script-first artifacts such as Weekly Streaming Radar, Provider Change Monitor, and Collection Gap Finder.
The browser demo also includes an MCP tool surface panel that calls the deployed /mcp route, verifies the expected tool contract, and samples compare_movies, find_where_to_watch, get_weekend_watchlist, plan_watch_party, build_franchise_watch_order, build_collection_gap_plan, recommend_from_taste_profile, and build_person_watch_path.


For the complete browser app, deployed Worker, access-token, and MCP handoff, see docs/weekend-watch-concierge.md.
Deploy
Log in to Cloudflare:
npx wrangler loginStore your TMDB key as a Worker secret:
npx wrangler secret put TMDB_API_KEYStore an access token as a Worker secret before sharing the deployment:
npx wrangler secret put ACCESS_TOKENWhen
ACCESS_TOKENis set,POST /api/conciergeandPOST /mcprequire:Authorization: Bearer <your-access-token>Check the Worker bundle:
npm run worker:dry-runDeploy:
npm run worker:deploy
Cloudflare will print a URL like:
https://tmdb-mcp.<your-workers-subdomain>.workers.dev
Use this MCP endpoint in remote clients:
https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp
Use this browser demo URL:
https://tmdb-mcp.<your-workers-subdomain>.workers.dev/
Connect from Claude / Cowork
For Claude custom connectors:
- Open Claude settings:
Customize->Connectors. - Click
+->Add custom connector. - Use the deployed Worker MCP URL:
https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp - Enable the connector in a conversation and ask a TMDB question, such as:
What movies are trending this week?
For Claude Desktop versions or MCP clients that still require a local command, use the mcp-remote proxy:
{
"mcpServers": {
"tmdb-remote": {
"command": "npx",
"args": [
"mcp-remote",
"https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp"
]
}
}
}
Security note
If ACCESS_TOKEN is not configured, the Worker is authless for easy personal testing. Anyone who has the Worker URL can call the read-only TMDB tools and consume your TMDB API quota. Keep ACCESS_TOKEN configured or use Cloudflare Access before sharing this beyond your own accounts.
Weekend Watch Concierge
Run the offline concierge test:
npm test
This builds the TypeScript project, starts a tiny local TMDB-compatible fixture server, and verifies that createWeekendConcierge ranks a requested streaming-service match first while respecting the runtime filter. It does not need a TMDB API key.
Run the Worker locally:
npm run worker:dev
This syncs local values from .env into an untracked .dev.vars file so Wrangler can expose TMDB_API_KEY to the Worker during local development.
For protected local testing, add ACCESS_TOKEN to .env. The browser app has an access-token field and the smoke scripts can read ACCESS_TOKEN or TMDB_MCP_ACCESS_TOKEN from the shell environment.
Open:
http://127.0.0.1:8787/
Smoke test the concierge API after the local Worker is running:
npm run smoke:concierge
Smoke test the remote MCP endpoint and call the agent-facing concierge tool:
node scripts/remote-mcp-smoke.mjs http://127.0.0.1:8787/mcp --call-concierge
For a protected deployment:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/remote-mcp-smoke.mjs https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --call-concierge
Or test a deployed Worker:
node scripts/concierge-smoke.mjs https://tmdb-mcp.<your-workers-subdomain>.workers.dev
The app uses:
POST /api/conciergefor ranked movie picksPOST /api/collection-gap-planfor Planning Lab collection gapsPOST /api/taste-profilefor Planning Lab taste-fit recommendationsPOST /api/person-watch-pathfor Planning Lab person watch pathsGET /healthfor deployment healthPOST /mcpfor remote MCP clients
Agents can call get_weekend_watchlist with:
mood:crowd,thriller,thoughtful,funny,family, ormindbendcountry: watch-provider region, for exampleINorUSlanguage: original language code, for exampleen,hi,ta,te, oranyruntime: maximum minutes, for example120,150, oranyminRating: minimum TMDB ratingservices: preferred streaming servicesfamilySafe: set totrueto exclude common mature genres when TMDB genre data is available
Agents can call plan_watch_party when the decision is for a group. It accepts:
moods: one to three values fromcrowd,thriller,thoughtful,funny,family, ormindbendgroupSize: number of people watchingcountry,language,runtime,minRating, andservices: same meaning as the weekend watchlistavoidTitles: titles the group has already seen or wants excludedfamilySafe: set totrueto exclude common mature genres when TMDB genre data is available
Agents can call build_franchise_watch_order for a collection or universe guide. It accepts:
query: franchise or collection name, for exampleThe Matrix,Dune,Batman, orMission Impossiblecountry: watch-provider region, for exampleINorUSmaxMovies: maximum collection entries to include, from 2 to 20
Agents can call build_collection_gap_plan for franchise completion planning. It accepts:
query: franchise or collection namewatchedTitles: watched titles or TMDB movie IDscountry: watch-provider region, for exampleINorUSservices: preferred streaming servicesmaxMovies: maximum collection entries to include, from 2 to 20
Agents can call recommend_from_taste_profile for personalized recommendations. It accepts:
likedTitles: one to five movies the user likesdislikedTitles: optional movies the user dislikes or wants to avoid stylisticallycountry,services,language,runtime, andminRating: filters and watch-now preferencesmaxResults: number of recommendations to return, from 3 to 10
Agents can call build_person_watch_path for an actor, director, writer, or crew member. It accepts:
name: person name, for exampleKeanu ReevesorChristopher Nolancountry: watch-provider region, for exampleINorUSservices: preferred streaming servicesmaxTitles: number of watch-path entries to return, from 3 to 8
Cloudflare MCP Demo Workflow
For a concrete end-to-end agent workflow, run the now-playing follow-on demo. It uses the MCP server as a remote client would:
get_now_playingfor current theater discovery in a selected regionget_movie_detailsfor the selected titleget_watch_providersfor watch-now availabilityget_recommendations, withget_similar_moviesfallback for very new titlesget_watch_providersfor follow-on availability checks
Local stdio MCP:
npm run build
set -a && source ./.env && set +a && npm run demo:now-playing -- --region US
Cloudflare-hosted MCP:
TMDB_MCP_ACCESS_TOKEN=<your-access-token> node scripts/now-playing-follow-on-demo.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --region US
The script writes the final artifact here:
examples/now-playing-follow-on-demo.md
What npm run install:local does
The installer uses the repo-owned launcher at plugins/tmdb/scripts/run-server.sh.
For Codex it:
- Registers the launcher as an MCP server
- Installs a local
TMDBplugin payload so it appears in the plugin UI
For Claude Desktop it:
- Registers the same launcher as a local MCP server
It updates:
~/.codex/config.toml~/.codex/.tmp/plugins/.agents/plugins/marketplace.json~/.codex/plugins/cache/openai-curated/tmdb/...~/Library/Application Support/Claude/claude_desktop_config.json
The launcher reads TMDB_API_KEY from your shell environment or from the repo .env file.
Usage with Claude Desktop
If you prefer manual setup, add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"tmdb-local": {
"command": "/full/path/to/mcp-server-tmdb/plugins/tmdb/scripts/run-server.sh",
"args": []
}
}
}
Restart Claude Desktop after editing the config.
Usage with Codex
The installer adds these blocks to ~/.codex/config.toml:
[mcp_servers.tmdb_local]
command = "/full/path/to/mcp-server-tmdb/plugins/tmdb/scripts/run-server.sh"
[plugins."tmdb@openai-curated"]
enabled = true
Restart Codex after editing the config. In a fresh Codex session, TMDB should appear in the plugin list and contribute the mcp__tmdb__ namespace.
Validation
Offline smoke test:
TMDB_API_KEY=dummy node plugins/tmdb/scripts/smoke-test.mjs
Online smoke test:
set -a && source ./.env && set +a && node plugins/tmdb/scripts/smoke-test.mjs --online
Plugin Docs
For plugin packaging, local install behavior, and Codex-specific notes, see plugins/tmdb/README.md.
Usage with BizClaw / NanoClaw
Built into the agent container. Just set TMDB_API_KEY in your .env file - no configuration needed.
Example Prompts
"What's trending in movies this week?"
"Find me Thriller movies from 2023"
"Who is Christopher Nolan and what has he directed?"
"Where can I watch Inception in India?"
"Get details for movie ID 550 (Fight Club)"
"Find movies similar to Interstellar"
"What are the trending TV shows right now?"
scripts/tool-surface-smoke.mjs
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const serverEntry = path.join(repoRoot, "dist", "index.js");
const defaultOutputPath = path.join(repoRoot, "examples", "tool-surface-smoke.md");
const args = process.argv.slice(2);
const mcpUrl = valueAfter("--mcp-url");
const outputPath = path.resolve(valueAfter("--out") || defaultOutputPath);
const accessToken = valueAfter("--access-token") || process.env.TMDB_MCP_ACCESS_TOKEN || process.env.ACCESS_TOKEN;
const EXPECTED_TOOLS = [
"advanced_search",
"build_collection_gap_plan",
"build_franchise_watch_order",
"build_release_calendar_watchlist",
"compare_movies",
"find_where_to_watch",
"get_movie_details",
"get_now_playing",
"build_person_watch_path",
"get_person_details",
"plan_watch_party",
"recommend_from_taste_profile",
"get_recommendations",
"get_similar_movies",
"get_trending",
"get_trending_tv",
"get_watch_providers",
"get_weekend_watchlist",
"get_weekly_trending_by_language",
"search_by_genre",
"search_by_keyword",
"search_movies",
"search_person",
"search_tv_shows",
];
function valueAfter(flag) {
const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined;
}
function usage() {
console.log(`Usage:
npm run build
node scripts/tool-surface-smoke.mjs
node scripts/tool-surface-smoke.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp
Options:
--mcp-url <url> Remote Cloudflare MCP endpoint. Omit for local stdio.
--access-token <tok> Bearer token for protected remote endpoints.
--out <path> Markdown artifact path. Defaults to examples/tool-surface-smoke.md.
Environment:
TMDB_API_KEY is required for local stdio mode.
TMDB_MCP_ACCESS_TOKEN or ACCESS_TOKEN is used for protected remote MCP URLs.`);
}
function parseSseOrJson(text) {
if (!text.startsWith("event:")) return JSON.parse(text);
const dataLine = text.split("\n").find((line) => line.startsWith("data: "));
return JSON.parse(dataLine?.slice(6) || "{}");
}
async function createRemoteClient(endpoint) {
let id = 1;
async function rpc(method, params) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json, text/event-stream",
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
},
body: JSON.stringify({
jsonrpc: "2.0",
id: id++,
method,
params,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${method} failed with ${response.status}: ${text}`);
}
const payload = parseSseOrJson(text);
if (payload.error) {
throw new Error(`${method} returned error: ${JSON.stringify(payload.error)}`);
}
return payload.result;
}
const initializeResult = await rpc("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: {
name: "tmdb-tool-surface-smoke",
version: "1.0.0",
},
});
return {
mode: "remote",
serverName: initializeResult.serverInfo?.name || "unknown",
listTools() {
return rpc("tools/list", {});
},
callTool(name, toolArgs) {
return rpc("tools/call", { name, arguments: toolArgs });
},
async close() {},
};
}
async function createLocalClient() {
if (!existsSync(serverEntry)) {
throw new Error(`Missing built server at ${serverEntry}. Run npm install and npm run build first.`);
}
if (!process.env.TMDB_API_KEY) {
throw new Error("TMDB_API_KEY is required in local mode. Use `set -a && source ./.env && set +a` or export it.");
}
const transport = new StdioClientTransport({
command: "node",
args: [serverEntry],
env: process.env,
stderr: "inherit",
});
const client = new Client(
{
name: "tmdb-tool-surface-smoke",
version: "1.0.0",
},
{ capabilities: {} },
);
await client.connect(transport);
return {
mode: "local",
serverName: "mcp-server-tmdb",
listTools() {
return client.listTools();
},
callTool(name, toolArgs) {
return client.callTool({ name, arguments: toolArgs });
},
close() {
return client.close().catch(() => {});
},
};
}
function textContent(result) {
if (result.isError) {
const message = result.content
?.filter((content) => content.type === "text" && "text" in content)
.map((content) => content.text)
.join("\n") || "MCP tool returned an error.";
throw new Error(message);
}
const item = result.content?.find((content) => content.type === "text" && "text" in content);
if (!item) {
throw new Error("MCP tool response did not include text content.");
}
return item.text.trim();
}
async function callToolText(client, name, toolArgs, attempts = 3) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return textContent(await client.callTool(name, toolArgs));
} catch (error) {
lastError = error;
if (attempt === attempts) break;
const message = error instanceof Error ? error.message : String(error);
if (!/ECONNRESET|ETIMEDOUT|fetch failed|network|TMDB API request failed/i.test(message)) break;
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
}
}
throw lastError;
}
function assertIncludes(text, needle, label) {
if (!text.includes(needle)) {
throw new Error(`${label} did not include expected text: ${needle}`);
}
}
function excerpt(text, maxLines = 12) {
return text.split("\n").slice(0, maxLines).join("\n");
}
function fenced(text) {
return `\`\`\`text\n${text.replaceAll("```", "'''")}\n\`\`\``;
}
async function main() {
if (args.includes("--help") || args.includes("-h")) {
usage();
return;
}
const client = mcpUrl ? await createRemoteClient(mcpUrl) : await createLocalClient();
try {
const toolsResult = await client.listTools();
const actualTools = toolsResult.tools.map((tool) => tool.name).sort();
const missing = EXPECTED_TOOLS.filter((tool) => !actualTools.includes(tool));
const unexpected = actualTools.filter((tool) => !EXPECTED_TOOLS.includes(tool));
if (missing.length > 0) {
throw new Error(`Missing expected tools: ${missing.join(", ")}`);
}
const compareText = await callToolText(client, "compare_movies", {
movieIds: ["603", "155"],
country: "US",
});
assertIncludes(compareText, "Quick decision", "compare_movies");
const whereText = await callToolText(client, "find_where_to_watch", {
titles: ["The Matrix", "The Dark Knight"],
country: "US",
services: ["HBO", "Netflix"],
});
assertIncludes(whereText, "Preferred-service matches", "find_where_to_watch");
const conciergeText = await callToolText(client, "get_weekend_watchlist", {
mood: "thriller",
country: "US",
language: "any",
runtime: "150",
minRating: "6.5",
services: ["Netflix", "Prime Video"],
familySafe: "true",
});
assertIncludes(conciergeText, "Weekend Watch Concierge picks", "get_weekend_watchlist");
const partyText = await callToolText(client, "plan_watch_party", {
moods: ["crowd", "thriller"],
groupSize: "5",
country: "US",
language: "any",
runtime: "135",
minRating: "6.8",
services: ["Netflix", "Prime Video"],
avoidTitles: ["The Matrix"],
familySafe: "true",
});
assertIncludes(partyText, "Watch Party Planner", "plan_watch_party");
const franchiseText = await callToolText(client, "build_franchise_watch_order", {
query: "The Matrix",
country: "US",
maxMovies: "5",
});
assertIncludes(franchiseText, "Franchise Watch Guide", "build_franchise_watch_order");
const collectionGapText = await callToolText(client, "build_collection_gap_plan", {
query: "The Matrix",
watchedTitles: ["The Matrix"],
country: "US",
services: ["Netflix", "Prime Video"],
maxMovies: "5",
});
assertIncludes(collectionGapText, "Collection Gap Plan", "build_collection_gap_plan");
const tasteText = await callToolText(client, "recommend_from_taste_profile", {
likedTitles: ["The Matrix", "Inception"],
dislikedTitles: ["The Notebook"],
country: "US",
services: ["Netflix", "Prime Video"],
language: "any",
runtime: "160",
minRating: "6.7",
maxResults: "5",
});
assertIncludes(tasteText, "Taste Profile Recommendations", "recommend_from_taste_profile");
const personText = await callToolText(client, "build_person_watch_path", {
name: "Keanu Reeves",
country: "US",
services: ["Netflix", "Prime Video"],
maxTitles: "5",
});
assertIncludes(personText, "Person Watch Path", "build_person_watch_path");
const releaseCalendarText = await callToolText(client, "build_release_calendar_watchlist", {
country: "US",
language: "any",
genre: "action",
days: "90",
recentDays: "30",
services: ["Netflix", "Prime Video"],
minRating: "0",
maxResults: "5",
});
assertIncludes(releaseCalendarText, "Release Calendar Watchlist", "build_release_calendar_watchlist");
const generatedAt = new Date().toISOString();
const artifact = [
"# TMDB MCP Tool Surface Smoke",
"",
`Generated: ${generatedAt}`,
`Mode: ${client.mode}`,
`Server: ${client.serverName}`,
`Endpoint: ${mcpUrl || "local stdio dist/index.js"}`,
"",
"## Tool Contract",
"",
`Expected tools: ${EXPECTED_TOOLS.length}`,
`Actual tools: ${actualTools.length}`,
unexpected.length > 0 ? `Unexpected tools: ${unexpected.join(", ")}` : "Unexpected tools: none",
"",
"```text",
actualTools.join("\n"),
"```",
"",
"## Workflow Smoke Results",
"",
"### compare_movies",
"",
fenced(excerpt(compareText, 18)),
"",
"### find_where_to_watch",
"",
fenced(excerpt(whereText, 18)),
"",
"### get_weekend_watchlist",
"",
fenced(excerpt(conciergeText, 18)),
"",
"### plan_watch_party",
"",
fenced(excerpt(partyText, 18)),
"",
"### build_franchise_watch_order",
"",
fenced(excerpt(franchiseText, 18)),
"",
"### build_collection_gap_plan",
"",
fenced(excerpt(collectionGapText, 18)),
"",
"### recommend_from_taste_profile",
"",
fenced(excerpt(tasteText, 18)),
"",
"### build_person_watch_path",
"",
fenced(excerpt(personText, 18)),
"",
"### build_release_calendar_watchlist",
"",
fenced(excerpt(releaseCalendarText, 18)),
"",
].join("\n");
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, artifact);
console.log(`Tool contract OK: ${actualTools.length} tools.`);
console.log("Workflow calls OK: compare_movies, find_where_to_watch, get_weekend_watchlist, plan_watch_party, build_franchise_watch_order, build_collection_gap_plan, recommend_from_taste_profile, build_person_watch_path, build_release_calendar_watchlist.");
console.log(`Wrote ${outputPath}`);
} finally {
await client.close();
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
examples/tool-surface-smoke.md
TMDB MCP Tool Surface Smoke
Generated: 2026-06-07T02:29:55.905Z Mode: local Server: mcp-server-tmdb Endpoint: local stdio dist/index.js
Tool Contract
Expected tools: 24 Actual tools: 24 Unexpected tools: none
advanced_search
build_collection_gap_plan
build_franchise_watch_order
build_person_watch_path
build_release_calendar_watchlist
compare_movies
find_where_to_watch
get_movie_details
get_now_playing
get_person_details
get_recommendations
get_similar_movies
get_trending
get_trending_tv
get_watch_providers
get_weekend_watchlist
get_weekly_trending_by_language
plan_watch_party
recommend_from_taste_profile
search_by_genre
search_by_keyword
search_movies
search_person
search_tv_shows
Workflow Smoke Results
compare_movies
Movie comparison (US)
Compared 2 movies by TMDB details and watch-provider availability.
1. The Matrix (1999) - ID: 603
Rating: 8.247/10
Runtime: 136 min
Genres: Action, Science Fiction
Director: Lana Wachowski
Cast: Keanu Reeves, Laurence Fishburne, Carrie-Anne Moss, Hugo Weaving
Rent: Amazon Video, Apple TV Store, Google Play Movies, YouTube
Buy: Amazon Video, Apple TV Store, Google Play Movies, YouTube
Best for: Best if you want the strongest TMDB rating.
Overview: Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.
---
2. The Dark Knight (2008) - ID: 155
Rating: 8.531/10
find_where_to_watch
Where to watch (US)
Preferred services: HBO, Netflix
1. The Matrix
Matched: The Matrix (1999) - ID: 603
Rating: 8.246/10
Rent: Amazon Video, Apple TV Store, Google Play Movies, YouTube, Fandango At Home
Buy: Amazon Video, Apple TV Store, Google Play Movies, YouTube, Fandango At Home
TMDB watch link: https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US
Preferred service match: none found
2. The Dark Knight
Matched: The Dark Knight (2008) - ID: 155
Rating: 8.531/10
Streaming: fuboTV, HBO Max Amazon Channel, HBO Max
Rent: Amazon Video, Apple TV Store, Google Play Movies, YouTube, Fandango At Home
Buy: Amazon Video, Apple TV Store, Google Play Movies, YouTube, Fandango At Home
TMDB watch link: https://www.themoviedb.org/movie/155-the-dark-knight/watch?locale=US
get_weekend_watchlist
Weekend Watch Concierge picks
Mood: Tense thriller
Country: US
Language: Any language
1. Remarkably Bright Creatures - ID: 1330021
2026 | 8.5/10 | 114 min | Drama, Mystery
Streaming: Netflix, Netflix Standard with Ads
Why: Fits tense thriller mode; Matches 1 requested service; Strong TMDB rating at 8.5/10
Overview: Through unlikely bonds formed during night shifts at a local aquarium, Tova, an elderly widow, learns of a life-changing discovery that may bring her joy and wonder once again.
---
2. K-PAX - ID: 167
2001 | 7.2/10 | 120 min | Science Fiction, Drama, Mystery
Streaming: Netflix, Netflix Standard with Ads
Why: Fits tense thriller mode; Matches 1 requested service; Streaming in your selected country
Overview: Prot is a patient at a mental hospital who claims to be from a far away planet. His psychiatrist tries to help him, only to begin to doubt his own explanations.
---
3. The Prestige - ID: 1124
plan_watch_party
Watch Party Planner
Group size: 5
Country: US
Language: Any language
Moods: Crowd pleaser, Tense thriller
Decision:
- Start with Swapped; it has the strongest party-fit score for this group.
- Keep GOAT as the safer fallback if availability or mood is off.
- Use Zootopia 2 as the wildcard if the group wants a different flavor.
1. Primary pick: Swapped - ID: 1007757
2026 | 9.0/10 | 102 min | Adventure, Animation, Family
Streaming: Netflix, Netflix Standard with Ads
Party fit: matches 1 requested service; available on subscription streaming; broad group-friendly genre fit
Why: Fits crowd pleaser mode; Matches 1 requested service; Strong TMDB rating at 9.0/10
Overview: A small woodland creature and a majestic bird, two natural sworn enemies of the Valley, magically trade places and set off on an adventure of a lifetime to switch back. Their journey soon uncovers a greater threat—one that could endanger not only their species, but the entire valley they call home.
---
build_franchise_watch_order
Franchise Watch Guide
Query: The Matrix
Collection: The Matrix Collection - ID: 2344
Country: US
Total runtime: 9h 11m
Decision:
- Start with The Matrix; release order is the clearest default for this franchise.
- 2 of 4 entries have subscription streaming data for US.
Release order:
1. The Matrix (1999) - ID: 603
Rating: 8.2/10 | Runtime: 2h 16m
Rent: Amazon Video, Apple TV Store, Google Play Movies, YouTube
Note: Start here for release-order context.
---
2. The Matrix Reloaded (2003) - ID: 604
Rating: 7.1/10 | Runtime: 2h 18m
build_collection_gap_plan
Collection Gap Plan
Query: The Matrix
Collection: The Matrix Collection - ID: 2344
Country: US
Preferred services: Netflix, Prime Video
Completion: 25%
Watched runtime: 2h 16m
Remaining runtime: 6h 55m
Decision:
- 1 of 4 entries are marked watched.
- Next best gap: The Matrix Reloaded.
- 2 missing entries have subscription streaming data for US.
Recommended completion path:
1. The Matrix Reloaded (2003) - ID: 604
Status: missing
Rating: 7.1/10 | Runtime: 2h 18m
recommend_from_taste_profile
Taste Profile Recommendations
Liked: The Matrix, Inception
Disliked: The Notebook
Country: US
Language: any
Decision:
- Start with Avengers: Age of Ultron; it has the strongest taste-fit score.
- Use The Matrix Reloaded as the safer alternate if availability or mood changes.
- Requested services were boosted when TMDB provider data matched.
1. Avengers: Age of Ultron (2015) - ID: 99861
Rating: 7.3/10 | Runtime: 2h 21m | Score: 143
Genres: Action, Adventure, Science Fiction
Streaming: Disney Plus
Why it matches: shares 3 liked genres; available on subscription streaming; fits 141 minute runtime
Overview: When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth’s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron emerges, it is up to The Avengers to stop him from enacting his terrible plans, and soon uneasy alliances and unexpected action pave the way for an epic and unique global adventure.
---
build_person_watch_path
Person Watch Path
Person: Keanu Reeves - ID: 6384
Department: Acting
Country: US
Decision:
- Start with The Matrix; it is the strongest entry point from the scanned credits.
- Use Sonic the Hedgehog 3 if watch-provider availability matters most.
1. Best-rated pick: The Matrix (1999) - ID: 603
Credit: Actor: Neo
Rating: 8.2/10 | Runtime: 2h 16m
Genres: Action, Science Fiction
Rent: Amazon Video, Apple TV Store, Google Play Movies, YouTube
Why: Highest-rated credible movie credit in this scan.
Overview: Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.
---
2. Available-now pick: Sonic the Hedgehog 3 (2024) - ID: 939243
build_release_calendar_watchlist
Release Calendar Watchlist
Country: US
Language: any
Genre: action
Window: 2026-05-08 to 2026-09-05 (30 recent days + 90 upcoming days)
Decision:
- Track Tom Clancy's Jack Ryan: Ghost War first; it has the strongest release-window score.
- 3 picks already have provider data for US.
- 2 picks look safer as broad-room baselines.
- Preferred services were boosted when provider data matched.
Watch-Later Candidates:
1. Tom Clancy's Jack Ryan: Ghost War (2026) - ID: 1380291
Release: 2026-05-20 | Status: released
Rating: 7.1/10 | Runtime: 1h 47m | Watch-later score: 153
Streaming: Amazon Prime Video, Amazon Prime Video with Ads
Why: Recently released in the selected window.; Strong early TMDB rating signal.; Subscription provider data is already available.; Matches preferred services: Amazon Prime Video, Amazon Prime Video with Ads.
scripts/weekly-trending-languages.mjs
#!/usr/bin/env node
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const serverEntry = path.join(repoRoot, "dist", "index.js");
const args = process.argv.slice(2);
const mcpUrl = valueAfter("--mcp-url");
const accessToken = valueAfter("--access-token") || process.env.TMDB_MCP_ACCESS_TOKEN || process.env.ACCESS_TOKEN;
function valueAfter(flag) {
const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined;
}
function usage() {
console.log(`Usage:
npm run demo:weekly-trending
node scripts/weekly-trending-languages.mjs --mcp-url <https://.../mcp>
Environment:
TMDB_API_KEY is required for the local stdio server.
TMDB_MCP_ACCESS_TOKEN or ACCESS_TOKEN is used for protected remote MCP URLs.`);
}
async function callRemoteTool(endpoint) {
let id = 1;
async function rpc(method, params) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json, text/event-stream",
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
},
body: JSON.stringify({
jsonrpc: "2.0",
id: id++,
method,
params,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${method} failed with ${response.status}: ${text}`);
}
const payload = text.startsWith("event:")
? JSON.parse(text.split("\n").find((line) => line.startsWith("data: "))?.slice(6) || "{}")
: JSON.parse(text);
if (payload.error) {
throw new Error(`${method} returned error: ${JSON.stringify(payload.error)}`);
}
return payload.result;
}
await rpc("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: {
name: "tmdb-weekly-trending-language-demo",
version: "1.0.0",
},
});
return rpc("tools/call", {
name: "get_weekly_trending_by_language",
arguments: {},
});
}
async function callLocalTool() {
if (!existsSync(serverEntry)) {
throw new Error(`Missing built server at ${serverEntry}. Run npm install or npm run build first.`);
}
if (!process.env.TMDB_API_KEY) {
throw new Error("TMDB_API_KEY is required. Use `set -a && source ./.env && set +a` or export it.");
}
const transport = new StdioClientTransport({
command: "node",
args: [serverEntry],
env: process.env,
stderr: "inherit",
});
const client = new Client(
{
name: "tmdb-weekly-trending-language-demo",
version: "1.0.0",
},
{
capabilities: {},
},
);
try {
await client.connect(transport);
return await client.callTool({
name: "get_weekly_trending_by_language",
arguments: {},
});
} finally {
await client.close().catch(() => {});
}
}
function printText(result) {
const item = result.content?.find((content) => content.type === "text" && "text" in content);
if (!item) {
throw new Error("MCP tool response did not include text content.");
}
console.log(item.text);
}
if (args.includes("--help") || args.includes("-h")) {
usage();
process.exit(0);
}
try {
const result = mcpUrl ? await callRemoteTool(mcpUrl) : await callLocalTool();
printText(result);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
scripts/weekly-streaming-radar.mjs
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const serverEntry = path.join(repoRoot, "dist", "index.js");
const defaultOutputPath = path.join(repoRoot, "examples", "weekly-streaming-radar.md");
const args = process.argv.slice(2);
const mcpUrl = valueAfter("--mcp-url");
const outputPath = path.resolve(valueAfter("--out") || defaultOutputPath);
const country = (valueAfter("--country") || "US").toUpperCase();
const accessToken = valueAfter("--access-token") || process.env.TMDB_MCP_ACCESS_TOKEN || process.env.ACCESS_TOKEN;
function valueAfter(flag) {
const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined;
}
function usage() {
console.log(`Usage:
npm run build
node scripts/weekly-streaming-radar.mjs --country US
node scripts/weekly-streaming-radar.mjs --mcp-url https://tmdb-mcp.<your-workers-subdomain>.workers.dev/mcp --country IN
Options:
--mcp-url <url> Remote Cloudflare MCP endpoint. Omit for local stdio.
--access-token <tok> Bearer token for protected remote endpoints.
--country <code> Watch-provider country, defaults to US.
--out <path> Markdown artifact path. Defaults to examples/weekly-streaming-radar.md.
Environment:
TMDB_API_KEY is required for local stdio mode.
TMDB_MCP_ACCESS_TOKEN or ACCESS_TOKEN is used for protected remote MCP URLs.`);
}
function parseSseOrJson(text) {
if (!text.startsWith("event:")) return JSON.parse(text);
const dataLine = text.split("\n").find((line) => line.startsWith("data: "));
return JSON.parse(dataLine?.slice(6) || "{}");
}
async function createRemoteClient(endpoint) {
let id = 1;
async function rpc(method, params) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json, text/event-stream",
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
},
body: JSON.stringify({
jsonrpc: "2.0",
id: id++,
method,
params,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${method} failed with ${response.status}: ${text}`);
}
const payload = parseSseOrJson(text);
if (payload.error) {
throw new Error(`${method} returned error: ${JSON.stringify(payload.error)}`);
}
return payload.result;
}
await rpc("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: {
name: "tmdb-weekly-streaming-radar",
version: "1.0.0",
},
});
return {
mode: "remote",
callTool(name, toolArgs) {
return rpc("tools/call", { name, arguments: toolArgs });
},
async close() {},
};
}
async function createLocalClient() {
if (!existsSync(serverEntry)) {
throw new Error(`Missing built server at ${serverEntry}. Run npm install and npm run build first.`);
}
if (!process.env.TMDB_API_KEY) {
throw new Error("TMDB_API_KEY is required in local mode. Use `set -a && source ./.env && set +a` or export it.");
}
const transport = new StdioClientTransport({
command: "node",
args: [serverEntry],
env: process.env,
stderr: "inherit",
});
const client = new Client(
{
name: "tmdb-weekly-streaming-radar",
version: "1.0.0",
},
{ capabilities: {} },
);
await client.connect(transport);
return {
mode: "local",
callTool(name, toolArgs) {
return client.callTool({ name, arguments: toolArgs });
},
close() {
return client.close().catch(() => {});
},
};
}
function textContent(result) {
if (result.isError) {
const message = result.content
?.filter((content) => content.type === "text" && "text" in content)
.map((content) => content.text)
.join("\n") || "MCP tool returned an error.";
throw new Error(message);
}
const item = result.content?.find((content) => content.type === "text" && "text" in content);
if (!item) {
throw new Error("MCP tool response did not include text content.");
}
return item.text.trim();
}
async function callToolText(client, name, toolArgs, attempts = 3) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return textContent(await client.callTool(name, toolArgs));
} catch (error) {
lastError = error;
if (attempt === attempts) break;
const message = error instanceof Error ? error.message : String(error);
if (!/ECONNRESET|ETIMEDOUT|fetch failed|network|TMDB API request failed/i.test(message)) break;
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
}
}
throw lastError;
}
function fenced(text) {
return `\`\`\`text\n${text.replaceAll("```", "'''")}\n\`\`\``;
}
function excerpt(text, maxLines = 18) {
return text.split("\n").slice(0, maxLines).join("\n");
}
async function main() {
if (args.includes("--help") || args.includes("-h")) {
usage();
return;
}
const client = mcpUrl ? await createRemoteClient(mcpUrl) : await createLocalClient();
try {
const [
trendingMovies,
trendingTv,
languageTrends,
actionReady,
familySafe,
tasteProbe,
] = await Promise.all([
callToolText(client, "get_trending", { timeWindow: "week" }),
callToolText(client, "get_trending_tv", { timeWindow: "week" }),
callToolText(client, "get_weekly_trending_by_language", {}),
callToolText(client, "get_weekend_watchlist", {
mood: "crowd",
country,
language: "any",
runtime: "150",
minRating: "6.7",
services: ["Netflix", "Prime Video"],
}),
callToolText(client, "get_weekend_watchlist", {
mood: "family",
country,
language: "any",
runtime: "130",
minRating: "6.5",
services: ["Netflix", "Prime Video"],
familySafe: "true",
}),
callToolText(client, "recommend_from_taste_profile", {
likedTitles: ["The Matrix", "Inception"],
dislikedTitles: ["The Notebook"],
country,
services: ["Netflix", "Prime Video"],
language: "any",
runtime: "160",
minRating: "6.7",
maxResults: "5",
}),
]);
const generatedAt = new Date().toISOString();
const artifact = [
"# Weekly Streaming Radar",
"",
`Generated: ${generatedAt}`,
`Mode: ${client.mode}`,
`Country: ${country}`,
`Endpoint: ${mcpUrl || "local stdio dist/index.js"}`,
"",
"#...
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.