# 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: -------------------------------------------------------------------------------- ``` 1 | .env 2 | /node_modules ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # TMDB MCP Server 2 | 3 | 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. 4 | 5 | ## Features 6 | 7 | ### Resources 8 | - **Static Resources**: 9 | - `tmdb://info` - Information about TMDB API 10 | - `tmdb://trending` - Currently trending movies 11 | 12 | - **Resource Templates**: 13 | - `tmdb://movie/{id}` - Detailed information about a specific movie 14 | 15 | ### Prompts 16 | - **Movie Review**: Generate a customized movie review with specified style and rating 17 | - **Movie Recommendation**: Get personalized movie recommendations based on genres and mood 18 | 19 | ### Tools 20 | - **Search Movies**: Find movies by title or keywords 21 | - **Get Trending Movies**: Retrieve trending movies for day or week 22 | - **Get Similar Movies**: Find movies similar to a specified movie 23 | 24 | ## Setup Instructions 25 | 26 | ### Prerequisites 27 | - Node.js (v16 or later) 28 | - npm or yarn 29 | - TMDB API key 30 | 31 | ### Installation 32 | 33 | 1. Clone this repository 34 | ``` 35 | git clone https://github.com/your-username/tmdb-mcp.git 36 | cd tmdb-mcp 37 | ``` 38 | 39 | 2. Install dependencies 40 | ``` 41 | npm install 42 | ``` 43 | 44 | 3. Configure your TMDB API key 45 | - Create a `.env` file in the project root (alternative: edit `src/config.ts` directly) 46 | - Add your TMDB API key: `TMDB_API_KEY=your_api_key_here` 47 | 48 | 4. Build the project 49 | ``` 50 | npm run build 51 | ``` 52 | 53 | 5. Start the server 54 | ``` 55 | npm start 56 | ``` 57 | 58 | ### Setup for Claude Desktop 59 | 60 | 1. Open Claude Desktop 61 | 2. Go to Settings > Developer tab 62 | 3. Click "Edit Config" to open the configuration file 63 | 4. Add the following to your configuration: 64 | 65 | ```json 66 | { 67 | "mcpServers": { 68 | "tmdb-mcp": { 69 | "command": "node", 70 | "args": ["/absolute/path/to/your/tmdb-mcp/build/index.js"] 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | 5. Restart Claude Desktop 77 | 78 | ## Usage Examples 79 | 80 | ### Using Static Resources 81 | 82 | - "What is TMDB?" 83 | - "Show me currently trending movies" 84 | 85 | ### Using Resource Templates 86 | 87 | - "Get details about movie with ID 550" (Fight Club) 88 | - "Tell me about the movie with ID 155" (The Dark Knight) 89 | 90 | ### Using Prompts 91 | 92 | - "Write a detailed review for Inception with a rating of 9/10" 93 | - "Recommend sci-fi movies for a thoughtful mood" 94 | 95 | ### Using Tools 96 | 97 | - "Search for movies about space exploration" 98 | - "What are the trending movies today?" 99 | - "Find movies similar to The Matrix" 100 | 101 | ## Development 102 | 103 | ### Project Structure 104 | 105 | ``` 106 | tmdb-mcp/ 107 | ├── src/ 108 | │ ├── index.ts # Main server file 109 | │ ├── config.ts # Configuration and API keys 110 | │ ├── handlers.ts # Request handlers 111 | │ ├── resources.ts # Static resources 112 | │ ├── resource-templates.ts # Dynamic resource templates 113 | │ ├── prompts.ts # Prompt definitions 114 | │ ├── tools.ts # Tool implementations 115 | │ └── tmdb-api.ts # TMDB API wrapper 116 | ├── package.json 117 | ├── tsconfig.json 118 | └── README.md 119 | ``` 120 | 121 | ### Testing 122 | 123 | Use the MCP Inspector to test your server during development: 124 | 125 | ``` 126 | npx @modelcontextprotocol/inspector node build/index.js 127 | ``` 128 | 129 | ## License 130 | 131 | MIT 132 | 133 | ## Acknowledgements 134 | 135 | - [The Movie Database (TMDB)](https://www.themoviedb.org/) 136 | - [Model Context Protocol](https://modelcontextprotocol.github.io/) ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import dotenv from "dotenv"; 2 | 3 | dotenv.config(); 4 | 5 | export const TMDB_API_KEY = process.env.TMDB_API_KEY; 6 | export const TMDB_BASE_URL = "https://api.themoviedb.org/3"; ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "tmdb-mcp", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node build/index.js", 8 | "dev": "tsc && node build/index.js" 9 | }, 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.1.0", 12 | "axios": "^1.8.3", 13 | "dotenv": "^16.4.7", 14 | "node-fetch": "^3.3.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.10.5", 18 | "typescript": "^5.7.2" 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { setupHandlers } from "./handlers.js"; 4 | 5 | const server = new Server( 6 | { 7 | name: "tmdb-mcp", 8 | version: "1.0.0", 9 | }, 10 | { 11 | capabilities: { 12 | resources: {}, 13 | prompts: {}, 14 | tools: {}, 15 | }, 16 | }, 17 | ); 18 | 19 | setupHandlers(server); 20 | 21 | // Start server using stdio transport 22 | const transport = new StdioServerTransport(); 23 | await server.connect(transport); 24 | 25 | console.info( 26 | '{"jsonrpc": "2.0", "method": "log", "params": { "message": "TMDB MCP Server running..." }}', 27 | ); ``` -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getTrendingMovies } from './tmdb-api.js'; 2 | 3 | export const resources = [ 4 | { 5 | uri: "tmdb://info", 6 | name: "TMDB Info", 7 | description: "Information about The Movie Database API", 8 | mimeType: "text/plain", 9 | }, 10 | { 11 | uri: "tmdb://trending", 12 | name: "Trending Movies", 13 | description: "Currently trending movies on TMDB", 14 | mimeType: "application/json", 15 | } 16 | ]; 17 | 18 | export const resourceHandlers = { 19 | "tmdb://info": async () => ({ 20 | contents: [ 21 | { 22 | uri: "tmdb://info", 23 | 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.", 24 | }, 25 | ], 26 | }), 27 | "tmdb://trending": async () => { 28 | const trendingData = await getTrendingMovies(); 29 | return { 30 | contents: [ 31 | { 32 | uri: "tmdb://trending", 33 | text: JSON.stringify(trendingData, null, 2), 34 | }, 35 | ], 36 | }; 37 | }, 38 | }; ``` -------------------------------------------------------------------------------- /src/resource-templates.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getMovieDetails } from './tmdb-api.js'; 2 | 3 | export const resourceTemplates = [ 4 | { 5 | uriTemplate: "tmdb://movie/{id}", 6 | name: "Movie Details", 7 | description: "Get details about a specific movie by ID", 8 | mimeType: "application/json", 9 | }, 10 | ]; 11 | 12 | const movieDetailsExp = /^tmdb:\/\/movie\/(\d+)$/; 13 | 14 | export const getResourceTemplate = async (uri: string) => { 15 | const movieMatch = uri.match(movieDetailsExp); 16 | if (movieMatch) { 17 | const movieId = movieMatch[1]; 18 | 19 | return async () => { 20 | try { 21 | // Get the raw movie details from your API 22 | const movieDetails = await getMovieDetails(movieId); 23 | 24 | // Return in the correct format expected by the MCP SDK 25 | return { 26 | contents: [ 27 | { 28 | uri, 29 | text: JSON.stringify(movieDetails, null, 2), // This should be the raw movie data 30 | }, 31 | ], 32 | }; 33 | } catch (error: unknown) { 34 | if (error instanceof Error) { 35 | throw new Error(`Failed to fetch movie details: ${error.message}`); 36 | } 37 | throw new Error('Failed to fetch movie details: Unknown error'); 38 | } 39 | }; 40 | } 41 | return null; 42 | }; ``` -------------------------------------------------------------------------------- /src/prompts.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const prompts = { 2 | "movie-review": { 3 | name: "movie-review", 4 | description: "Create a movie review based on provided details", 5 | arguments: [ 6 | { 7 | name: "title", 8 | description: "Title of the movie", 9 | required: true, 10 | }, 11 | { 12 | name: "rating", 13 | description: "Your rating of the movie (1-10)", 14 | required: true, 15 | }, 16 | { 17 | name: "style", 18 | description: "Review style (brief, detailed, critical)", 19 | required: false, 20 | }, 21 | ], 22 | }, 23 | "movie-recommendation": { 24 | name: "movie-recommendation", 25 | description: "Get personalized movie recommendations", 26 | arguments: [ 27 | { 28 | name: "genres", 29 | description: "Preferred genres (comma-separated)", 30 | required: true, 31 | }, 32 | { 33 | name: "mood", 34 | description: "Current mood (happy, thoughtful, excited, etc.)", 35 | required: false, 36 | }, 37 | { 38 | name: "avoidGenres", 39 | description: "Genres to avoid (comma-separated)", 40 | required: false, 41 | }, 42 | ], 43 | }, 44 | }; 45 | 46 | export const promptHandlers = { 47 | "movie-review": ({ 48 | title, 49 | rating, 50 | style = "detailed", 51 | }: { 52 | title: string; 53 | rating: number; 54 | style?: string; 55 | }) => { 56 | return { 57 | messages: [ 58 | { 59 | role: "user", 60 | content: { 61 | type: "text", 62 | 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.`, 63 | }, 64 | }, 65 | ], 66 | }; 67 | }, 68 | "movie-recommendation": ({ 69 | genres, 70 | mood = "any", 71 | avoidGenres = "", 72 | }: { 73 | genres: string[]; 74 | mood?: string; 75 | avoidGenres?: string; 76 | }) => { 77 | return { 78 | messages: [ 79 | { 80 | role: "user", 81 | content: { 82 | type: "text", 83 | 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.`, 84 | }, 85 | }, 86 | ], 87 | }; 88 | }, 89 | }; 90 | ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | CallToolRequestSchema, 3 | GetPromptRequestSchema, 4 | ListPromptsRequestSchema, 5 | ListResourcesRequestSchema, 6 | ListResourceTemplatesRequestSchema, 7 | ListToolsRequestSchema, 8 | ReadResourceRequestSchema, 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | import { resourceHandlers, resources } from "./resources.js"; 11 | import { getResourceTemplate, resourceTemplates } from "./resource-templates.js"; 12 | import { promptHandlers, prompts } from "./prompts.js"; 13 | import { toolHandlers, tools } from "./tools.js"; 14 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 15 | 16 | export const setupHandlers = (server: Server): void => { 17 | // Resource handlers 18 | server.setRequestHandler( 19 | ListResourcesRequestSchema, 20 | async () => ({ resources }), 21 | ); 22 | 23 | server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ 24 | resourceTemplates, 25 | })); 26 | 27 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 28 | const { uri } = request.params; 29 | // Using type assertion to tell TypeScript this is a valid key 30 | const resourceHandler = resourceHandlers[uri as keyof typeof resourceHandlers]; 31 | if (resourceHandler) return await resourceHandler(); 32 | 33 | const resourceTemplateHandler = await getResourceTemplate(uri); 34 | if (resourceTemplateHandler) return await resourceTemplateHandler(); 35 | 36 | throw new Error(`Resource not found: ${uri}`); 37 | }); 38 | 39 | // Prompt handlers 40 | server.setRequestHandler(ListPromptsRequestSchema, async () => ({ 41 | prompts: Object.values(prompts), 42 | })); 43 | 44 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 45 | const { name, arguments: args } = request.params; 46 | // Using type assertion to tell TypeScript this is a valid key 47 | const promptHandler = promptHandlers[name as keyof typeof promptHandlers]; 48 | 49 | if (promptHandler) { 50 | return promptHandler(args as any); 51 | } 52 | 53 | throw new Error(`Prompt not found: ${name}`); 54 | }); 55 | 56 | // Tool handlers 57 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 58 | tools: Object.values(tools), 59 | })); 60 | 61 | // This is the key fix - we need to format the response properly 62 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 63 | try { 64 | const { name, arguments: args } = request.params; 65 | // Using type assertion to tell TypeScript this is a valid key 66 | const handler = toolHandlers[name as keyof typeof toolHandlers]; 67 | 68 | if (!handler) throw new Error(`Tool not found: ${name}`); 69 | 70 | // Execute the handler but wrap the response in the expected format 71 | const result = await handler(args as any); 72 | 73 | // Return in the format expected by the SDK 74 | return { 75 | tools: [{ 76 | name, 77 | inputSchema: { 78 | type: "object", 79 | properties: {} // This would ideally be populated with actual schema 80 | }, 81 | description: `Tool: ${name}`, 82 | result 83 | }] 84 | }; 85 | } catch (error) { 86 | // Properly handle errors 87 | if (error instanceof Error) { 88 | return { 89 | tools: [], 90 | error: error.message 91 | }; 92 | } 93 | return { 94 | tools: [], 95 | error: "An unknown error occurred" 96 | }; 97 | } 98 | }); 99 | }; ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | searchMovies, 3 | getTrendingMovies, 4 | getSimilarMovies, 5 | getMovieDetails, 6 | } from "./tmdb-api.js"; 7 | 8 | export const tools = { 9 | "search-movies": { 10 | name: "search-movies", 11 | description: "Search for movies by title or keywords", 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | query: { 16 | type: "string", 17 | description: "Search query", 18 | }, 19 | page: { 20 | type: "number", 21 | description: "Page number for results", 22 | }, 23 | }, 24 | required: ["query"], 25 | }, 26 | }, 27 | "get-trending": { 28 | name: "get-trending", 29 | description: "Get trending movies", 30 | inputSchema: { 31 | type: "object", 32 | properties: { 33 | timeWindow: { 34 | type: "string", 35 | enum: ["day", "week"], 36 | description: "Time window for trending movies", 37 | }, 38 | }, 39 | required: [], 40 | }, 41 | }, 42 | "get-similar": { 43 | name: "get-similar", 44 | description: "Get similar movies to a given movie", 45 | inputSchema: { 46 | type: "object", 47 | properties: { 48 | movieId: { 49 | type: "string", 50 | description: "ID of the movie to find similar movies for", 51 | }, 52 | }, 53 | required: ["movieId"], 54 | }, 55 | }, 56 | "get-movie-details": { 57 | name: "get-movie-details", 58 | description: "Get detailed information about a specific movie", 59 | inputSchema: { 60 | type: "object", 61 | properties: { 62 | movieId: { 63 | type: "string", 64 | description: "ID of the movie to get details for", 65 | }, 66 | }, 67 | required: ["movieId"], 68 | }, 69 | }, 70 | }; 71 | 72 | export const toolHandlers = { 73 | "search-movies": async ({ 74 | query, 75 | page = 1, 76 | }: { 77 | query: string; 78 | page?: number; 79 | }) => { 80 | try { 81 | // Return the raw results directly 82 | return await searchMovies(query, page); 83 | } catch (error: unknown) { 84 | if (error instanceof Error) { 85 | throw new Error(`Failed to search movies: ${error.message}`); 86 | } 87 | throw new Error("Failed to search movies: Unknown error"); 88 | } 89 | }, 90 | "get-trending": async ({ 91 | timeWindow = "week", 92 | }: { 93 | timeWindow?: "day" | "week"; 94 | }) => { 95 | try { 96 | // Return the raw results directly 97 | return await getTrendingMovies(timeWindow); 98 | } catch (error: unknown) { 99 | if (error instanceof Error) { 100 | throw new Error(`Failed to get trending movies: ${error.message}`); 101 | } 102 | throw new Error("Failed to get trending movies: Unknown error"); 103 | } 104 | }, 105 | "get-similar": async ({ movieId }: { movieId: string }) => { 106 | try { 107 | // Return the raw results directly 108 | return await getSimilarMovies(movieId); 109 | } catch (error: unknown) { 110 | if (error instanceof Error) { 111 | throw new Error(`Failed to get similar movies: ${error.message}`); 112 | } 113 | throw new Error("Failed to get similar movies: Unknown error"); 114 | } 115 | }, 116 | "get-movie-details": async ({ movieId }: { movieId: string }) => { 117 | try { 118 | const result = await getMovieDetails(movieId); 119 | return result; 120 | } catch (error: unknown) { 121 | if (error instanceof Error) { 122 | return { text: `Failed to get movie details: ${error.message}` }; 123 | } 124 | return { text: "Failed to get movie details: Unknown error" }; 125 | } 126 | }, 127 | }; 128 | ``` -------------------------------------------------------------------------------- /src/tmdb-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'; 2 | import { TMDB_API_KEY, TMDB_BASE_URL } from './config.js'; 3 | 4 | // Response types 5 | interface MovieResult { 6 | id: number; 7 | title: string; 8 | poster_path: string | null; 9 | backdrop_path: string | null; 10 | overview: string; 11 | release_date: string; 12 | vote_average: number; 13 | [key: string]: any; 14 | } 15 | 16 | interface SearchMoviesResponse { 17 | page: number; 18 | results: MovieResult[]; 19 | total_results: number; 20 | total_pages: number; 21 | } 22 | 23 | interface MovieDetailsResponse extends MovieResult { 24 | credits: { 25 | cast: Array<{ 26 | id: number; 27 | name: string; 28 | character: string; 29 | profile_path: string | null; 30 | [key: string]: any; 31 | }>; 32 | crew: Array<{ 33 | id: number; 34 | name: string; 35 | job: string; 36 | department: string; 37 | [key: string]: any; 38 | }>; 39 | }; 40 | videos: { 41 | results: Array<{ 42 | id: string; 43 | key: string; 44 | site: string; 45 | type: string; 46 | [key: string]: any; 47 | }>; 48 | }; 49 | images: { 50 | backdrops: Array<{ 51 | file_path: string; 52 | width: number; 53 | height: number; 54 | [key: string]: any; 55 | }>; 56 | posters: Array<{ 57 | file_path: string; 58 | width: number; 59 | height: number; 60 | [key: string]: any; 61 | }>; 62 | }; 63 | } 64 | 65 | // Axios instance with default configurations 66 | const tmdbClient = axios.create({ 67 | baseURL: TMDB_BASE_URL, 68 | timeout: 10000, // 10 seconds timeout 69 | params: { 70 | api_key: TMDB_API_KEY 71 | } 72 | }); 73 | 74 | // Retry logic 75 | const axiosWithRetry = async <T>( 76 | config: AxiosRequestConfig, 77 | retries: number = 3, 78 | backoff: number = 300 79 | ): Promise<AxiosResponse<T>> => { 80 | try { 81 | return await tmdbClient(config); 82 | } catch (err) { 83 | const error = err as AxiosError; 84 | 85 | if (retries > 0 && ( 86 | error.code === 'ECONNRESET' || 87 | error.code === 'ETIMEDOUT' || 88 | (error.response && (error.response.status >= 500 || error.response.status === 429)) 89 | )) { 90 | console.log(`Request failed, retrying... (${retries} attempts left)`); 91 | await new Promise(resolve => setTimeout(resolve, backoff)); 92 | return axiosWithRetry<T>(config, retries - 1, backoff * 2); 93 | } 94 | throw error; 95 | } 96 | }; 97 | 98 | export async function searchMovies(query: string, page: number = 1): Promise<SearchMoviesResponse> { 99 | try { 100 | const response = await axiosWithRetry<SearchMoviesResponse>({ 101 | url: '/search/movie', 102 | params: { 103 | query: query, 104 | page: page 105 | } 106 | }); 107 | return response.data; 108 | } catch (error) { 109 | const err = error as Error; 110 | console.error('Error searching movies:', err.message); 111 | throw new Error(`Failed to search movies: ${err.message}`); 112 | } 113 | } 114 | 115 | export async function getMovieDetails(movieId: number | string): Promise<MovieDetailsResponse> { 116 | try { 117 | const response = await axiosWithRetry<MovieDetailsResponse>({ 118 | url: `/movie/${movieId}`, 119 | params: { 120 | append_to_response: 'credits,videos,images' 121 | } 122 | }); 123 | return response.data; 124 | } catch (error) { 125 | const err = error as Error; 126 | console.error('Error getting movie details:', err.message); 127 | throw new Error(`Failed to get movie details: ${err.message}`); 128 | } 129 | } 130 | 131 | export async function getTrendingMovies(timeWindow: 'day' | 'week' = 'week'): Promise<SearchMoviesResponse> { 132 | try { 133 | const response = await axiosWithRetry<SearchMoviesResponse>({ 134 | url: `/trending/movie/${timeWindow}` 135 | }); 136 | return response.data; 137 | } catch (error) { 138 | const err = error as Error; 139 | console.error('Error getting trending movies:', err.message); 140 | throw new Error(`Failed to get trending movies: ${err.message}`); 141 | } 142 | } 143 | 144 | export async function getSimilarMovies(movieId: number | string): Promise<SearchMoviesResponse> { 145 | try { 146 | const response = await axiosWithRetry<SearchMoviesResponse>({ 147 | url: `/movie/${movieId}/similar` 148 | }); 149 | return response.data; 150 | } catch (error) { 151 | const err = error as Error; 152 | console.error('Error getting similar movies:', err.message); 153 | throw new Error(`Failed to get similar movies: ${err.message}`); 154 | } 155 | } ```