Skip to main content
Glama

search_media

Search for movies and TV shows across media libraries, check availability status, and manage batch requests with deduplication to avoid duplicates.

Instructions

Search movies/TV with single/batch/dedupe modes. Dedupe returns actionable status for batch processing. Status: NOT_FOUND | ALREADY_AVAILABLE | ALREADY_REQUESTED | SEASON_AVAILABLE | SEASON_REQUESTED | AVAILABLE_FOR_REQUEST

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryNoSingle search query
queriesNoMultiple search queries (batch mode)
dedupeModeNoBatch dedupe with availability check
titlesNoTitles to check (dedupe mode)
autoNormalizeNoStrip "Season N"/"Part N" from titles
autoRequestNoAuto-request passing items (requires dedupeMode)
requestOptionsNoAutoRequest options
checkAvailabilityNoCheck status (slower, fetches per-result details)
formatNoResponse formatcompact
limitNoMax results
pageNoPage number
languageNoLanguage codeen
includeDetailsNoAdd details to dedupe results (dedupe only)

Implementation Reference

  • Main handler function for 'search_media' tool that dispatches to single search, batch search, or dedupe mode based on input arguments
    private async handleSearchMedia(args: SearchMediaArgs) { const searchArgs = args as SearchMediaArgs; // Dedupe mode - batch check multiple titles if (searchArgs.dedupeMode && searchArgs.titles) { return this.handleDedupeMode(searchArgs); } // Batch mode - multiple queries if (searchArgs.queries && searchArgs.queries.length > 0) { return this.handleBatchSearch(searchArgs); } // Single search mode if (searchArgs.query) { return this.handleSingleSearch(searchArgs); } throw new McpError( ErrorCode.InvalidParams, 'Must provide either query, queries, or (dedupeMode + titles)' ); }
  • Core dedupe mode handler performing TMDB search, Overseerr availability checks, status determination (ALREADY_AVAILABLE, SEASON_REQUESTED, etc.), and optional auto-requesting
    private async handleDedupeMode(args: SearchMediaArgs) { const titles = args.titles!; const autoNormalize = args.autoNormalize || false; const autoRequest = args.autoRequest || false; const includeDetails = args.includeDetails; const requestedFields = includeDetails?.fields || []; const includeSeason = includeDetails?.includeSeason !== false; // default true const dedupeResults: DedupeResult[] = []; const autoRequestQueue: Array<{ mediaType: 'movie' | 'tv'; mediaId: number; seasons?: number[] | 'all' }> = []; const processedTitles = await batchWithRetry( titles, async (originalTitle) => { // Normalize title if requested const searchTitle = autoNormalize ? normalizeTitle(originalTitle) : originalTitle; const seasonNumber = extractSeasonNumber(originalTitle); // Search for the title - build URL manually with encoded query const cacheKey = { query: searchTitle, page: 1, language: args.language || 'en' }; let searchResult = this.cache.get<SearchResult>('search', cacheKey); if (!searchResult) { const encodedQuery = encodeSearchQuery(searchTitle); const language = args.language || 'en'; const url = `/search?query=${encodedQuery}&page=1&language=${language}`; const response = await this.axiosInstance.get<SearchResult>(url); searchResult = response.data; this.cache.set('search', cacheKey, searchResult); } // If no results, it's a pass (not in system) if (!searchResult.results || searchResult.results.length === 0) { // Not found since NOT_FOUND items cannot be requested const baseResult: DedupeResult = { title: originalTitle, id: 0, mediaType: undefined, // Unknown since not found status: 'blocked' as const, reasonCode: 'NOT_FOUND', isActionable: false, note: 'Not found in TMDB', }; // No enrichment for not found items return baseResult; } // Smart result selection with media type validation const expectedType = inferExpectedMediaType(originalTitle); const selection = selectBestMatch(searchResult.results, expectedType, searchTitle); let bestMatch = selection.match; let alternates = selection.alternates; // Log low confidence matches for debugging if (selection.confidence === 'low') { console.error(`[WARN] Low confidence match for "${originalTitle}": expected ${expectedType}, got ${bestMatch.mediaType} (${bestMatch.title || bestMatch.name})`); } // For season-specific queries, validate season number exists in matched series if (seasonNumber && bestMatch.mediaType === 'tv') { // Fetch details to check numberOfSeasons const detailsCacheKey = { mediaType: 'tv', mediaId: bestMatch.id }; let details = this.cache.get<MediaDetails>('mediaDetails', detailsCacheKey); if (!details) { const detailsResponse = await this.axiosInstance.get<MediaDetails>( `/tv/${bestMatch.id}` ); details = detailsResponse.data; details.mediaType = 'tv'; this.cache.set('mediaDetails', detailsCacheKey, details); } // Validate: if requested season > total seasons, try alternates if (details.numberOfSeasons && seasonNumber > details.numberOfSeasons) { console.error(`[WARN] Season ${seasonNumber} requested but "${bestMatch.title || bestMatch.name}" only has ${details.numberOfSeasons} seasons. Trying alternates...`); // Try each alternate let foundValid = false; for (const alternate of alternates) { if (alternate.mediaType !== 'tv') continue; const altCacheKey = { mediaType: 'tv', mediaId: alternate.id }; let altDetails = this.cache.get<MediaDetails>('mediaDetails', altCacheKey); if (!altDetails) { const altResponse = await this.axiosInstance.get<MediaDetails>(`/tv/${alternate.id}`); altDetails = altResponse.data; altDetails.mediaType = 'tv'; this.cache.set('mediaDetails', altCacheKey, altDetails); } // Check if this alternate has enough seasons if (altDetails.numberOfSeasons && seasonNumber <= altDetails.numberOfSeasons) { console.error(`[INFO] Found valid alternate: "${alternate.title || alternate.name}" with ${altDetails.numberOfSeasons} seasons`); bestMatch = alternate; foundValid = true; break; } } // If no valid match found, return NOT_FOUND if (!foundValid) { return { title: originalTitle, id: 0, mediaType: undefined, status: 'blocked' as const, reasonCode: 'NOT_FOUND', isActionable: false, note: `Season ${seasonNumber} not found - no matching series with that many seasons` }; } } } // Check if it's TV and we need details for season checking if (bestMatch.mediaType === 'tv') { const detailsCacheKey = { mediaType: 'tv', mediaId: bestMatch.id }; let details = this.cache.get<MediaDetails>('mediaDetails', detailsCacheKey); if (!details) { const detailsResponse = await this.axiosInstance.get<MediaDetails>( `/tv/${bestMatch.id}` ); details = detailsResponse.data; // Add mediaType to details for enrichment details.mediaType = 'tv'; this.cache.set('mediaDetails', detailsCacheKey, details); } // Get media info for status checking const mediaInfo = details.mediaInfo; // CASE 1: Specific season mentioned in title if (seasonNumber) { // Check if this specific season is in library (PENDING, PROCESSING, PARTIALLY_AVAILABLE, or AVAILABLE) // Do NOT block: UNKNOWN(1), DELETED(6), or missing const targetSeasonInfo = mediaInfo?.seasons?.find(s => s.seasonNumber === seasonNumber); if (targetSeasonInfo && [2, 3, 4, 5].includes(targetSeasonInfo.status)) { const statusStr = this.getMediaStatusString(targetSeasonInfo.status); const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `Season ${seasonNumber} is ${statusStr.toLowerCase()}`, reasonCode: 'SEASON_AVAILABLE', isActionable: false, franchiseInfo: `Season ${seasonNumber} of ${details.name || bestMatch.name}`, }; if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, seasonNumber, includeSeason ) }; } return { result: baseResult }; } // Check if this specific season is requested const seasonRequested = mediaInfo?.requests?.some(req => req.media.seasons?.some(s => s.seasonNumber === seasonNumber) ); if (seasonRequested) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `Season ${seasonNumber} is already requested`, reasonCode: 'SEASON_REQUESTED', isActionable: false, franchiseInfo: `Season ${seasonNumber} of ${details.name || bestMatch.name}`, }; if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, seasonNumber, includeSeason ) }; } return { result: baseResult }; } // Specific season not in library/requested - it's a pass const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'pass' as const, reasonCode: 'AVAILABLE_FOR_REQUEST', isActionable: true, franchiseInfo: `Season ${seasonNumber} of ${details.name || bestMatch.name}`, }; // Auto-add enhanced details if requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, seasonNumber, includeSeason ) }; } return { result: baseResult }; } // CASE 2: No specific season mentioned - check base series availability // BUG FIX: Check show-level status FIRST before checking individual seasons // This catches shows marked as AVAILABLE at show level even without complete season data if (mediaInfo && [5].includes(mediaInfo.status)) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `Already in library (show-level)`, reasonCode: 'ALREADY_AVAILABLE', isActionable: false, franchiseInfo: `${details.name || bestMatch.name}`, }; if (requestedFields.length > 0) { return this.enrichDedupeResult(baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason); } return { result: baseResult }; } // Check if there are show-level requests (not season-specific) if (mediaInfo?.requests && mediaInfo.requests.length > 0) { const hasShowLevelRequest = mediaInfo.requests.some(req => !req.media.seasons || req.media.seasons.length === 0 ); if (hasShowLevelRequest) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: 'Already requested (show-level)', reasonCode: 'ALREADY_REQUESTED', isActionable: false, franchiseInfo: `${details.name || bestMatch.name}`, }; if (requestedFields.length > 0) { return this.enrichDedupeResult(baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason); } return { result: baseResult }; } } // Now check individual seasons const regularSeasons = details.seasons?.filter(s => s.seasonNumber > 0) || []; if (regularSeasons.length > 0) { // Check if ALL regular seasons are in library (statuses 2-5) const allSeasonsAvailable = regularSeasons.every(season => { const seasonInfo = mediaInfo?.seasons?.find(s => s.seasonNumber === season.seasonNumber); return seasonInfo && [2, 3, 4, 5].includes(seasonInfo.status); }); if (allSeasonsAvailable && mediaInfo?.seasons && mediaInfo.seasons.length > 0) { const availableSeasons = mediaInfo.seasons.filter(s => s.seasonNumber > 0 && [2, 3, 4, 5].includes(s.status)).map(s => s.seasonNumber).sort((a, b) => a - b); const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `All regular seasons already in library (${availableSeasons.join(', ')})`, reasonCode: 'ALREADY_AVAILABLE', isActionable: false, franchiseInfo: `${details.name || bestMatch.name} - All ${availableSeasons.length} seasons in library (S${availableSeasons.join(', S')})`, }; if (requestedFields.length > 0) { return this.enrichDedupeResult(baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason); } return { result: baseResult }; } // Check if ALL regular seasons are requested const allSeasonsRequested = regularSeasons.every(season => { return mediaInfo?.requests?.some(req => req.media.seasons?.some(s => s.seasonNumber === season.seasonNumber) ); }); if (allSeasonsRequested && mediaInfo?.requests && mediaInfo.requests.length > 0) { const requestedSeasons = regularSeasons.filter(season => mediaInfo.requests?.some(req => req.media.seasons?.some(s => s.seasonNumber === season.seasonNumber)) ).map(s => s.seasonNumber).sort((a, b) => a - b); const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `All regular seasons already requested (${requestedSeasons.join(', ')})`, reasonCode: 'ALREADY_REQUESTED', isActionable: false, franchiseInfo: `${details.name || bestMatch.name} - All ${requestedSeasons.length} seasons requested (S${requestedSeasons.join(', S')})`, }; if (requestedFields.length > 0) { return this.enrichDedupeResult(baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason); } return { result: baseResult }; } // Partial availability/requests - check enhanced franchise info let availableSeasons = mediaInfo?.seasons?.filter(s => s.seasonNumber > 0 && [2, 3, 4, 5].includes(s.status)).map(s => s.seasonNumber).sort((a, b) => a - b) || []; let requestedSeasons = regularSeasons.filter(season => mediaInfo?.requests?.some(req => req.media.seasons?.some(s => s.seasonNumber === season.seasonNumber)) ).map(s => s.seasonNumber).sort((a, b) => a - b); // Build enhanced franchise info let franchiseInfo = `${details.name || bestMatch.name}`; if (availableSeasons.length > 0 || requestedSeasons.length > 0) { const statusParts = []; if (availableSeasons.length > 0) { statusParts.push(`${availableSeasons.length} in library (S${availableSeasons.join(', S')})`); } if (requestedSeasons.length > 0) { statusParts.push(`${requestedSeasons.length} requested (S${requestedSeasons.join(', S')})`); } franchiseInfo += ` - ${statusParts.join(', ')}`; } // Some seasons in library/requested, but not all - it's a pass const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'pass' as const, reasonCode: 'AVAILABLE_FOR_REQUEST', isActionable: true, franchiseInfo: franchiseInfo, }; // Auto-add enhanced details if requested if (requestedFields.length > 0) { return this.enrichDedupeResult(baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason); } return { result: baseResult }; } // Fallback: No seasons info available, check overall status if (mediaInfo && [2, 3, 4, 5].includes(mediaInfo.status)) { const statusStr = this.getMediaStatusString(mediaInfo.status); const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: `Already in library (${statusStr.toLowerCase()})`, reasonCode: 'ALREADY_AVAILABLE', isActionable: false, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } if (mediaInfo?.requests && mediaInfo.requests.length > 0) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'blocked' as const, reason: 'Already requested', reasonCode: 'ALREADY_REQUESTED', isActionable: false, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } // Not requested - it's a pass const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'tv', status: 'pass' as const, reasonCode: 'AVAILABLE_FOR_REQUEST', isActionable: true, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'tv', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } else { // Movie - simpler check const detailsCacheKey = { mediaType: 'movie', mediaId: bestMatch.id }; let details = this.cache.get<MediaDetails>('mediaDetails', detailsCacheKey); if (!details) { const detailsResponse = await this.axiosInstance.get<MediaDetails>( `/movie/${bestMatch.id}` ); details = detailsResponse.data; // Add mediaType to details for enrichment details.mediaType = 'movie'; this.cache.set('mediaDetails', detailsCacheKey, details); } const mediaInfo = details.mediaInfo; if (mediaInfo && mediaInfo.status) { const statusStr = this.getMediaStatusString(mediaInfo.status); // Check if movie is in library (statuses 2-5) if ([2, 3, 4, 5].includes(mediaInfo.status)) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'movie', status: 'blocked' as const, reason: `Already in library (${statusStr.toLowerCase()})`, reasonCode: 'ALREADY_AVAILABLE', isActionable: false, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'movie', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } if (mediaInfo.requests && mediaInfo.requests.length > 0) { const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'movie', status: 'blocked' as const, reason: 'Already requested', reasonCode: 'ALREADY_REQUESTED', isActionable: false, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'movie', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } } // Not requested - it's a pass const baseResult: DedupeResult = { title: originalTitle, id: bestMatch.id, mediaType: 'movie', status: 'pass' as const, reasonCode: 'AVAILABLE_FOR_REQUEST', isActionable: true, }; // Enrich if details requested if (requestedFields.length > 0) { return { result: this.enrichDedupeResult( baseResult, { mediaType: 'movie', id: bestMatch.id }, details, requestedFields, null, includeSeason ) }; } return { result: baseResult }; } } ); // Collect results processedTitles.forEach(result => { if (result.success && result.result) { const dedupeItem = result.result as DedupeResult; dedupeResults.push(dedupeItem); // If autoRequest enabled, queue this item for requesting if (autoRequest && dedupeItem.isActionable === true && dedupeItem.mediaType === 'tv') { const seasonNumber = extractSeasonNumber(result.item); // For TV shows, determine which seasons to request let seasonsToRequest: number[] | 'all' | undefined; if (seasonNumber) { // Specific season mentioned in title seasonsToRequest = [seasonNumber]; } else if (args.requestOptions?.seasons) { // Use requestOptions.seasons for TV shows without specific season seasonsToRequest = args.requestOptions.seasons; } else { // Default to 'all' if no season specified seasonsToRequest = 'all'; } autoRequestQueue.push({ mediaType: dedupeItem.mediaType, mediaId: dedupeItem.id, seasons: seasonsToRequest, }); } else if (autoRequest && dedupeItem.isActionable === true && dedupeItem.mediaType === 'movie') { // Movies don't need seasons autoRequestQueue.push({ mediaType: dedupeItem.mediaType, mediaId: dedupeItem.id, }); } } }); const passCount = dedupeResults.filter(r => r.status === 'pass').length; const blockedCount = dedupeResults.filter(r => r.status === 'blocked').length; const actionableCount = dedupeResults.filter(r => r.isActionable === true).length; // If autoRequest enabled and there are items to request, process them let autoRequestResults; if (autoRequest && autoRequestQueue.length > 0) { // Check if this is a dry run const isDryRun = args.requestOptions?.dryRun === true; if (isDryRun) { // Dry run - don't actually request, just show what would be requested autoRequestResults = { dryRun: true, totalQueued: autoRequestQueue.length, wouldRequest: autoRequestQueue.map(item => ({ mediaType: item.mediaType, mediaId: item.mediaId, seasons: item.seasons, })), message: 'Dry run - no requests were made. Remove "dryRun: true" from requestOptions to actually request.', }; } else { // Actually make the requests const requestResults = await batchWithRetry( autoRequestQueue, async (item) => { try { // Expand "all" to actual season numbers (excluding season 0 - specials) let seasonsToRequest = item.seasons; if (item.mediaType === 'tv' && item.seasons === 'all') { const detailsResponse = await this.axiosInstance.get<MediaDetails>(`/tv/${item.mediaId}`); const details = detailsResponse.data; // Get all regular seasons excluding season 0 (specials) const regularSeasons = details.seasons?.filter(s => s.seasonNumber > 0) || []; seasonsToRequest = regularSeasons.map(s => s.seasonNumber); // If no regular seasons found, fall back to numberOfSeasons if (seasonsToRequest.length === 0 && details.numberOfSeasons) { seasonsToRequest = Array.from({ length: details.numberOfSeasons }, (_, i) => i + 1); // Filter out season 0 if it's in the list seasonsToRequest = seasonsToRequest.filter(s => s > 0); } } const requestBody: any = { mediaType: item.mediaType, mediaId: item.mediaId, is4k: args.requestOptions?.is4k || false, }; if (item.mediaType === 'tv' && seasonsToRequest) { requestBody.seasons = seasonsToRequest; } if (args.requestOptions?.serverId) requestBody.serverId = args.requestOptions.serverId; if (args.requestOptions?.profileId) requestBody.profileId = args.requestOptions.profileId; if (args.requestOptions?.rootFolder) requestBody.rootFolder = args.requestOptions.rootFolder; const response = await this.axiosInstance.post('/request', requestBody); // Invalidate caches this.cache.invalidate('requests'); this.cache.invalidate('mediaDetails'); return { success: true, requestId: response.data.id, mediaId: item.mediaId, mediaType: item.mediaType, seasons: seasonsToRequest, status: response.data.status }; } catch (error: any) { return { success: false, mediaId: item.mediaId, mediaType: item.mediaType, error: (error as any).response?.data?.message || (error as any).message || 'Unknown error', }; } } ); const successfulRequests = requestResults.filter(r => r.success && r.result?.success); const failedRequests = requestResults.filter(r => !r.success || !r.result?.success); autoRequestResults = { enqueue: true, totalRequested: autoRequestQueue.length, successful: successfulRequests.length, failed: failedRequests.length, requests: successfulRequests.map(r => r.result), errors: failedRequests.map(r => ({ mediaId: r.item.mediaId, mediaType: r.item.mediaType, error: r.error?.message || r.result?.error || 'Unknown error', })), }; } } const response: any = { summary: { total: titles.length, pass: passCount, blocked: blockedCount, actionable: actionableCount, passRate: `${((passCount / titles.length) * 100).toFixed(1)}%`, }, results: dedupeResults, }; if (autoRequestResults) { response.autoRequests = autoRequestResults; } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; }
  • src/index.ts:344-453 (registration)
    Registration of 'search_media' tool in MCP server, including name, description, and complete input schema
    { name: 'search_media', description: 'Search movies/TV with single/batch/dedupe modes. Dedupe returns actionable status for batch processing.\n' + 'Status: NOT_FOUND | ALREADY_AVAILABLE | ALREADY_REQUESTED | SEASON_AVAILABLE | SEASON_REQUESTED | AVAILABLE_FOR_REQUEST', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Single search query', }, queries: { type: 'array', items: { type: 'string' }, description: 'Multiple search queries (batch mode)', }, dedupeMode: { type: 'boolean', description: 'Batch dedupe with availability check', default: false, }, titles: { type: 'array', items: { type: 'string' }, description: 'Titles to check (dedupe mode)', }, autoNormalize: { type: 'boolean', description: 'Strip "Season N"/"Part N" from titles', default: false, }, autoRequest: { type: 'boolean', description: 'Auto-request passing items (requires dedupeMode)', default: false, }, requestOptions: { type: 'object', description: 'AutoRequest options', properties: { seasons: { oneOf: [ { type: 'array', items: { type: 'number' } }, { type: 'string', enum: ['all'] }, ], description: 'TV seasons. "all"=no season 0 (specials); [0,1,2]=with specials', }, is4k: { type: 'boolean', description: 'Request 4K', default: false, }, serverId: { type: 'number' }, profileId: { type: 'number' }, rootFolder: { type: 'string' }, dryRun: { type: 'boolean', description: 'Preview only', default: false, }, }, }, checkAvailability: { type: 'boolean', description: 'Check status (slower, fetches per-result details)', default: false, }, format: { type: 'string', enum: ['compact', 'standard', 'full'], description: 'Response format', default: 'compact', }, limit: { type: 'number', description: 'Max results', }, page: { type: 'number', description: 'Page number', default: 1, }, language: { type: 'string', description: 'Language code', default: 'en', }, includeDetails: { type: 'object', description: 'Add details to dedupe results (dedupe only)', properties: { fields: { type: 'array', items: { type: 'string' }, description: 'Basic: mediaType,year,posterPath | Standard: rating,overview,genres,runtime | ' + 'TV: numberOfSeasons,numberOfEpisodes,seasons | Advanced: releaseDate,firstAirDate,originalTitle,originalName,popularity,backdropPath,homepage,status,tagline | ' + 'Availability: mediaStatus,hasRequests,requestCount | targetSeason auto-adds for season numbers', }, includeSeason: { type: 'boolean', description: 'Auto-add targetSeason for TV with season in title', default: true, }, }, }, }, }, },
  • TypeScript interface defining input parameters for search_media tool
    export interface SearchMediaArgs { query?: string; queries?: string[]; dedupeMode?: boolean; titles?: string[]; autoNormalize?: boolean; autoRequest?: boolean; requestOptions?: { seasons?: number[] | 'all'; is4k?: boolean; serverId?: number; profileId?: number; rootFolder?: string; dryRun?: boolean; }; checkAvailability?: boolean; format?: 'compact' | 'standard' | 'full'; limit?: number; page?: number; language?: string; // NEW: Optional details enrichment for dedupe mode includeDetails?: { fields?: string[]; // Array of field names to include includeSeason?: boolean; // Auto-include season info for TV shows (default: true) }; }
  • Helper function to enrich seasons with availability status for dedupe results
    function enrichSeasons(details: MediaDetails): DedupeDetails['seasons'] { if (!details.seasons || !Array.isArray(details.seasons)) { return undefined; } return details.seasons.map(season => { // Find status for this season from mediaInfo let status = 'NOT_REQUESTED'; if (details.mediaInfo?.seasons) { const seasonInfo = details.mediaInfo.seasons.find(s => s.seasonNumber === season.seasonNumber); if (seasonInfo) { if (seasonInfo.status === 5) { status = 'AVAILABLE'; } else if (seasonInfo.status === 4) { status = 'PARTIALLY_AVAILABLE'; } else if (seasonInfo.status === 3) { status = 'PROCESSING'; } else if (seasonInfo.status === 2) { status = 'PENDING'; } } } // Check if this season has been requested if (details.mediaInfo?.requests) { const hasRequest = details.mediaInfo.requests.some(req => req.media.seasons?.some(s => s.seasonNumber === season.seasonNumber) ); if (hasRequest && status === 'NOT_REQUESTED') { status = 'REQUESTED'; } } return { seasonNumber: season.seasonNumber, episodeCount: season.episodeCount, airDate: season.airDate, status }; }); }

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/jhomen368/overseerr-mcp'

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