Skip to main content
Glama
overseerr_mcp_server.ts17.3 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; interface OverseerrConfig { baseUrl: string; apiKey: string; } interface MediaRequest { id: number; status: number; media: { id: number; mediaType: string; tmdbId: number; tvdbId?: number; status: number; createdAt: string; updatedAt: string; title?: string; name?: string; originalTitle?: string; }; requestedBy: { id: number; displayName: string; email: string; }; modifiedBy?: { id: number; displayName: string; email: string; }; createdAt: string; updatedAt: string; type: string; requestedSeason?: number; } interface SearchResult { id: number; mediaType: string; title: string; name?: string; originalTitle?: string; overview: string; posterPath?: string; backdropPath?: string; releaseDate?: string; firstAirDate?: string; genreIds: number[]; popularity: number; voteAverage: number; voteCount: number; adult?: boolean; video?: boolean; originalLanguage: string; mediaInfo?: { id: number; tmdbId: number; tvdbId?: number; status: number; requests?: MediaRequest[]; }; } interface SearchResponse { page: number; totalPages: number; totalResults: number; results: SearchResult[]; } interface RequestResponse { pageInfo: { pages: number; pageSize: number; results: number; page: number; }; results: MediaRequest[]; } interface RequestCreationResponse { id: number; status: number; } interface ApprovalResponse { status: number; } class OverseerrMCPServer { private config: OverseerrConfig; private server: Server; constructor() { // Konfiguration aus Umgebungsvariablen laden this.config = { baseUrl: process.env.OVERSEERR_BASE_URL || "http://localhost:5055", apiKey: process.env.OVERSEERR_API_KEY || "", }; if (!this.config.apiKey) { throw new Error("OVERSEERR_API_KEY environment variable is required"); } this.server = new Server( { name: "overseerr-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private async makeApiRequest(endpoint: string, options: any = {}) { const url = `${this.config.baseUrl}/api/v1${endpoint}`; const headers = { "X-Api-Key": this.config.apiKey, "Content-Type": "application/json", ...options.headers, }; try { const response = await fetch(url, { ...options, headers, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Prüfen ob die Antwort leer ist (z.B. bei DELETE-Operationen) const text = await response.text(); if (!text) { return {}; // Leeres Objekt zurückgeben bei leerer Antwort } return JSON.parse(text); } catch (error) { throw new McpError( ErrorCode.InternalError, `Overseerr API request failed: ${error instanceof Error ? error.message : String(error)}` ); } } private setupToolHandlers() { // Tool-Liste bereitstellen this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "search_media", description: "Search for movies and TV shows in Overseerr", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for movies or TV shows", }, page: { type: "number", description: "Page number for pagination (default: 1)", default: 1, }, }, required: ["query"], }, }, { name: "get_requests", description: "Get all media requests from Overseerr", inputSchema: { type: "object", properties: { take: { type: "number", description: "Number of requests to return (default: 20, max: 50)", default: 20, }, skip: { type: "number", description: "Number of requests to skip for pagination (default: 0)", default: 0, }, filter: { type: "string", description: "Filter requests by status", enum: ["all", "approved", "denied", "pending", "processing", "available"], default: "all", }, sort: { type: "string", description: "Sort order", enum: ["added", "modified"], default: "added", }, }, }, }, { name: "request_media", description: "Request a movie or TV show", inputSchema: { type: "object", properties: { mediaId: { type: "number", description: "TMDB ID of the media to request", }, mediaType: { type: "string", description: "Type of media", enum: ["movie", "tv"], }, seasons: { type: "array", description: "Seasons to request (for TV shows only)", items: { type: "number", }, }, is4k: { type: "boolean", description: "Request 4K version", default: false, }, }, required: ["mediaId", "mediaType"], }, }, { name: "approve_request", description: "Approve a pending media request", inputSchema: { type: "object", properties: { requestId: { type: "number", description: "ID of the request to approve", }, }, required: ["requestId"], }, }, { name: "deny_request", description: "Deny a pending media request", inputSchema: { type: "object", properties: { requestId: { type: "number", description: "ID of the request to deny", }, reason: { type: "string", description: "Reason for denial", }, }, required: ["requestId"], }, }, { name: "delete_request", description: "Delete a media request", inputSchema: { type: "object", properties: { requestId: { type: "number", description: "ID of the request to delete", }, }, required: ["requestId"], }, }, { name: "get_media_details", description: "Get detailed information about a specific media item", inputSchema: { type: "object", properties: { mediaType: { type: "string", description: "Type of media", enum: ["movie", "tv"], }, mediaId: { type: "number", description: "TMDB ID of the media", }, }, required: ["mediaType", "mediaId"], }, }, { name: "get_server_status", description: "Get Overseerr server status and information", inputSchema: { type: "object", properties: {}, }, }, { name: "get_user_requests", description: "Get requests for a specific user", inputSchema: { type: "object", properties: { userId: { type: "number", description: "User ID to get requests for", }, take: { type: "number", description: "Number of requests to return (default: 20)", default: 20, }, skip: { type: "number", description: "Number of requests to skip (default: 0)", default: 0, }, }, required: ["userId"], }, }, ], })); // Tool-Aufrufe behandeln this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "search_media": return await this.searchMedia(args as { query: string; page?: number }); case "get_requests": return await this.getRequests(args as { take?: number; skip?: number; filter?: string; sort?: string; }); case "request_media": return await this.requestMedia(args as { mediaId: number; mediaType: string; seasons?: number[]; is4k?: boolean; }); case "approve_request": return await this.approveRequest(args as { requestId: number }); case "deny_request": return await this.denyRequest(args as { requestId: number; reason?: string }); case "delete_request": return await this.deleteRequest(args as { requestId: number }); case "get_media_details": return await this.getMediaDetails(args as { mediaType: string; mediaId: number; }); case "get_server_status": return await this.getServerStatus(); case "get_user_requests": return await this.getUserRequests(args as { userId: number; take?: number; skip?: number; }); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async searchMedia(args: { query: string; page?: number }) { const { query, page = 1 } = args; const data = await this.makeApiRequest(`/search?query=${encodeURIComponent(query)}&page=${page}`) as SearchResponse; return { content: [ { type: "text", text: JSON.stringify({ query, page, totalPages: data.totalPages, totalResults: data.totalResults, results: data.results.map((item: SearchResult) => ({ id: item.id, mediaType: item.mediaType, title: item.title || item.name, overview: item.overview, releaseDate: item.releaseDate || item.firstAirDate, voteAverage: item.voteAverage, posterPath: item.posterPath, status: item.mediaInfo?.status, alreadyRequested: item.mediaInfo?.requests && item.mediaInfo.requests.length > 0, })), }, null, 2), }, ], }; } private async getRequests(args: { take?: number; skip?: number; filter?: string; sort?: string; }) { const { take = 20, skip = 0, filter = "all", sort = "added" } = args; const endpoint = `/request?take=${take}&skip=${skip}&filter=${filter}&sort=${sort}`; const data = await this.makeApiRequest(endpoint) as RequestResponse; return { content: [ { type: "text", text: JSON.stringify({ pageInfo: data.pageInfo, results: data.results.map((request: MediaRequest) => ({ id: request.id, status: this.getStatusText(request.status), mediaType: request.media.mediaType, mediaTitle: request.media.title || request.media.name || 'Unbekannter Titel', requestType: request.type, requestedBy: request.requestedBy.displayName, createdAt: request.createdAt, updatedAt: request.updatedAt, tmdbId: request.media.tmdbId, })), }, null, 2), }, ], }; } private async requestMedia(args: { mediaId: number; mediaType: string; seasons?: number[]; is4k?: boolean; }) { const { mediaId, mediaType, seasons, is4k = false } = args; const requestData: any = { mediaId, mediaType, is4k, }; if (mediaType === "tv" && seasons) { requestData.seasons = seasons; } const data = await this.makeApiRequest("/request", { method: "POST", body: JSON.stringify(requestData), }) as RequestCreationResponse; return { content: [ { type: "text", text: JSON.stringify({ success: true, requestId: data.id, status: this.getStatusText(data.status), message: `Successfully requested ${mediaType} with ID ${mediaId}`, }, null, 2), }, ], }; } private async approveRequest(args: { requestId: number }) { const { requestId } = args; const data = await this.makeApiRequest(`/request/${requestId}/approve`, { method: "POST", }) as ApprovalResponse; return { content: [ { type: "text", text: JSON.stringify({ success: true, requestId, status: this.getStatusText(data.status), message: `Request ${requestId} has been approved`, }, null, 2), }, ], }; } private async denyRequest(args: { requestId: number; reason?: string }) { const { requestId, reason } = args; const requestData: any = {}; if (reason) { requestData.reason = reason; } const data = await this.makeApiRequest(`/request/${requestId}/decline`, { method: "POST", body: JSON.stringify(requestData), }) as ApprovalResponse; return { content: [ { type: "text", text: JSON.stringify({ success: true, requestId, status: this.getStatusText(data.status), message: `Request ${requestId} has been denied${reason ? ` with reason: ${reason}` : ""}`, }, null, 2), }, ], }; } private async deleteRequest(args: { requestId: number }) { const { requestId } = args; await this.makeApiRequest(`/request/${requestId}`, { method: "DELETE", }); return { content: [{ type: "text", text: JSON.stringify({ success: true, requestId, message: `Request ${requestId} wurde gelöscht` }, null, 2) }] }; } private async getMediaDetails(args: { mediaType: string; mediaId: number }) { const { mediaType, mediaId } = args; const data = await this.makeApiRequest(`/${mediaType}/${mediaId}`); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } private async getServerStatus() { const data = await this.makeApiRequest("/status"); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } private async getUserRequests(args: { userId: number; take?: number; skip?: number; }) { const { userId, take = 20, skip = 0 } = args; const endpoint = `/user/${userId}/requests?take=${take}&skip=${skip}`; const data = await this.makeApiRequest(endpoint); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } private getStatusText(status: number): string { const statusMap: { [key: number]: string } = { 1: "pending", 2: "approved", 3: "declined", 4: "processing", 5: "available", }; return statusMap[status] || "unknown"; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Overseerr MCP server running on stdio"); } } // Server starten const server = new OverseerrMCPServer(); server.run().catch((error) => { console.error("Failed to run server:", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/edemal/mcp-server-overseerr'

If you have feedback or need assistance with the MCP directory API, please join our Discord server