@lonzzi/tmdb-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # TMDB MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that allows LLMs to search for movies and TV shows using the The Movie Database (TMDB) API.
4
+
5
+ ## Features
6
+
7
+ - **Search Movies**: Find movies by title and get metadata including ID, URL, release date, rating, and overview.
8
+ - **Search TV Shows**: Find TV shows by title and get metadata including ID, URL, first air date, rating, and overview.
9
+ - **Direct Links**: Provides TMDb URLs for easy access to more details.
10
+ - **Top Results**: Returns the top 5 most relevant results for each query.
11
+
12
+ ## Prerequisites
13
+
14
+ - [Node.js](https://nodejs.org/) (v18 or higher)
15
+ - A TMDB API Key. You can get one by creating an account on [themoviedb.org](https://www.themoviedb.org/) and applying for an API key in your account settings.
16
+
17
+ ## Installation
18
+
19
+ 1. Clone the repository:
20
+ ```bash
21
+ git clone <repository-url>
22
+ cd tmdb_mcp
23
+ ```
24
+
25
+ 2. Install dependencies:
26
+ ```bash
27
+ npm install
28
+ ```
29
+
30
+ 3. Build the project:
31
+ ```bash
32
+ npm run build
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ ### Environment Variables
38
+
39
+ Create a `.env` file in the root directory (or use `.env.example` as a template):
40
+
41
+ ```env
42
+ TMDB_API_KEY=your_api_key_here
43
+ ```
44
+
45
+ ### MCP Client Configuration (e.g., Claude Desktop)
46
+
47
+ To use this server with Claude Desktop, add it to your `claude_desktop_config.json`:
48
+
49
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
50
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "tmdb": {
56
+ "command": "node",
57
+ "args": ["/absolute/path/to/tmdb_mcp/build/index.js"],
58
+ "env": {
59
+ "TMDB_API_KEY": "your_api_key_here"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ *Note: Replace `/absolute/path/to/tmdb_mcp/` with the actual path to your project directory.*
67
+
68
+ ## Available Tools
69
+
70
+ ### `search_movies`
71
+ Search for movies on TMDB by title.
72
+ - **Arguments**:
73
+ - `query` (string, required): The movie title to search for.
74
+
75
+ ### `search_tv_shows`
76
+ Search for TV shows on TMDB by title.
77
+ - **Arguments**:
78
+ - `query` (string, required): The TV show title to search for.
79
+
80
+ ## Development
81
+
82
+ ### Running Tests
83
+ ```bash
84
+ npm test
85
+ ```
86
+
87
+ ### Building
88
+ ```bash
89
+ npm run build
90
+ ```
package/build/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const zod_1 = require("zod");
10
+ const dotenv_1 = __importDefault(require("dotenv"));
11
+ const tmdb_1 = require("./tmdb");
12
+ dotenv_1.default.config();
13
+ const API_KEY = process.env.TMDB_API_KEY;
14
+ if (!API_KEY) {
15
+ console.error("TMDB_API_KEY environment variable is not set");
16
+ process.exit(1);
17
+ }
18
+ const server = new mcp_js_1.McpServer({
19
+ name: "tmdb-search",
20
+ version: "1.0.0",
21
+ });
22
+ server.registerTool("search_movies", {
23
+ description: "Search for movies on TMDB by title to get metadata like overview, release date, and rating",
24
+ inputSchema: zod_1.z.object({
25
+ query: zod_1.z.string().describe("The movie title to search for"),
26
+ }),
27
+ }, async ({ query }) => {
28
+ try {
29
+ const result = await (0, tmdb_1.searchMovies)(API_KEY, query);
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: JSON.stringify(result, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: error.message || "Unknown error occurred",
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ });
51
+ server.registerTool("search_tv_shows", {
52
+ description: "Search for TV shows on TMDB by title to get metadata like overview, first air date, and rating",
53
+ inputSchema: zod_1.z.object({
54
+ query: zod_1.z.string().describe("The TV show title to search for"),
55
+ }),
56
+ }, async ({ query }) => {
57
+ try {
58
+ const result = await (0, tmdb_1.searchTvShows)(API_KEY, query);
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: JSON.stringify(result, null, 2),
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: error.message || "Unknown error occurred",
74
+ },
75
+ ],
76
+ isError: true,
77
+ };
78
+ }
79
+ });
80
+ const main = async () => {
81
+ const transport = new stdio_js_1.StdioServerTransport();
82
+ await server.connect(transport);
83
+ };
84
+ main().catch((error) => {
85
+ console.error("Server error:", error);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tmdb_1 = require("./tmdb");
7
+ const dotenv_1 = __importDefault(require("dotenv"));
8
+ const path_1 = __importDefault(require("path"));
9
+ // Load environment variables from .env file
10
+ dotenv_1.default.config({ path: path_1.default.resolve(__dirname, "../.env") });
11
+ const apiKey = process.env.TMDB_API_KEY;
12
+ // Conditional describe: Only run if API key is present
13
+ const describeIfApiKey = apiKey ? describe : describe.skip;
14
+ describeIfApiKey("TMDB Integration Tests", () => {
15
+ // Increase timeout for real network requests
16
+ jest.setTimeout(30000);
17
+ it("should find 'Inception'", async () => {
18
+ console.log("Querying TMDB for 'Inception'...");
19
+ const results = await (0, tmdb_1.searchMovies)(apiKey, "Inception");
20
+ expect(results.length).toBeGreaterThan(0);
21
+ const inception = results.find(m => m.title === "Inception");
22
+ expect(inception).toBeDefined();
23
+ expect(inception?.id).toBe(27205); // TMDB ID for Inception
24
+ });
25
+ it("should find 'Breaking Bad'", async () => {
26
+ console.log("Querying TMDB for 'Breaking Bad'...");
27
+ const results = await (0, tmdb_1.searchTvShows)(apiKey, "Breaking Bad");
28
+ expect(results.length).toBeGreaterThan(0);
29
+ const breakingBad = results.find(show => show.name === "Breaking Bad");
30
+ expect(breakingBad).toBeDefined();
31
+ expect(breakingBad?.id).toBe(1396); // TMDB ID for Breaking Bad
32
+ });
33
+ it("should find 'The Matrix'", async () => {
34
+ console.log("Querying TMDB for 'The Matrix'...");
35
+ const results = await (0, tmdb_1.searchMovies)(apiKey, "The Matrix");
36
+ expect(results.length).toBeGreaterThan(0);
37
+ const matrix = results.find(m => m.title === "The Matrix");
38
+ expect(matrix).toBeDefined();
39
+ expect(matrix?.id).toBe(603); // TMDB ID for The Matrix
40
+ });
41
+ it("should find 'One Punch Man'", async () => {
42
+ console.log("Querying TMDB for 'One Punch Man'...");
43
+ const results = await (0, tmdb_1.searchTvShows)(apiKey, "One Punch Man");
44
+ expect(results.length).toBeGreaterThan(0);
45
+ const opm = results.find(show => show.name === "One-Punch Man");
46
+ expect(opm).toBeDefined();
47
+ expect(opm?.id).toBe(63926); // TMDB ID for One Punch Man
48
+ });
49
+ it("should return empty array for gibberish query", async () => {
50
+ const results = await (0, tmdb_1.searchMovies)(apiKey, "asdfjkl;qweruiop1234");
51
+ expect(results).toEqual([]);
52
+ });
53
+ });
54
+ if (!apiKey) {
55
+ test("Skipping integration tests", () => {
56
+ console.warn("TMDB_API_KEY not found in .env. Skipping integration tests.");
57
+ });
58
+ }
package/build/tmdb.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.searchTvShows = exports.searchMovies = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const searchMovies = async (apiKey, query) => {
9
+ if (!query) {
10
+ throw new Error("Query argument is required");
11
+ }
12
+ try {
13
+ const response = await axios_1.default.get("https://api.themoviedb.org/3/search/movie", {
14
+ params: {
15
+ api_key: apiKey,
16
+ query: query,
17
+ language: "en-US",
18
+ page: 1,
19
+ },
20
+ });
21
+ return response.data.results.slice(0, 5); // Limit to top 5 results
22
+ }
23
+ catch (error) {
24
+ if (axios_1.default.isAxiosError(error)) {
25
+ throw new Error(`TMDB API Error: ${error.message}`);
26
+ }
27
+ throw error;
28
+ }
29
+ };
30
+ exports.searchMovies = searchMovies;
31
+ const searchTvShows = async (apiKey, query) => {
32
+ if (!query) {
33
+ throw new Error("Query argument is required");
34
+ }
35
+ try {
36
+ const response = await axios_1.default.get("https://api.themoviedb.org/3/search/tv", {
37
+ params: {
38
+ api_key: apiKey,
39
+ query: query,
40
+ language: "en-US",
41
+ page: 1,
42
+ },
43
+ });
44
+ return response.data.results.slice(0, 5);
45
+ }
46
+ catch (error) {
47
+ if (axios_1.default.isAxiosError(error)) {
48
+ throw new Error(`TMDB API Error: ${error.message}`);
49
+ }
50
+ throw error;
51
+ }
52
+ };
53
+ exports.searchTvShows = searchTvShows;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const axios_1 = __importDefault(require("axios"));
7
+ const tmdb_1 = require("./tmdb");
8
+ jest.mock("axios");
9
+ const mockedAxios = axios_1.default;
10
+ describe("searchMovies", () => {
11
+ const apiKey = "test-api-key";
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+ it("should return movie results and call API with correct parameters", async () => {
16
+ const mockMovies = [
17
+ {
18
+ id: 1,
19
+ title: "Test Movie",
20
+ overview: "Test Overview",
21
+ release_date: "2023-01-01",
22
+ vote_average: 8.0,
23
+ },
24
+ ];
25
+ const mockResponse = {
26
+ data: {
27
+ results: mockMovies,
28
+ },
29
+ };
30
+ mockedAxios.get.mockResolvedValue(mockResponse);
31
+ const query = "Test Query";
32
+ const result = await (0, tmdb_1.searchMovies)(apiKey, query);
33
+ expect(result).toEqual(mockMovies);
34
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://api.themoviedb.org/3/search/movie", {
35
+ params: {
36
+ api_key: apiKey,
37
+ query: query,
38
+ language: "en-US",
39
+ page: 1,
40
+ },
41
+ });
42
+ });
43
+ it("should return an empty array when result list is empty", async () => {
44
+ mockedAxios.get.mockResolvedValue({
45
+ data: {
46
+ results: [],
47
+ },
48
+ });
49
+ const result = await (0, tmdb_1.searchMovies)(apiKey, "Nonexistent Movie");
50
+ expect(result).toEqual([]);
51
+ });
52
+ it("should throw an error if the query is empty", async () => {
53
+ await expect((0, tmdb_1.searchMovies)(apiKey, "")).rejects.toThrow("Query argument is required");
54
+ });
55
+ it("should handle API errors", async () => {
56
+ const errorMessage = "Network Error";
57
+ mockedAxios.get.mockRejectedValue({
58
+ isAxiosError: true,
59
+ message: errorMessage,
60
+ });
61
+ // We need to mock isAxiosError since we are using it in the catch block
62
+ axios_1.default.isAxiosError = jest.fn().mockReturnValue(true);
63
+ await expect((0, tmdb_1.searchMovies)(apiKey, "Error Movie")).rejects.toThrow(`TMDB API Error: ${errorMessage}`);
64
+ });
65
+ });
66
+ describe("searchTvShows", () => {
67
+ const apiKey = "test-api-key";
68
+ beforeEach(() => {
69
+ jest.clearAllMocks();
70
+ });
71
+ it("should return TV show results and call API with correct parameters", async () => {
72
+ const mockShows = [
73
+ {
74
+ id: 101,
75
+ name: "Test Show",
76
+ overview: "Test Overview",
77
+ first_air_date: "2023-01-01",
78
+ vote_average: 7.5,
79
+ },
80
+ ];
81
+ const mockResponse = {
82
+ data: {
83
+ results: mockShows,
84
+ },
85
+ };
86
+ mockedAxios.get.mockResolvedValue(mockResponse);
87
+ const query = "Test Show Query";
88
+ const result = await (0, tmdb_1.searchTvShows)(apiKey, query);
89
+ expect(result).toEqual(mockShows);
90
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://api.themoviedb.org/3/search/tv", {
91
+ params: {
92
+ api_key: apiKey,
93
+ query: query,
94
+ language: "en-US",
95
+ page: 1,
96
+ },
97
+ });
98
+ });
99
+ it("should return an empty array when result list is empty", async () => {
100
+ mockedAxios.get.mockResolvedValue({
101
+ data: {
102
+ results: [],
103
+ },
104
+ });
105
+ const result = await (0, tmdb_1.searchTvShows)(apiKey, "Nonexistent Show");
106
+ expect(result).toEqual([]);
107
+ });
108
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lonzzi/tmdb-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for searching movies on TMDB",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "tmdb-mcp": "build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node build/index.js",
12
+ "test": "jest",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "files": [
16
+ "build"
17
+ ],
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.25.1",
23
+ "axios": "^1.13.2",
24
+ "dotenv": "^16.6.1",
25
+ "zod": "^4.2.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/axios": "^0.14.0",
29
+ "@types/jest": "^30.0.0",
30
+ "@types/node": "^20.0.0",
31
+ "jest": "^30.2.0",
32
+ "ts-jest": "^29.4.6",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }