Skip to main content
Glama
index.ts50.7 kB
#!/usr/bin/env node /** * MCP Server for *arr Media Management Suite * * Provides tools for managing Sonarr (TV), Radarr (Movies), Lidarr (Music), * Readarr (Books), and Prowlarr (Indexers) through Claude Code. * * Environment variables: * - SONARR_URL, SONARR_API_KEY * - RADARR_URL, RADARR_API_KEY * - LIDARR_URL, LIDARR_API_KEY * - READARR_URL, READARR_API_KEY * - PROWLARR_URL, PROWLARR_API_KEY */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { SonarrClient, RadarrClient, LidarrClient, ReadarrClient, ProwlarrClient, ArrService, } from "./arr-client.js"; // Configuration from environment interface ServiceConfig { name: ArrService; displayName: string; url?: string; apiKey?: string; } const services: ServiceConfig[] = [ { name: 'sonarr', displayName: 'Sonarr (TV)', url: process.env.SONARR_URL, apiKey: process.env.SONARR_API_KEY }, { name: 'radarr', displayName: 'Radarr (Movies)', url: process.env.RADARR_URL, apiKey: process.env.RADARR_API_KEY }, { name: 'lidarr', displayName: 'Lidarr (Music)', url: process.env.LIDARR_URL, apiKey: process.env.LIDARR_API_KEY }, { name: 'readarr', displayName: 'Readarr (Books)', url: process.env.READARR_URL, apiKey: process.env.READARR_API_KEY }, { name: 'prowlarr', displayName: 'Prowlarr (Indexers)', url: process.env.PROWLARR_URL, apiKey: process.env.PROWLARR_API_KEY }, ]; // Check which services are configured const configuredServices = services.filter(s => s.url && s.apiKey); if (configuredServices.length === 0) { console.error("Error: No *arr services configured. Set at least one pair of URL and API_KEY environment variables."); console.error("Example: SONARR_URL and SONARR_API_KEY"); process.exit(1); } // Initialize clients for configured services const clients: { sonarr?: SonarrClient; radarr?: RadarrClient; lidarr?: LidarrClient; readarr?: ReadarrClient; prowlarr?: ProwlarrClient; } = {}; for (const service of configuredServices) { const config = { url: service.url!, apiKey: service.apiKey! }; switch (service.name) { case 'sonarr': clients.sonarr = new SonarrClient(config); break; case 'radarr': clients.radarr = new RadarrClient(config); break; case 'lidarr': clients.lidarr = new LidarrClient(config); break; case 'readarr': clients.readarr = new ReadarrClient(config); break; case 'prowlarr': clients.prowlarr = new ProwlarrClient(config); break; } } // Build tools based on configured services const TOOLS: Tool[] = [ // General tool available for all { name: "arr_status", description: `Get status of all configured *arr services. Currently configured: ${configuredServices.map(s => s.displayName).join(', ')}`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, ]; // Configuration review tools for each service // These are added dynamically based on configured services // Helper function to create config tools for a service function addConfigTools(serviceName: string, displayName: string) { TOOLS.push( { name: `${serviceName}_get_quality_profiles`, description: `Get detailed quality profiles from ${displayName}. Shows allowed qualities, upgrade settings, and custom format scores.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_get_health`, description: `Get health check warnings and issues from ${displayName}. Shows any problems detected by the application.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_get_root_folders`, description: `Get root folders and storage info from ${displayName}. Shows paths, free space, and unmapped folders.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_get_download_clients`, description: `Get download client configurations from ${displayName}. Shows configured clients and their settings.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_get_naming`, description: `Get file naming configuration from ${displayName}. Shows naming patterns for files and folders.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_get_tags`, description: `Get all tags defined in ${displayName}. Tags can be used to organize and filter content.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: `${serviceName}_review_setup`, description: `Get comprehensive configuration review for ${displayName}. Returns all settings for analysis: quality profiles, download clients, naming, storage, indexers, health warnings, and more. Use this to analyze the setup and suggest improvements.`, inputSchema: { type: "object" as const, properties: {}, required: [], }, } ); } // Add config tools for each configured service (except Prowlarr which has different config) if (clients.sonarr) addConfigTools('sonarr', 'Sonarr (TV)'); if (clients.radarr) addConfigTools('radarr', 'Radarr (Movies)'); if (clients.lidarr) addConfigTools('lidarr', 'Lidarr (Music)'); if (clients.readarr) addConfigTools('readarr', 'Readarr (Books)'); // Sonarr tools if (clients.sonarr) { TOOLS.push( { name: "sonarr_get_series", description: "Get all TV series in Sonarr library", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "sonarr_search", description: "Search for TV series to add to Sonarr", inputSchema: { type: "object" as const, properties: { term: { type: "string", description: "Search term (show name)", }, }, required: ["term"], }, }, { name: "sonarr_get_queue", description: "Get Sonarr download queue", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "sonarr_get_calendar", description: "Get upcoming TV episodes from Sonarr", inputSchema: { type: "object" as const, properties: { days: { type: "number", description: "Number of days to look ahead (default: 7)", }, }, required: [], }, }, { name: "sonarr_get_episodes", description: "Get episodes for a TV series. Shows which episodes are available and which are missing.", inputSchema: { type: "object" as const, properties: { seriesId: { type: "number", description: "Series ID to get episodes for", }, seasonNumber: { type: "number", description: "Optional: filter to a specific season", }, }, required: ["seriesId"], }, }, { name: "sonarr_search_missing", description: "Trigger a search for all missing episodes in a series", inputSchema: { type: "object" as const, properties: { seriesId: { type: "number", description: "Series ID to search for missing episodes", }, }, required: ["seriesId"], }, }, { name: "sonarr_search_episode", description: "Trigger a search for specific episode(s)", inputSchema: { type: "object" as const, properties: { episodeIds: { type: "array", items: { type: "number" }, description: "Episode ID(s) to search for", }, }, required: ["episodeIds"], }, } ); } // Radarr tools if (clients.radarr) { TOOLS.push( { name: "radarr_get_movies", description: "Get all movies in Radarr library", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "radarr_search", description: "Search for movies to add to Radarr", inputSchema: { type: "object" as const, properties: { term: { type: "string", description: "Search term (movie name)", }, }, required: ["term"], }, }, { name: "radarr_get_queue", description: "Get Radarr download queue", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "radarr_get_calendar", description: "Get upcoming movie releases from Radarr", inputSchema: { type: "object" as const, properties: { days: { type: "number", description: "Number of days to look ahead (default: 30)", }, }, required: [], }, }, { name: "radarr_search_movie", description: "Trigger a search to download a movie that's already in your library", inputSchema: { type: "object" as const, properties: { movieId: { type: "number", description: "Movie ID to search for", }, }, required: ["movieId"], }, } ); } // Lidarr tools if (clients.lidarr) { TOOLS.push( { name: "lidarr_get_artists", description: "Get all artists in Lidarr library", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "lidarr_search", description: "Search for artists to add to Lidarr", inputSchema: { type: "object" as const, properties: { term: { type: "string", description: "Search term (artist name)", }, }, required: ["term"], }, }, { name: "lidarr_get_queue", description: "Get Lidarr download queue", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "lidarr_get_albums", description: "Get albums for an artist in Lidarr. Shows which albums are available and which are missing.", inputSchema: { type: "object" as const, properties: { artistId: { type: "number", description: "Artist ID to get albums for", }, }, required: ["artistId"], }, }, { name: "lidarr_search_album", description: "Trigger a search for a specific album to download", inputSchema: { type: "object" as const, properties: { albumId: { type: "number", description: "Album ID to search for", }, }, required: ["albumId"], }, }, { name: "lidarr_search_missing", description: "Trigger a search for all missing albums for an artist", inputSchema: { type: "object" as const, properties: { artistId: { type: "number", description: "Artist ID to search missing albums for", }, }, required: ["artistId"], }, }, { name: "lidarr_get_calendar", description: "Get upcoming album releases from Lidarr", inputSchema: { type: "object" as const, properties: { days: { type: "number", description: "Number of days to look ahead (default: 30)", }, }, required: [], }, } ); } // Readarr tools if (clients.readarr) { TOOLS.push( { name: "readarr_get_authors", description: "Get all authors in Readarr library", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "readarr_search", description: "Search for authors to add to Readarr", inputSchema: { type: "object" as const, properties: { term: { type: "string", description: "Search term (author name)", }, }, required: ["term"], }, }, { name: "readarr_get_queue", description: "Get Readarr download queue", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "readarr_get_books", description: "Get books for an author in Readarr. Shows which books are available and which are missing.", inputSchema: { type: "object" as const, properties: { authorId: { type: "number", description: "Author ID to get books for", }, }, required: ["authorId"], }, }, { name: "readarr_search_book", description: "Trigger a search for a specific book to download", inputSchema: { type: "object" as const, properties: { bookIds: { type: "array", items: { type: "number" }, description: "Book ID(s) to search for", }, }, required: ["bookIds"], }, }, { name: "readarr_search_missing", description: "Trigger a search for all missing books for an author", inputSchema: { type: "object" as const, properties: { authorId: { type: "number", description: "Author ID to search missing books for", }, }, required: ["authorId"], }, }, { name: "readarr_get_calendar", description: "Get upcoming book releases from Readarr", inputSchema: { type: "object" as const, properties: { days: { type: "number", description: "Number of days to look ahead (default: 30)", }, }, required: [], }, } ); } // Prowlarr tools if (clients.prowlarr) { TOOLS.push( { name: "prowlarr_get_indexers", description: "Get all configured indexers in Prowlarr", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "prowlarr_search", description: "Search across all Prowlarr indexers", inputSchema: { type: "object" as const, properties: { query: { type: "string", description: "Search query", }, }, required: ["query"], }, }, { name: "prowlarr_test_indexers", description: "Test all indexers and return their health status", inputSchema: { type: "object" as const, properties: {}, required: [], }, }, { name: "prowlarr_get_stats", description: "Get indexer statistics (queries, grabs, failures)", inputSchema: { type: "object" as const, properties: {}, required: [], }, } ); } // Cross-service search tool TOOLS.push({ name: "arr_search_all", description: "Search across all configured *arr services for any media", inputSchema: { type: "object" as const, properties: { term: { type: "string", description: "Search term", }, }, required: ["term"], }, }); // Create server instance const server = new Server( { name: "mcp-arr", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "arr_status": { const statuses: Record<string, unknown> = {}; for (const service of configuredServices) { try { const client = clients[service.name]; if (client) { const status = await client.getStatus(); statuses[service.name] = { configured: true, connected: true, version: status.version, appName: status.appName, }; } } catch (error) { statuses[service.name] = { configured: true, connected: false, error: error instanceof Error ? error.message : String(error), }; } } // Add unconfigured services for (const service of services) { if (!statuses[service.name]) { statuses[service.name] = { configured: false }; } } return { content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }], }; } // Dynamic config tool handlers // Quality Profiles case "sonarr_get_quality_profiles": case "radarr_get_quality_profiles": case "lidarr_get_quality_profiles": case "readarr_get_quality_profiles": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const profiles = await client.getQualityProfiles(); return { content: [{ type: "text", text: JSON.stringify({ count: profiles.length, profiles: profiles.map(p => ({ id: p.id, name: p.name, upgradeAllowed: p.upgradeAllowed, cutoff: p.cutoff, allowedQualities: p.items .filter(i => i.allowed) .map(i => i.quality?.name || i.name || (i.items?.map(q => q.quality.name).join(', '))) .filter(Boolean), customFormats: p.formatItems?.filter(f => f.score !== 0).map(f => ({ name: f.name, score: f.score, })) || [], minFormatScore: p.minFormatScore, cutoffFormatScore: p.cutoffFormatScore, })), }, null, 2), }], }; } // Health checks case "sonarr_get_health": case "radarr_get_health": case "lidarr_get_health": case "readarr_get_health": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const health = await client.getHealth(); return { content: [{ type: "text", text: JSON.stringify({ issueCount: health.length, issues: health.map(h => ({ source: h.source, type: h.type, message: h.message, wikiUrl: h.wikiUrl, })), status: health.length === 0 ? 'healthy' : 'issues detected', }, null, 2), }], }; } // Root folders case "sonarr_get_root_folders": case "radarr_get_root_folders": case "lidarr_get_root_folders": case "readarr_get_root_folders": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const folders = await client.getRootFoldersDetailed(); return { content: [{ type: "text", text: JSON.stringify({ count: folders.length, folders: folders.map(f => ({ id: f.id, path: f.path, accessible: f.accessible, freeSpace: formatBytes(f.freeSpace), freeSpaceBytes: f.freeSpace, unmappedFolders: f.unmappedFolders?.length || 0, })), }, null, 2), }], }; } // Download clients case "sonarr_get_download_clients": case "radarr_get_download_clients": case "lidarr_get_download_clients": case "readarr_get_download_clients": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const downloadClients = await client.getDownloadClients(); return { content: [{ type: "text", text: JSON.stringify({ count: downloadClients.length, clients: downloadClients.map(c => ({ id: c.id, name: c.name, implementation: c.implementationName, protocol: c.protocol, enabled: c.enable, priority: c.priority, removeCompletedDownloads: c.removeCompletedDownloads, removeFailedDownloads: c.removeFailedDownloads, tags: c.tags, })), }, null, 2), }], }; } // Naming config case "sonarr_get_naming": case "radarr_get_naming": case "lidarr_get_naming": case "readarr_get_naming": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const naming = await client.getNamingConfig(); return { content: [{ type: "text", text: JSON.stringify(naming, null, 2), }], }; } // Tags case "sonarr_get_tags": case "radarr_get_tags": case "lidarr_get_tags": case "readarr_get_tags": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); const tags = await client.getTags(); return { content: [{ type: "text", text: JSON.stringify({ count: tags.length, tags: tags.map(t => ({ id: t.id, label: t.label })), }, null, 2), }], }; } // Comprehensive setup review case "sonarr_review_setup": case "radarr_review_setup": case "lidarr_review_setup": case "readarr_review_setup": { const serviceName = name.split('_')[0] as keyof typeof clients; const client = clients[serviceName]; if (!client) throw new Error(`${serviceName} not configured`); // Gather all configuration data const [status, health, qualityProfiles, qualityDefinitions, downloadClients, naming, mediaManagement, rootFolders, tags, indexers] = await Promise.all([ client.getStatus(), client.getHealth(), client.getQualityProfiles(), client.getQualityDefinitions(), client.getDownloadClients(), client.getNamingConfig(), client.getMediaManagement(), client.getRootFoldersDetailed(), client.getTags(), client.getIndexers(), ]); // For Lidarr/Readarr, also get metadata profiles let metadataProfiles = null; if (serviceName === 'lidarr' && clients.lidarr) { metadataProfiles = await clients.lidarr.getMetadataProfiles(); } else if (serviceName === 'readarr' && clients.readarr) { metadataProfiles = await clients.readarr.getMetadataProfiles(); } const review = { service: serviceName, version: status.version, appName: status.appName, platform: { os: status.osName, isDocker: status.isDocker, }, health: { issueCount: health.length, issues: health, }, storage: { rootFolders: rootFolders.map(f => ({ path: f.path, accessible: f.accessible, freeSpace: formatBytes(f.freeSpace), freeSpaceBytes: f.freeSpace, unmappedFolderCount: f.unmappedFolders?.length || 0, })), }, qualityProfiles: qualityProfiles.map(p => ({ id: p.id, name: p.name, upgradeAllowed: p.upgradeAllowed, cutoff: p.cutoff, allowedQualities: p.items .filter(i => i.allowed) .map(i => i.quality?.name || i.name || (i.items?.map(q => q.quality.name).join(', '))) .filter(Boolean), customFormatsWithScores: p.formatItems?.filter(f => f.score !== 0).length || 0, minFormatScore: p.minFormatScore, })), qualityDefinitions: qualityDefinitions.map(d => ({ quality: d.quality.name, minSize: d.minSize + ' MB/min', maxSize: d.maxSize === 0 ? 'unlimited' : d.maxSize + ' MB/min', preferredSize: d.preferredSize + ' MB/min', })), downloadClients: downloadClients.map(c => ({ name: c.name, type: c.implementationName, protocol: c.protocol, enabled: c.enable, priority: c.priority, })), indexers: indexers.map(i => ({ name: i.name, protocol: i.protocol, enableRss: i.enableRss, enableAutomaticSearch: i.enableAutomaticSearch, enableInteractiveSearch: i.enableInteractiveSearch, priority: i.priority, })), naming: naming, mediaManagement: { recycleBin: mediaManagement.recycleBin || 'not set', recycleBinCleanupDays: mediaManagement.recycleBinCleanupDays, downloadPropersAndRepacks: mediaManagement.downloadPropersAndRepacks, deleteEmptyFolders: mediaManagement.deleteEmptyFolders, copyUsingHardlinks: mediaManagement.copyUsingHardlinks, importExtraFiles: mediaManagement.importExtraFiles, extraFileExtensions: mediaManagement.extraFileExtensions, }, tags: tags.map(t => t.label), ...(metadataProfiles && { metadataProfiles }), }; return { content: [{ type: "text", text: JSON.stringify(review, null, 2), }], }; } // Sonarr handlers case "sonarr_get_series": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const series = await clients.sonarr.getSeries(); return { content: [{ type: "text", text: JSON.stringify({ count: series.length, series: series.map(s => ({ id: s.id, title: s.title, year: s.year, status: s.status, network: s.network, seasons: s.statistics?.seasonCount, episodes: s.statistics?.episodeFileCount + '/' + s.statistics?.totalEpisodeCount, sizeOnDisk: formatBytes(s.statistics?.sizeOnDisk || 0), monitored: s.monitored, })), }, null, 2), }], }; } case "sonarr_search": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const term = (args as { term: string }).term; const results = await clients.sonarr.searchSeries(term); return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results: results.slice(0, 10).map(r => ({ title: r.title, year: r.year, tvdbId: r.tvdbId, overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''), })), }, null, 2), }], }; } case "sonarr_get_queue": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const queue = await clients.sonarr.getQueue(); return { content: [{ type: "text", text: JSON.stringify({ totalRecords: queue.totalRecords, items: queue.records.map(q => ({ title: q.title, status: q.status, progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%', timeLeft: q.timeleft, downloadClient: q.downloadClient, })), }, null, 2), }], }; } case "sonarr_get_calendar": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const days = (args as { days?: number })?.days || 7; const start = new Date().toISOString().split('T')[0]; const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const calendar = await clients.sonarr.getCalendar(start, end); return { content: [{ type: "text", text: JSON.stringify(calendar, null, 2) }], }; } case "sonarr_get_episodes": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const { seriesId, seasonNumber } = args as { seriesId: number; seasonNumber?: number }; const episodes = await clients.sonarr.getEpisodes(seriesId, seasonNumber); return { content: [{ type: "text", text: JSON.stringify({ count: episodes.length, episodes: episodes.map(e => ({ id: e.id, seasonNumber: e.seasonNumber, episodeNumber: e.episodeNumber, title: e.title, airDate: e.airDate, hasFile: e.hasFile, monitored: e.monitored, })), }, null, 2), }], }; } case "sonarr_search_missing": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const seriesId = (args as { seriesId: number }).seriesId; const result = await clients.sonarr.searchMissing(seriesId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for missing episodes`, commandId: result.id, }, null, 2), }], }; } case "sonarr_search_episode": { if (!clients.sonarr) throw new Error("Sonarr not configured"); const episodeIds = (args as { episodeIds: number[] }).episodeIds; const result = await clients.sonarr.searchEpisode(episodeIds); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for ${episodeIds.length} episode(s)`, commandId: result.id, }, null, 2), }], }; } // Radarr handlers case "radarr_get_movies": { if (!clients.radarr) throw new Error("Radarr not configured"); const movies = await clients.radarr.getMovies(); return { content: [{ type: "text", text: JSON.stringify({ count: movies.length, movies: movies.map(m => ({ id: m.id, title: m.title, year: m.year, status: m.status, hasFile: m.hasFile, sizeOnDisk: formatBytes(m.sizeOnDisk), monitored: m.monitored, studio: m.studio, })), }, null, 2), }], }; } case "radarr_search": { if (!clients.radarr) throw new Error("Radarr not configured"); const term = (args as { term: string }).term; const results = await clients.radarr.searchMovies(term); return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results: results.slice(0, 10).map(r => ({ title: r.title, year: r.year, tmdbId: r.tmdbId, imdbId: r.imdbId, overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''), })), }, null, 2), }], }; } case "radarr_get_queue": { if (!clients.radarr) throw new Error("Radarr not configured"); const queue = await clients.radarr.getQueue(); return { content: [{ type: "text", text: JSON.stringify({ totalRecords: queue.totalRecords, items: queue.records.map(q => ({ title: q.title, status: q.status, progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%', timeLeft: q.timeleft, downloadClient: q.downloadClient, })), }, null, 2), }], }; } case "radarr_get_calendar": { if (!clients.radarr) throw new Error("Radarr not configured"); const days = (args as { days?: number })?.days || 30; const start = new Date().toISOString().split('T')[0]; const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const calendar = await clients.radarr.getCalendar(start, end); return { content: [{ type: "text", text: JSON.stringify(calendar, null, 2) }], }; } case "radarr_search_movie": { if (!clients.radarr) throw new Error("Radarr not configured"); const movieId = (args as { movieId: number }).movieId; const result = await clients.radarr.searchMovie(movieId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for movie`, commandId: result.id, }, null, 2), }], }; } // Lidarr handlers case "lidarr_get_artists": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const artists = await clients.lidarr.getArtists(); return { content: [{ type: "text", text: JSON.stringify({ count: artists.length, artists: artists.map(a => ({ id: a.id, artistName: a.artistName, status: a.status, albums: a.statistics?.albumCount, tracks: a.statistics?.trackFileCount + '/' + a.statistics?.totalTrackCount, sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0), monitored: a.monitored, })), }, null, 2), }], }; } case "lidarr_search": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const term = (args as { term: string }).term; const results = await clients.lidarr.searchArtists(term); return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results: results.slice(0, 10).map(r => ({ title: r.title, foreignArtistId: r.foreignArtistId, overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''), })), }, null, 2), }], }; } case "lidarr_get_queue": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const queue = await clients.lidarr.getQueue(); return { content: [{ type: "text", text: JSON.stringify({ totalRecords: queue.totalRecords, items: queue.records.map(q => ({ title: q.title, status: q.status, progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%', timeLeft: q.timeleft, downloadClient: q.downloadClient, })), }, null, 2), }], }; } case "lidarr_get_albums": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const artistId = (args as { artistId: number }).artistId; const albums = await clients.lidarr.getAlbums(artistId); return { content: [{ type: "text", text: JSON.stringify({ count: albums.length, albums: albums.map(a => ({ id: a.id, title: a.title, releaseDate: a.releaseDate, albumType: a.albumType, monitored: a.monitored, tracks: a.statistics ? `${a.statistics.trackFileCount}/${a.statistics.totalTrackCount}` : 'unknown', sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0), percentComplete: a.statistics?.percentOfTracks || 0, grabbed: a.grabbed, })), }, null, 2), }], }; } case "lidarr_search_album": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const albumId = (args as { albumId: number }).albumId; const result = await clients.lidarr.searchAlbum(albumId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for album`, commandId: result.id, }, null, 2), }], }; } case "lidarr_search_missing": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const artistId = (args as { artistId: number }).artistId; const result = await clients.lidarr.searchMissingAlbums(artistId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for missing albums`, commandId: result.id, }, null, 2), }], }; } case "lidarr_get_calendar": { if (!clients.lidarr) throw new Error("Lidarr not configured"); const days = (args as { days?: number })?.days || 30; const start = new Date().toISOString().split('T')[0]; const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const calendar = await clients.lidarr.getCalendar(start, end); return { content: [{ type: "text", text: JSON.stringify({ count: calendar.length, albums: calendar.map(a => ({ id: a.id, title: a.title, artistId: a.artistId, releaseDate: a.releaseDate, albumType: a.albumType, monitored: a.monitored, })), }, null, 2), }], }; } // Readarr handlers case "readarr_get_authors": { if (!clients.readarr) throw new Error("Readarr not configured"); const authors = await clients.readarr.getAuthors(); return { content: [{ type: "text", text: JSON.stringify({ count: authors.length, authors: authors.map(a => ({ id: a.id, authorName: a.authorName, status: a.status, books: a.statistics?.bookFileCount + '/' + a.statistics?.totalBookCount, sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0), monitored: a.monitored, })), }, null, 2), }], }; } case "readarr_search": { if (!clients.readarr) throw new Error("Readarr not configured"); const term = (args as { term: string }).term; const results = await clients.readarr.searchAuthors(term); return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results: results.slice(0, 10).map(r => ({ title: r.title, foreignAuthorId: r.foreignAuthorId, overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''), })), }, null, 2), }], }; } case "readarr_get_queue": { if (!clients.readarr) throw new Error("Readarr not configured"); const queue = await clients.readarr.getQueue(); return { content: [{ type: "text", text: JSON.stringify({ totalRecords: queue.totalRecords, items: queue.records.map(q => ({ title: q.title, status: q.status, progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%', timeLeft: q.timeleft, downloadClient: q.downloadClient, })), }, null, 2), }], }; } case "readarr_get_books": { if (!clients.readarr) throw new Error("Readarr not configured"); const authorId = (args as { authorId: number }).authorId; const books = await clients.readarr.getBooks(authorId); return { content: [{ type: "text", text: JSON.stringify({ count: books.length, books: books.map(b => ({ id: b.id, title: b.title, releaseDate: b.releaseDate, pageCount: b.pageCount, monitored: b.monitored, hasFile: b.statistics ? b.statistics.bookFileCount > 0 : false, sizeOnDisk: formatBytes(b.statistics?.sizeOnDisk || 0), grabbed: b.grabbed, })), }, null, 2), }], }; } case "readarr_search_book": { if (!clients.readarr) throw new Error("Readarr not configured"); const bookIds = (args as { bookIds: number[] }).bookIds; const result = await clients.readarr.searchBook(bookIds); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for ${bookIds.length} book(s)`, commandId: result.id, }, null, 2), }], }; } case "readarr_search_missing": { if (!clients.readarr) throw new Error("Readarr not configured"); const authorId = (args as { authorId: number }).authorId; const result = await clients.readarr.searchMissingBooks(authorId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Search triggered for missing books`, commandId: result.id, }, null, 2), }], }; } case "readarr_get_calendar": { if (!clients.readarr) throw new Error("Readarr not configured"); const days = (args as { days?: number })?.days || 30; const start = new Date().toISOString().split('T')[0]; const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const calendar = await clients.readarr.getCalendar(start, end); return { content: [{ type: "text", text: JSON.stringify({ count: calendar.length, books: calendar.map(b => ({ id: b.id, title: b.title, authorId: b.authorId, releaseDate: b.releaseDate, monitored: b.monitored, })), }, null, 2), }], }; } // Prowlarr handlers case "prowlarr_get_indexers": { if (!clients.prowlarr) throw new Error("Prowlarr not configured"); const indexers = await clients.prowlarr.getIndexers(); return { content: [{ type: "text", text: JSON.stringify({ count: indexers.length, indexers: indexers.map(i => ({ id: i.id, name: i.name, protocol: i.protocol, enableRss: i.enableRss, enableAutomaticSearch: i.enableAutomaticSearch, enableInteractiveSearch: i.enableInteractiveSearch, priority: i.priority, })), }, null, 2), }], }; } case "prowlarr_search": { if (!clients.prowlarr) throw new Error("Prowlarr not configured"); const query = (args as { query: string }).query; const results = await clients.prowlarr.search(query); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } case "prowlarr_test_indexers": { if (!clients.prowlarr) throw new Error("Prowlarr not configured"); const results = await clients.prowlarr.testAllIndexers(); const indexers = await clients.prowlarr.getIndexers(); const indexerMap = new Map(indexers.map(i => [i.id, i.name])); return { content: [{ type: "text", text: JSON.stringify({ count: results.length, indexers: results.map(r => ({ id: r.id, name: indexerMap.get(r.id) || 'Unknown', isValid: r.isValid, errors: r.validationFailures.map(f => f.errorMessage), })), healthy: results.filter(r => r.isValid).length, failed: results.filter(r => !r.isValid).length, }, null, 2), }], }; } case "prowlarr_get_stats": { if (!clients.prowlarr) throw new Error("Prowlarr not configured"); const stats = await clients.prowlarr.getIndexerStats(); return { content: [{ type: "text", text: JSON.stringify({ count: stats.indexers.length, indexers: stats.indexers.map(s => ({ name: s.indexerName, queries: s.numberOfQueries, grabs: s.numberOfGrabs, failedQueries: s.numberOfFailedQueries, failedGrabs: s.numberOfFailedGrabs, avgResponseTime: s.averageResponseTime + 'ms', })), totals: { queries: stats.indexers.reduce((sum, s) => sum + s.numberOfQueries, 0), grabs: stats.indexers.reduce((sum, s) => sum + s.numberOfGrabs, 0), failedQueries: stats.indexers.reduce((sum, s) => sum + s.numberOfFailedQueries, 0), failedGrabs: stats.indexers.reduce((sum, s) => sum + s.numberOfFailedGrabs, 0), }, }, null, 2), }], }; } // Cross-service search case "arr_search_all": { const term = (args as { term: string }).term; const results: Record<string, unknown> = {}; if (clients.sonarr) { try { const sonarrResults = await clients.sonarr.searchSeries(term); results.sonarr = { count: sonarrResults.length, results: sonarrResults.slice(0, 5) }; } catch (e) { results.sonarr = { error: e instanceof Error ? e.message : String(e) }; } } if (clients.radarr) { try { const radarrResults = await clients.radarr.searchMovies(term); results.radarr = { count: radarrResults.length, results: radarrResults.slice(0, 5) }; } catch (e) { results.radarr = { error: e instanceof Error ? e.message : String(e) }; } } if (clients.lidarr) { try { const lidarrResults = await clients.lidarr.searchArtists(term); results.lidarr = { count: lidarrResults.length, results: lidarrResults.slice(0, 5) }; } catch (e) { results.lidarr = { error: e instanceof Error ? e.message : String(e) }; } } if (clients.readarr) { try { const readarrResults = await clients.readarr.searchAuthors(term); results.readarr = { count: readarrResults.length, results: readarrResults.slice(0, 5) }; } catch (e) { results.readarr = { error: e instanceof Error ? e.message : String(e) }; } } return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); // Helper function to format bytes function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`*arr MCP server running - configured services: ${configuredServices.map(s => s.name).join(', ')}`); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });

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/aplaceforallmystuff/mcp-arr'

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