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
        };
      });
    }

Tool Definition Quality

Score is being calculated. Check back soon.

Install Server

Other Tools

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