Skip to main content
Glama

request_media

Request movies or TV shows through Overseerr for your Plex media library. Submit requests using TMDB IDs, specify seasons for TV content, and configure quality preferences.

Instructions

Request a movie or TV show in Overseerr. For TV shows, you can request specific seasons or all seasons.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
mediaTypeYesType of media to request
mediaIdYesTMDB ID of the media
seasonsNoFor TV shows: array of season numbers or "all" (optional)
is4kNoRequest 4K version (default: false)
serverIdNoSpecific server ID (optional)
profileIdNoQuality profile ID (optional)
rootFolderNoRoot folder path (optional)

Implementation Reference

  • src/index.ts:454-523 (registration)
    Registration of the 'request_media' tool in the ListToolsRequestSchema handler, including name, description, and detailed inputSchema.
    { name: 'request_media', description: 'Request media with auto-confirm for TV ≤24 eps. Single/batch with validation.\n' + 'Confirm: Movies auto | TV ≤24 eps auto | TV >24 eps needs confirmed:true\n' + 'TV needs seasons (array or "all"). "all"=no specials; [0,1,2]=with specials', inputSchema: { type: 'object', properties: { mediaType: { type: 'string', enum: ['movie', 'tv'], description: 'Media type (single)', }, mediaId: { type: 'number', description: 'TMDB ID (single)', }, items: { type: 'array', items: { type: 'object', properties: { mediaType: { type: 'string', enum: ['movie', 'tv'] }, mediaId: { type: 'number' }, seasons: { oneOf: [ { type: 'array', items: { type: 'number' } }, { type: 'string', enum: ['all'] }, ], description: 'TV seasons (REQUIRED). "all"=no season 0 (specials); [0,1,2]=with specials', }, is4k: { type: 'boolean' }, }, required: ['mediaType', 'mediaId'], }, description: 'Batch items', }, 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' }, validateFirst: { type: 'boolean', description: 'Check existing', default: true, }, dryRun: { type: 'boolean', description: 'Preview only', default: false, }, confirmed: { type: 'boolean', description: 'Confirm multi-season', default: false, }, }, },
  • TypeScript interface RequestMediaArgs defining the input parameters for the request_media tool.
    export interface RequestMediaArgs { mediaType?: 'movie' | 'tv'; mediaId?: number; items?: Array<{ mediaType: 'movie' | 'tv'; mediaId: number; seasons?: number[] | 'all'; is4k?: boolean; }>; seasons?: number[] | 'all'; is4k?: boolean; serverId?: number; profileId?: number; rootFolder?: string; validateFirst?: boolean; dryRun?: boolean; confirmed?: boolean; }
  • Main handler functions for request_media: handleRequestMedia (dispatcher), handleSingleRequest (core logic: validation, auto-confirm, request), handleBatchRequest (batch support). Dispatched from switch case at line 634.
    private async handleRequestMedia(args: any) { const requestArgs = args as RequestMediaArgs; // Batch mode if (requestArgs.items && requestArgs.items.length > 0) { return this.handleBatchRequest(requestArgs); } // Single mode if (!requestArgs.mediaType || !requestArgs.mediaId) { throw new McpError( ErrorCode.InvalidParams, 'Must provide mediaType and mediaId (or items array for batch)' ); } return this.handleSingleRequest(requestArgs); } private async handleSingleRequest(args: RequestMediaArgs) { const { mediaType, mediaId, seasons, is4k, validateFirst, dryRun, confirmed } = args; // Validate TV show requests have seasons specified if (mediaType === 'tv' && !seasons) { throw new McpError( ErrorCode.InvalidParams, 'seasons parameter is required for TV show requests. Use seasons: [1,2,3] for specific seasons or seasons: "all" for all seasons.' ); } // Expand "all" to actual season numbers (excluding season 0) early in the function let expandedSeasons: number[] | undefined = undefined; if (mediaType === 'tv' && seasons) { if (seasons === 'all') { const response = await this.axiosInstance.get<MediaDetails>(`/tv/${mediaId}`); const details = response.data; // Get all regular seasons (exclude season 0 - specials) const regularSeasons = details.seasons?.filter(s => s.seasonNumber > 0) || []; expandedSeasons = regularSeasons.map(s => s.seasonNumber); // If no regular seasons found, fall back to numberOfSeasons if (expandedSeasons.length === 0 && details.numberOfSeasons) { expandedSeasons = Array.from({ length: details.numberOfSeasons }, (_, i) => i + 1); } } else { // Already an array, use as-is expandedSeasons = seasons as number[]; } } // Validate first if requested if (validateFirst) { const detailsCacheKey = { mediaType, mediaId }; let details = this.cache.get<MediaDetails>('mediaDetails', detailsCacheKey); if (!details) { const response = await this.axiosInstance.get<MediaDetails>( `/${mediaType}/${mediaId}` ); details = response.data; this.cache.set('mediaDetails', detailsCacheKey, details); } const mediaInfo = details.mediaInfo; if (mediaInfo?.requests && mediaInfo.requests.length > 0) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, status: 'ALREADY_REQUESTED', message: `${details.title || details.name} is already requested`, existingRequests: mediaInfo.requests.map(r => ({ id: r.id, status: this.getStatusString(r.status), requestedBy: r.requestedBy.displayName || r.requestedBy.email, createdAt: r.createdAt, })), }, null, 2), }, ], }; } if (mediaInfo?.status === 5) { // AVAILABLE return { content: [ { type: 'text', text: JSON.stringify({ success: false, status: 'ALREADY_AVAILABLE', message: `${details.title || details.name} is already available`, }, null, 2), }, ], }; } } // Multi-season confirmation check if (mediaType === 'tv' && !confirmed && expandedSeasons) { const requireConfirm = process.env.REQUIRE_MULTI_SEASON_CONFIRM !== 'false'; if (requireConfirm) { // Get details to calculate episode count const response = await this.axiosInstance.get<MediaDetails>(`/tv/${mediaId}`); const details = response.data; const totalSeasons = details.numberOfSeasons || 0; const seasonsToRequest = expandedSeasons; // Calculate total episode count for requested seasons let totalEpisodes = 0; if (details.seasons) { seasonsToRequest.forEach((seasonNum: number) => { const seasonData = details.seasons?.find(s => s.seasonNumber === seasonNum); if (seasonData) { totalEpisodes += seasonData.episodeCount; } }); } // Only require confirmation if episode count exceeds threshold (24) const EPISODE_THRESHOLD = 24; if (totalEpisodes > EPISODE_THRESHOLD) { // Build message including episode count for context return { content: [ { type: 'text', text: JSON.stringify({ requiresConfirmation: true, media: { totalSeasons, totalEpisodes: details.numberOfEpisodes, requestingSeasons: seasonsToRequest, requestingEpisodes: totalEpisodes, threshold: EPISODE_THRESHOLD, }, message: `This will request ${seasonsToRequest.length} season(s) with ${totalEpisodes} episodes of ${details.name}. Add "confirmed: true" to proceed.`, confirmWith: { ...args, confirmed: true, }, }, null, 2), }, ], }; } } } // Dry run - don't actually request if (dryRun) { const response = await this.axiosInstance.get<MediaDetails>( `/${mediaType}/${mediaId}` ); const details = response.data; return { content: [ { type: 'text', text: JSON.stringify({ dryRun: true, wouldRequest: { title: details.title || details.name, mediaType, mediaId, seasons: mediaType === 'tv' ? expandedSeasons : undefined, is4k: is4k || false, }, message: 'Dry run - no request was made. Remove "dryRun: true" to actually request.', }, null, 2), }, ], }; } // Actually make the request const requestBody: any = { mediaType, mediaId, is4k: is4k || false, }; // Use expandedSeasons (array) to ensure season 0 is not included if (mediaType === 'tv' && expandedSeasons) { requestBody.seasons = expandedSeasons; } if (args.serverId) requestBody.serverId = args.serverId; if (args.profileId) requestBody.profileId = args.profileId; if (args.rootFolder) requestBody.rootFolder = args.rootFolder; const response = await withRetry(async () => { return await this.axiosInstance.post('/request', requestBody); }); // Invalidate caches this.cache.invalidate('requests'); this.cache.invalidate('mediaDetails'); return { content: [ { type: 'text', text: JSON.stringify({ success: true, requestId: response.data.id, status: this.getStatusString(response.data.status), message: `Successfully requested ${response.data.media.title || response.data.media.name}`, seasonsRequested: response.data.seasons?.map((s: any) => s.seasonNumber), }, null, 2), }, ], }; } private async handleBatchRequest(args: RequestMediaArgs) { const items = args.items!; const results = await batchWithRetry( items, async (item) => { const singleArgs = { ...args, mediaType: item.mediaType, mediaId: item.mediaId, seasons: item.seasons, is4k: item.is4k, items: undefined, }; const result = await this.handleSingleRequest(singleArgs); return JSON.parse(result.content[0].text); } ); const successful = results.filter(r => r.success && r.result?.success); const failed = results.filter(r => !r.success || !r.result?.success); return { content: [ { type: 'text', text: JSON.stringify({ summary: { total: items.length, successful: successful.length, failed: failed.length, }, results: successful.map(r => r.result), errors: failed.map(r => ({ item: r.item, error: r.error?.message || r.result?.message || 'Unknown error', })), }, null, 2), }, ], };
  • Core single-request handler implementing validation, season expansion, pre-checks, confirmation prompts, and Overseerr API request submission.
    private async handleSingleRequest(args: RequestMediaArgs) { const { mediaType, mediaId, seasons, is4k, validateFirst, dryRun, confirmed } = args; // Validate TV show requests have seasons specified if (mediaType === 'tv' && !seasons) { throw new McpError( ErrorCode.InvalidParams, 'seasons parameter is required for TV show requests. Use seasons: [1,2,3] for specific seasons or seasons: "all" for all seasons.' ); } // Expand "all" to actual season numbers (excluding season 0) early in the function let expandedSeasons: number[] | undefined = undefined; if (mediaType === 'tv' && seasons) { if (seasons === 'all') { const response = await this.axiosInstance.get<MediaDetails>(`/tv/${mediaId}`); const details = response.data; // Get all regular seasons (exclude season 0 - specials) const regularSeasons = details.seasons?.filter(s => s.seasonNumber > 0) || []; expandedSeasons = regularSeasons.map(s => s.seasonNumber); // If no regular seasons found, fall back to numberOfSeasons if (expandedSeasons.length === 0 && details.numberOfSeasons) { expandedSeasons = Array.from({ length: details.numberOfSeasons }, (_, i) => i + 1); } } else { // Already an array, use as-is expandedSeasons = seasons as number[]; } } // Validate first if requested if (validateFirst) { const detailsCacheKey = { mediaType, mediaId }; let details = this.cache.get<MediaDetails>('mediaDetails', detailsCacheKey); if (!details) { const response = await this.axiosInstance.get<MediaDetails>( `/${mediaType}/${mediaId}` ); details = response.data; this.cache.set('mediaDetails', detailsCacheKey, details); } const mediaInfo = details.mediaInfo; if (mediaInfo?.requests && mediaInfo.requests.length > 0) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, status: 'ALREADY_REQUESTED', message: `${details.title || details.name} is already requested`, existingRequests: mediaInfo.requests.map(r => ({ id: r.id, status: this.getStatusString(r.status), requestedBy: r.requestedBy.displayName || r.requestedBy.email, createdAt: r.createdAt, })), }, null, 2), }, ], }; } if (mediaInfo?.status === 5) { // AVAILABLE return { content: [ { type: 'text', text: JSON.stringify({ success: false, status: 'ALREADY_AVAILABLE', message: `${details.title || details.name} is already available`, }, null, 2), }, ], }; } } // Multi-season confirmation check if (mediaType === 'tv' && !confirmed && expandedSeasons) { const requireConfirm = process.env.REQUIRE_MULTI_SEASON_CONFIRM !== 'false'; if (requireConfirm) { // Get details to calculate episode count const response = await this.axiosInstance.get<MediaDetails>(`/tv/${mediaId}`); const details = response.data; const totalSeasons = details.numberOfSeasons || 0; const seasonsToRequest = expandedSeasons; // Calculate total episode count for requested seasons let totalEpisodes = 0; if (details.seasons) { seasonsToRequest.forEach((seasonNum: number) => { const seasonData = details.seasons?.find(s => s.seasonNumber === seasonNum); if (seasonData) { totalEpisodes += seasonData.episodeCount; } }); } // Only require confirmation if episode count exceeds threshold (24) const EPISODE_THRESHOLD = 24; if (totalEpisodes > EPISODE_THRESHOLD) { // Build message including episode count for context return { content: [ { type: 'text', text: JSON.stringify({ requiresConfirmation: true, media: { totalSeasons, totalEpisodes: details.numberOfEpisodes, requestingSeasons: seasonsToRequest, requestingEpisodes: totalEpisodes, threshold: EPISODE_THRESHOLD, }, message: `This will request ${seasonsToRequest.length} season(s) with ${totalEpisodes} episodes of ${details.name}. Add "confirmed: true" to proceed.`, confirmWith: { ...args, confirmed: true, }, }, null, 2), }, ], }; } } } // Dry run - don't actually request if (dryRun) { const response = await this.axiosInstance.get<MediaDetails>( `/${mediaType}/${mediaId}` ); const details = response.data; return { content: [ { type: 'text', text: JSON.stringify({ dryRun: true, wouldRequest: { title: details.title || details.name, mediaType, mediaId, seasons: mediaType === 'tv' ? expandedSeasons : undefined, is4k: is4k || false, }, message: 'Dry run - no request was made. Remove "dryRun: true" to actually request.', }, null, 2), }, ], }; } // Actually make the request const requestBody: any = { mediaType, mediaId, is4k: is4k || false, }; // Use expandedSeasons (array) to ensure season 0 is not included if (mediaType === 'tv' && expandedSeasons) { requestBody.seasons = expandedSeasons; } if (args.serverId) requestBody.serverId = args.serverId; if (args.profileId) requestBody.profileId = args.profileId; if (args.rootFolder) requestBody.rootFolder = args.rootFolder; const response = await withRetry(async () => { return await this.axiosInstance.post('/request', requestBody); }); // Invalidate caches this.cache.invalidate('requests'); this.cache.invalidate('mediaDetails'); return { content: [ { type: 'text', text: JSON.stringify({ success: true, requestId: response.data.id, status: this.getStatusString(response.data.status), message: `Successfully requested ${response.data.media.title || response.data.media.name}`, seasonsRequested: response.data.seasons?.map((s: any) => s.seasonNumber), }, null, 2), }, ], }; }

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