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
| Name | Required | Description | Default |
|---|---|---|---|
| mediaType | Yes | Type of media to request | |
| mediaId | Yes | TMDB ID of the media | |
| seasons | No | For TV shows: array of season numbers or "all" (optional) | |
| is4k | No | Request 4K version (default: false) | |
| serverId | No | Specific server ID (optional) | |
| profileId | No | Quality profile ID (optional) | |
| rootFolder | No | Root 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, }, }, },
- src/types.ts:112-129 (schema)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; }
- src/index.ts:1466-1729 (handler)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), }, ], };
- src/index.ts:1485-1686 (handler)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), }, ], }; }