mcp-rtfm
by ryanjoachim
- src
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fetch from 'node-fetch';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Type definitions
interface Movie {
id: number;
title: string;
release_date: string;
vote_average: number;
overview: string;
poster_path?: string;
genres?: Array<{ id: number; name: string }>;
}
interface TMDBResponse {
page: number;
results: Movie[];
total_pages: number;
}
interface MovieDetails extends Movie {
credits?: {
cast: Array<{
name: string;
character: string;
}>;
crew: Array<{
name: string;
job: string;
}>;
};
reviews?: {
results: Array<{
author: string;
content: string;
rating?: number;
}>;
};
}
const TMDB_API_KEY = process.env.TMDB_API_KEY;
const TMDB_BASE_URL = "https://api.themoviedb.org/3";
const server = new Server(
{
name: "example-servers/tmdb",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
async function fetchFromTMDB<T>(endpoint: string, params: Record<string, string> = {}): Promise<T> {
const url = new URL(`${TMDB_BASE_URL}${endpoint}`);
url.searchParams.append("api_key", TMDB_API_KEY!);
for (const [key, value] of Object.entries(params)) {
url.searchParams.append(key, value);
}
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`TMDB API error: ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async function getMovieDetails(movieId: string): Promise<MovieDetails> {
return fetchFromTMDB<MovieDetails>(`/movie/${movieId}`, { append_to_response: "credits,reviews" });
}
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
const params: Record<string, string> = {
page: request.params?.cursor || "1",
};
const data = await fetchFromTMDB<TMDBResponse>("/movie/popular", params);
return {
resources: data.results.map((movie) => ({
uri: `tmdb:///movie/${movie.id}`,
mimeType: "application/json",
name: `${movie.title} (${movie.release_date.split("-")[0]})`,
})),
nextCursor: data.page < data.total_pages ? String(data.page + 1) : undefined,
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const movieId = request.params.uri.replace("tmdb:///movie/", "");
const movie = await getMovieDetails(movieId);
const movieInfo = {
title: movie.title,
releaseDate: movie.release_date,
rating: movie.vote_average,
overview: movie.overview,
genres: movie.genres?.map(g => g.name).join(", "),
posterUrl: movie.poster_path ?
`https://image.tmdb.org/t/p/w500${movie.poster_path}` :
"No poster available",
cast: movie.credits?.cast?.slice(0, 5).map(actor => `${actor.name} as ${actor.character}`),
director: movie.credits?.crew?.find(person => person.job === "Director")?.name,
reviews: movie.reviews?.results?.slice(0, 3).map(review => ({
author: review.author,
content: review.content,
rating: review.rating
}))
};
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(movieInfo, null, 2),
},
],
};
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_movies",
description: "Search for movies by title or keywords",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for movie titles",
},
},
required: ["query"],
},
},
{
name: "get_recommendations",
description: "Get movie recommendations based on a movie ID",
inputSchema: {
type: "object",
properties: {
movieId: {
type: "string",
description: "TMDB movie ID to get recommendations for",
},
},
required: ["movieId"],
},
},
{
name: "get_trending",
description: "Get trending movies for a time window",
inputSchema: {
type: "object",
properties: {
timeWindow: {
type: "string",
enum: ["day", "week"],
description: "Time window for trending movies",
},
},
required: ["timeWindow"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case "search_movies": {
const query = request.params.arguments?.query as string;
const data = await fetchFromTMDB<TMDBResponse>("/search/movie", { query });
const results = data.results
.map((movie) =>
`${movie.title} (${movie.release_date?.split("-")[0]}) - ID: ${movie.id}\n` +
`Rating: ${movie.vote_average}/10\n` +
`Overview: ${movie.overview}\n`
)
.join("\n---\n");
return {
content: [
{
type: "text",
text: `Found ${data.results.length} movies:\n\n${results}`,
},
],
isError: false,
};
}
case "get_recommendations": {
const movieId = request.params.arguments?.movieId as string;
const data = await fetchFromTMDB<TMDBResponse>(`/movie/${movieId}/recommendations`);
const recommendations = data.results
.slice(0, 5)
.map((movie) =>
`${movie.title} (${movie.release_date?.split("-")[0]})\n` +
`Rating: ${movie.vote_average}/10\n` +
`Overview: ${movie.overview}\n`
)
.join("\n---\n");
return {
content: [
{
type: "text",
text: `Top 5 recommendations:\n\n${recommendations}`,
},
],
isError: false,
};
}
case "get_trending": {
const timeWindow = request.params.arguments?.timeWindow as string;
const data = await fetchFromTMDB<TMDBResponse>(`/trending/movie/${timeWindow}`);
const trending = data.results
.slice(0, 10)
.map((movie) =>
`${movie.title} (${movie.release_date?.split("-")[0]})\n` +
`Rating: ${movie.vote_average}/10\n` +
`Overview: ${movie.overview}\n`
)
.join("\n---\n");
return {
content: [
{
type: "text",
text: `Trending movies for the ${timeWindow}:\n\n${trending}`,
},
],
isError: false,
};
}
default:
throw new Error("Tool not found");
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
},
],
isError: true,
};
}
});
// Start the server
if (!TMDB_API_KEY) {
console.error("TMDB_API_KEY environment variable is required");
process.exit(1);
}
const transport = new StdioServerTransport();
server.connect(transport).catch((error) => {
console.error("Server connection error:", error);
process.exit(1);
});