Overseerr MCP Server
by jmagar
Verified
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from 'zod';
import { OverseerrService } from '../service.js';
export function registerTools(server: McpServer, service: OverseerrService) {
// Search Tools
server.tool(
"overseerr:search",
"Search for movies and TV shows",
{
query: z.string().describe("Search query"),
page: z.number().optional().describe("Page number")
},
async ({ query, page }) => {
try {
const results = await service.searchAll(query, { page });
const formattedText = [
'Search Results:',
'',
results.movies.length ? 'Movies:' : '',
...results.movies.map(m =>
`- ${m.title} (${m.releaseDate?.split('-')[0] || 'N/A'})\n ${m.overview}`
),
results.movies.length ? '' : '',
results.tvShows.length ? 'TV Shows:' : '',
...results.tvShows.map(t =>
`- ${t.name} (${t.firstAirDate?.split('-')[0] || 'N/A'})\n ${t.overview}`
)
].join('\n');
return {
content: [{
type: "text",
text: formattedText
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error searching: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
}
);
// Request Tools
server.tool(
"overseerr:request",
"Request a movie or TV show",
{
query: z.string().describe("Title to search for"),
type: z.enum(['movie', 'tv']).describe("Media type"),
autoApprove: z.boolean().optional().describe("Auto-approve the request"),
seasons: z.array(z.number()).optional().describe("Season numbers to request (TV only)")
},
async ({ query, type, autoApprove, seasons }) => {
try {
const request = await service.findAndRequest({
query,
mediaType: type,
autoApprove,
seasons
});
return {
content: [{
type: "text",
text: `Successfully ${autoApprove ? 'requested and approved' : 'requested'} ${type} "${query}" (ID: ${request.id})`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error making request: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
}
);
// List requests
server.tool(
'overseerr:requests:list',
'List media requests',
{
take: z.number().optional(),
skip: z.number().optional(),
filter: z.enum(['pending', 'approved', 'declined', 'available'] as const).optional(),
sort: z.string().optional(),
requestedBy: z.number().optional()
},
async (params) => {
const requests = await service.getRequests(params);
return {
content: [{
type: 'text',
text: JSON.stringify(requests, null, 2)
}]
};
}
);
// Get request details
server.tool(
'overseerr:requests:get',
'Get request details',
{
requestId: z.number()
},
async ({ requestId }) => {
const request = await service.getRequest(requestId);
return {
content: [{
type: 'text',
text: JSON.stringify(request, null, 2)
}]
};
}
);
// Update request status
server.tool(
'overseerr:requests:update-status',
'Update request status',
{
requestId: z.number(),
status: z.enum(['pending', 'approved', 'declined', 'available'] as const)
},
async ({ requestId, status }) => {
const request = await service.updateRequestStatus(requestId, status);
return {
content: [{
type: 'text',
text: JSON.stringify(request, null, 2)
}]
};
}
);
// Delete request
server.tool(
'overseerr:requests:delete',
'Delete request',
{
requestId: z.number()
},
async ({ requestId }) => {
await service.deleteRequest(requestId);
return {
content: [{
type: 'text',
text: `Request ${requestId} deleted successfully`
}]
};
}
);
// Get requests by status
server.tool(
'overseerr:requests:by-status',
'Get requests by status',
{
status: z.enum(['pending', 'approved', 'declined', 'available'] as const)
},
async ({ status }) => {
const requests = await service.getRequestsByStatus(status);
return {
content: [{
type: 'text',
text: JSON.stringify(requests, null, 2)
}]
};
}
);
// Discovery Tools
server.tool(
"overseerr:trending",
"Get trending media with recommendations",
{
type: z.enum(['movie', 'tv']).describe("Media type"),
page: z.number().optional().default(1).describe("Page number")
},
async ({ type, page }) => {
try {
const results = await service.getTrendingWithRecommendations(type, page);
return {
content: [{
type: "text",
text: JSON.stringify({
trending: results.trending.results.map(r => ({
title: 'title' in r ? r.title : r.name,
overview: r.overview,
popularity: r.popularity
})),
recommendations: results.recommendations.map(rec =>
rec.results.slice(0, 5).map(r => ({
title: 'title' in r ? r.title : r.name,
overview: r.overview
}))
)
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error getting trending media: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
}
);
server.tool(
"overseerr:available",
"Get popular available media",
{
type: z.enum(['movie', 'tv']).describe("Media type"),
page: z.number().optional().default(1).describe("Page number")
},
async ({ type, page }) => {
try {
const results = await service.getPopularAvailable(type, page);
return {
content: [{
type: "text",
text: JSON.stringify(results.results.map(r => ({
title: 'title' in r ? r.title : r.name,
overview: r.overview,
popularity: r.popularity
})), null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error getting available media: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
}
);
// Status Tools
server.tool(
"overseerr:status",
"Get system status",
{},
async () => {
try {
const status = await service.getSystemStatus();
return {
content: [{
type: "text",
text: JSON.stringify({
version: status.version,
healthy: status.healthy,
pendingRequestCount: status.pendingRequestCount,
availableMovies: status.availableMovies,
availableSeries: status.availableSeries
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error getting status: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
}
);
}