# Directory Structure ``` ├── .gitignore ├── build │ ├── config.js │ ├── handlers.js │ ├── index.js │ ├── prompts.js │ ├── resource-templates.js │ ├── resources.js │ ├── tmdb-api.js │ └── tools.js ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── config.ts │ ├── handlers.ts │ ├── index.ts │ ├── prompts.ts │ ├── resource-templates.ts │ ├── resources.ts │ ├── tmdb-api.ts │ └── tools.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` .env /node_modules ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # TMDB MCP Server This project implements a Model Context Protocol (MCP) server that integrates with The Movie Database (TMDB) API. It enables AI assistants like Claude to interact with movie data, providing capabilities for searching, retrieving details, and generating content related to movies. ## Features ### Resources - **Static Resources**: - `tmdb://info` - Information about TMDB API - `tmdb://trending` - Currently trending movies - **Resource Templates**: - `tmdb://movie/{id}` - Detailed information about a specific movie ### Prompts - **Movie Review**: Generate a customized movie review with specified style and rating - **Movie Recommendation**: Get personalized movie recommendations based on genres and mood ### Tools - **Search Movies**: Find movies by title or keywords - **Get Trending Movies**: Retrieve trending movies for day or week - **Get Similar Movies**: Find movies similar to a specified movie ## Setup Instructions ### Prerequisites - Node.js (v16 or later) - npm or yarn - TMDB API key ### Installation 1. Clone this repository ``` git clone https://github.com/your-username/tmdb-mcp.git cd tmdb-mcp ``` 2. Install dependencies ``` npm install ``` 3. Configure your TMDB API key - Create a `.env` file in the project root (alternative: edit `src/config.ts` directly) - Add your TMDB API key: `TMDB_API_KEY=your_api_key_here` 4. Build the project ``` npm run build ``` 5. Start the server ``` npm start ``` ### Setup for Claude Desktop 1. Open Claude Desktop 2. Go to Settings > Developer tab 3. Click "Edit Config" to open the configuration file 4. Add the following to your configuration: ```json { "mcpServers": { "tmdb-mcp": { "command": "node", "args": ["/absolute/path/to/your/tmdb-mcp/build/index.js"] } } } ``` 5. Restart Claude Desktop ## Usage Examples ### Using Static Resources - "What is TMDB?" - "Show me currently trending movies" ### Using Resource Templates - "Get details about movie with ID 550" (Fight Club) - "Tell me about the movie with ID 155" (The Dark Knight) ### Using Prompts - "Write a detailed review for Inception with a rating of 9/10" - "Recommend sci-fi movies for a thoughtful mood" ### Using Tools - "Search for movies about space exploration" - "What are the trending movies today?" - "Find movies similar to The Matrix" ## Development ### Project Structure ``` tmdb-mcp/ ├── src/ │ ├── index.ts # Main server file │ ├── config.ts # Configuration and API keys │ ├── handlers.ts # Request handlers │ ├── resources.ts # Static resources │ ├── resource-templates.ts # Dynamic resource templates │ ├── prompts.ts # Prompt definitions │ ├── tools.ts # Tool implementations │ └── tmdb-api.ts # TMDB API wrapper ├── package.json ├── tsconfig.json └── README.md ``` ### Testing Use the MCP Inspector to test your server during development: ``` npx @modelcontextprotocol/inspector node build/index.js ``` ## License MIT ## Acknowledgements - [The Movie Database (TMDB)](https://www.themoviedb.org/) - [Model Context Protocol](https://modelcontextprotocol.github.io/) ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import dotenv from "dotenv"; dotenv.config(); export const TMDB_API_KEY = process.env.TMDB_API_KEY; export const TMDB_BASE_URL = "https://api.themoviedb.org/3"; ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "tmdb-mcp", "version": "1.0.0", "type": "module", "scripts": { "build": "tsc", "start": "node build/index.js", "dev": "tsc && node build/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.1.0", "axios": "^1.8.3", "dotenv": "^16.4.7", "node-fetch": "^3.3.0" }, "devDependencies": { "@types/node": "^22.10.5", "typescript": "^5.7.2" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { setupHandlers } from "./handlers.js"; const server = new Server( { name: "tmdb-mcp", version: "1.0.0", }, { capabilities: { resources: {}, prompts: {}, tools: {}, }, }, ); setupHandlers(server); // Start server using stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.info( '{"jsonrpc": "2.0", "method": "log", "params": { "message": "TMDB MCP Server running..." }}', ); ``` -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- ```typescript import { getTrendingMovies } from './tmdb-api.js'; export const resources = [ { uri: "tmdb://info", name: "TMDB Info", description: "Information about The Movie Database API", mimeType: "text/plain", }, { uri: "tmdb://trending", name: "Trending Movies", description: "Currently trending movies on TMDB", mimeType: "application/json", } ]; export const resourceHandlers = { "tmdb://info": async () => ({ contents: [ { uri: "tmdb://info", text: "The Movie Database (TMDB) is a popular, user-editable database for movies and TV shows. This MCP server provides access to TMDB data through resources, prompts, and tools.", }, ], }), "tmdb://trending": async () => { const trendingData = await getTrendingMovies(); return { contents: [ { uri: "tmdb://trending", text: JSON.stringify(trendingData, null, 2), }, ], }; }, }; ``` -------------------------------------------------------------------------------- /src/resource-templates.ts: -------------------------------------------------------------------------------- ```typescript import { getMovieDetails } from './tmdb-api.js'; export const resourceTemplates = [ { uriTemplate: "tmdb://movie/{id}", name: "Movie Details", description: "Get details about a specific movie by ID", mimeType: "application/json", }, ]; const movieDetailsExp = /^tmdb:\/\/movie\/(\d+)$/; export const getResourceTemplate = async (uri: string) => { const movieMatch = uri.match(movieDetailsExp); if (movieMatch) { const movieId = movieMatch[1]; return async () => { try { // Get the raw movie details from your API const movieDetails = await getMovieDetails(movieId); // Return in the correct format expected by the MCP SDK return { contents: [ { uri, text: JSON.stringify(movieDetails, null, 2), // This should be the raw movie data }, ], }; } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to fetch movie details: ${error.message}`); } throw new Error('Failed to fetch movie details: Unknown error'); } }; } return null; }; ``` -------------------------------------------------------------------------------- /src/prompts.ts: -------------------------------------------------------------------------------- ```typescript export const prompts = { "movie-review": { name: "movie-review", description: "Create a movie review based on provided details", arguments: [ { name: "title", description: "Title of the movie", required: true, }, { name: "rating", description: "Your rating of the movie (1-10)", required: true, }, { name: "style", description: "Review style (brief, detailed, critical)", required: false, }, ], }, "movie-recommendation": { name: "movie-recommendation", description: "Get personalized movie recommendations", arguments: [ { name: "genres", description: "Preferred genres (comma-separated)", required: true, }, { name: "mood", description: "Current mood (happy, thoughtful, excited, etc.)", required: false, }, { name: "avoidGenres", description: "Genres to avoid (comma-separated)", required: false, }, ], }, }; export const promptHandlers = { "movie-review": ({ title, rating, style = "detailed", }: { title: string; rating: number; style?: string; }) => { return { messages: [ { role: "user", content: { type: "text", text: `Write a ${style} review for the movie "${title}" with a rating of ${rating}/10. Include your thoughts on the plot, characters, direction, and overall experience.`, }, }, ], }; }, "movie-recommendation": ({ genres, mood = "any", avoidGenres = "", }: { genres: string[]; mood?: string; avoidGenres?: string; }) => { return { messages: [ { role: "user", content: { type: "text", text: `Recommend movies in the following genres: ${genres}. I'm in a ${mood} mood. Please avoid these genres if possible: ${avoidGenres}. Include a brief description of why you're recommending each movie.`, }, }, ], }; }, }; ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { resourceHandlers, resources } from "./resources.js"; import { getResourceTemplate, resourceTemplates } from "./resource-templates.js"; import { promptHandlers, prompts } from "./prompts.js"; import { toolHandlers, tools } from "./tools.js"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; export const setupHandlers = (server: Server): void => { // Resource handlers server.setRequestHandler( ListResourcesRequestSchema, async () => ({ resources }), ); server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates, })); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; // Using type assertion to tell TypeScript this is a valid key const resourceHandler = resourceHandlers[uri as keyof typeof resourceHandlers]; if (resourceHandler) return await resourceHandler(); const resourceTemplateHandler = await getResourceTemplate(uri); if (resourceTemplateHandler) return await resourceTemplateHandler(); throw new Error(`Resource not found: ${uri}`); }); // Prompt handlers server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: Object.values(prompts), })); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Using type assertion to tell TypeScript this is a valid key const promptHandler = promptHandlers[name as keyof typeof promptHandlers]; if (promptHandler) { return promptHandler(args as any); } throw new Error(`Prompt not found: ${name}`); }); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: Object.values(tools), })); // This is the key fix - we need to format the response properly server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; // Using type assertion to tell TypeScript this is a valid key const handler = toolHandlers[name as keyof typeof toolHandlers]; if (!handler) throw new Error(`Tool not found: ${name}`); // Execute the handler but wrap the response in the expected format const result = await handler(args as any); // Return in the format expected by the SDK return { tools: [{ name, inputSchema: { type: "object", properties: {} // This would ideally be populated with actual schema }, description: `Tool: ${name}`, result }] }; } catch (error) { // Properly handle errors if (error instanceof Error) { return { tools: [], error: error.message }; } return { tools: [], error: "An unknown error occurred" }; } }); }; ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript import { searchMovies, getTrendingMovies, getSimilarMovies, getMovieDetails, } from "./tmdb-api.js"; export const tools = { "search-movies": { name: "search-movies", description: "Search for movies by title or keywords", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query", }, page: { type: "number", description: "Page number for results", }, }, required: ["query"], }, }, "get-trending": { name: "get-trending", description: "Get trending movies", inputSchema: { type: "object", properties: { timeWindow: { type: "string", enum: ["day", "week"], description: "Time window for trending movies", }, }, required: [], }, }, "get-similar": { name: "get-similar", description: "Get similar movies to a given movie", inputSchema: { type: "object", properties: { movieId: { type: "string", description: "ID of the movie to find similar movies for", }, }, required: ["movieId"], }, }, "get-movie-details": { name: "get-movie-details", description: "Get detailed information about a specific movie", inputSchema: { type: "object", properties: { movieId: { type: "string", description: "ID of the movie to get details for", }, }, required: ["movieId"], }, }, }; export const toolHandlers = { "search-movies": async ({ query, page = 1, }: { query: string; page?: number; }) => { try { // Return the raw results directly return await searchMovies(query, page); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to search movies: ${error.message}`); } throw new Error("Failed to search movies: Unknown error"); } }, "get-trending": async ({ timeWindow = "week", }: { timeWindow?: "day" | "week"; }) => { try { // Return the raw results directly return await getTrendingMovies(timeWindow); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to get trending movies: ${error.message}`); } throw new Error("Failed to get trending movies: Unknown error"); } }, "get-similar": async ({ movieId }: { movieId: string }) => { try { // Return the raw results directly return await getSimilarMovies(movieId); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to get similar movies: ${error.message}`); } throw new Error("Failed to get similar movies: Unknown error"); } }, "get-movie-details": async ({ movieId }: { movieId: string }) => { try { const result = await getMovieDetails(movieId); return result; } catch (error: unknown) { if (error instanceof Error) { return { text: `Failed to get movie details: ${error.message}` }; } return { text: "Failed to get movie details: Unknown error" }; } }, }; ``` -------------------------------------------------------------------------------- /src/tmdb-api.ts: -------------------------------------------------------------------------------- ```typescript import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'; import { TMDB_API_KEY, TMDB_BASE_URL } from './config.js'; // Response types interface MovieResult { id: number; title: string; poster_path: string | null; backdrop_path: string | null; overview: string; release_date: string; vote_average: number; [key: string]: any; } interface SearchMoviesResponse { page: number; results: MovieResult[]; total_results: number; total_pages: number; } interface MovieDetailsResponse extends MovieResult { credits: { cast: Array<{ id: number; name: string; character: string; profile_path: string | null; [key: string]: any; }>; crew: Array<{ id: number; name: string; job: string; department: string; [key: string]: any; }>; }; videos: { results: Array<{ id: string; key: string; site: string; type: string; [key: string]: any; }>; }; images: { backdrops: Array<{ file_path: string; width: number; height: number; [key: string]: any; }>; posters: Array<{ file_path: string; width: number; height: number; [key: string]: any; }>; }; } // Axios instance with default configurations const tmdbClient = axios.create({ baseURL: TMDB_BASE_URL, timeout: 10000, // 10 seconds timeout params: { api_key: TMDB_API_KEY } }); // Retry logic const axiosWithRetry = async <T>( config: AxiosRequestConfig, retries: number = 3, backoff: number = 300 ): Promise<AxiosResponse<T>> => { try { return await tmdbClient(config); } catch (err) { const error = err as AxiosError; if (retries > 0 && ( error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || (error.response && (error.response.status >= 500 || error.response.status === 429)) )) { console.log(`Request failed, retrying... (${retries} attempts left)`); await new Promise(resolve => setTimeout(resolve, backoff)); return axiosWithRetry<T>(config, retries - 1, backoff * 2); } throw error; } }; export async function searchMovies(query: string, page: number = 1): Promise<SearchMoviesResponse> { try { const response = await axiosWithRetry<SearchMoviesResponse>({ url: '/search/movie', params: { query: query, page: page } }); return response.data; } catch (error) { const err = error as Error; console.error('Error searching movies:', err.message); throw new Error(`Failed to search movies: ${err.message}`); } } export async function getMovieDetails(movieId: number | string): Promise<MovieDetailsResponse> { try { const response = await axiosWithRetry<MovieDetailsResponse>({ url: `/movie/${movieId}`, params: { append_to_response: 'credits,videos,images' } }); return response.data; } catch (error) { const err = error as Error; console.error('Error getting movie details:', err.message); throw new Error(`Failed to get movie details: ${err.message}`); } } export async function getTrendingMovies(timeWindow: 'day' | 'week' = 'week'): Promise<SearchMoviesResponse> { try { const response = await axiosWithRetry<SearchMoviesResponse>({ url: `/trending/movie/${timeWindow}` }); return response.data; } catch (error) { const err = error as Error; console.error('Error getting trending movies:', err.message); throw new Error(`Failed to get trending movies: ${err.message}`); } } export async function getSimilarMovies(movieId: number | string): Promise<SearchMoviesResponse> { try { const response = await axiosWithRetry<SearchMoviesResponse>({ url: `/movie/${movieId}/similar` }); return response.data; } catch (error) { const err = error as Error; console.error('Error getting similar movies:', err.message); throw new Error(`Failed to get similar movies: ${err.message}`); } } ```