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