Skip to main content
Glama

OpenSubtitles MCP Server

Official
index.js34.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { createOpenSubtitlesServer } from "./server.js"; import express from "express"; import cors from "cors"; import { createRequire } from 'module'; // Diagnostics: track recent initialize (handshake) per IP for helpful logs const HANDSHAKE_TTL_MS = 5 * 60 * 1000; // 5 minutes const lastHandshakeByIp = new Map(); const require = createRequire(import.meta.url); const packageJson = require('../package.json'); const serverVersion = packageJson.version; // Helper function for HTTP response (adaptive for n8n compatibility) function streamResponse(res, data, status = 200, forceJson = false) { // Prevent double response sending if (res.headersSent) { console.error('STREAMABLE: Headers already sent, skipping response'); return; } const userAgent = res.req?.get?.('User-Agent') || ''; const isN8n = userAgent.includes('node') || userAgent.includes('n8n') || userAgent.includes('langchain') || userAgent.includes('mcpClientTool') || forceJson; if (isN8n && data?.jsonrpc === '2.0') { // For n8n, always send plain JSON response for all JSON-RPC console.error('STREAMABLE: Sending plain JSON response for n8n compatibility, status:', status, 'UA:', userAgent); res.set({ 'Content-Type': 'application/json', 'Connection': 'keep-alive', 'Cache-Control': 'no-cache', 'MCP-Protocol-Version': '2025-06-18', 'X-Transport-Type': 'streamable-http', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'false' }); res.status(status).json(data); return; } console.error('STREAMABLE: Sending streaming response (chunked), status:', status); // Use chunked transfer for other clients res.set({ 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Cache-Control': 'no-cache', 'MCP-Protocol-Version': '2025-06-18', 'X-Transport-Type': 'streamable-http', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'false' }); res.status(status); if (data !== null) { const json = JSON.stringify(data); writeChunk(res, json); } // End the stream with empty chunk writeChunk(res, ''); } // Helper function to write HTTP chunks with proper encoding function writeChunk(res, data) { const length = Buffer.byteLength(data, 'utf8'); res.write(length.toString(16) + '\r\n'); res.write(data + '\r\n'); if (data === '') { res.end(); // End stream on empty chunk } } // Helper: build JSON-RPC response for a single request (no streaming write) async function dispatchJsonRpcSingle(obj, req) { if (!obj || obj.jsonrpc !== '2.0' || !obj.method) { return { jsonrpc: '2.0', id: obj?.id ?? null, error: { code: -32600, message: 'Invalid Request' } }; } if (obj.method === 'initialize') { const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.socket?.remoteAddress || 'unknown'; const ua = req.get?.('User-Agent') || ''; lastHandshakeByIp.set(ip, Date.now()); console.error(`HANDSHAKE OK: initialize from ip=${ip} ua="${ua}" ttl=${HANDSHAKE_TTL_MS}ms`); const requestedProto = obj.params?.protocolVersion; // Get tools for n8n compatibility - include in initialize response const openSubtitlesServerForInit = createOpenSubtitlesServer(); const toolsForInit = await openSubtitlesServerForInit.getTools(); console.error('STREAMABLE: Including tools in initialize response for n8n compatibility, count:', toolsForInit.length); const initResponse = { jsonrpc: '2.0', id: obj.id, result: { protocolVersion: requestedProto || '2024-11-05', capabilities: { tools: { listChanged: true, available: toolsForInit.map(t => t.name) }, resources: { subscribe: true, listChanged: true } }, serverInfo: { name: 'opensubtitles-mcp-server', version: serverVersion }, // Include tools directly for n8n compatibility tools: toolsForInit } }; console.error('STREAMABLE: Sending initialize response (dispatchJsonRpcSingle):', JSON.stringify(initResponse, null, 2)); return initResponse; } try { const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.socket?.remoteAddress || 'unknown'; const last = lastHandshakeByIp.get(ip); if (!last || (Date.now() - last) > HANDSHAKE_TTL_MS) { console.error(`HANDSHAKE MISSING: method=${obj.method} ip=${ip} (no initialize in last ${HANDSHAKE_TTL_MS}ms) – proceeding anyway`); } } catch { } const openSubtitlesServer = createOpenSubtitlesServer(); if (obj.method === 'tools/list' || obj.method === 'tools/list/all') { console.error('STREAMABLE: Listing tools'); const tools = await openSubtitlesServer.getTools(); console.error('STREAMABLE: Tools retrieved, count:', tools.length, 'names:', tools.map(t => t.name)); return { jsonrpc: '2.0', id: obj.id, result: { tools } }; } if (obj.method === 'tools/call') { console.error('STREAMABLE: Calling tool:', obj.params?.name); try { const headers = req.headers || {}; const authHeader = (headers['authorization'] || headers['Authorization']); const apiKeyHeader = (headers['api-key'] || headers['Api-Key']); const incomingApiKey = apiKeyHeader || (authHeader && authHeader.toLowerCase().startsWith('bearer ') ? authHeader.slice(7).trim() : undefined); if (incomingApiKey) { obj.params = obj.params || {}; obj.params.arguments = obj.params.arguments || {}; if (!obj.params.arguments.user_api_key) { obj.params.arguments.user_api_key = incomingApiKey; console.error('STREAMABLE: Injected user_api_key from request headers'); } } } catch (e) { console.error('STREAMABLE: Failed to inject user_api_key from headers:', e); } const result = await openSubtitlesServer.handleToolCall(obj.params); return { jsonrpc: '2.0', id: obj.id, result }; } if (obj.method === 'resources/list') { console.error('STREAMABLE: Listing resources'); const resources = await openSubtitlesServer.getResources(); return { jsonrpc: '2.0', id: obj.id, result: { resources } }; } if (obj.method === 'resources/read') { console.error('STREAMABLE: Reading resource:', obj.params?.uri); const result = await openSubtitlesServer.readResource(obj.params); return { jsonrpc: '2.0', id: obj.id, result }; } console.error('STREAMABLE: Method not found:', obj.method); return { jsonrpc: '2.0', id: obj.id ?? null, error: { code: -32601, message: `Method ${obj.method} is not supported` } }; } // Handle JSON-RPC MCP requests async function handleJsonRpcRequest(req, res) { try { // Parse JSON body from raw buffer let body; try { let rawBody; if (Buffer.isBuffer(req.body)) { rawBody = req.body.toString('utf8'); } else if (typeof req.body === 'string') { rawBody = req.body; } else if (req.body && typeof req.body === 'object') { // Already parsed by Express body = req.body; rawBody = JSON.stringify(req.body); } else { rawBody = ''; } console.error('STREAMABLE: Raw body received (first 100 chars):', rawBody.substring(0, 100)); if (!body && rawBody) { body = JSON.parse(rawBody); } else if (!body) { throw new Error('Empty request body'); } } catch (parseError) { console.error('STREAMABLE: JSON parse error:', parseError); streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error', data: parseError instanceof Error ? parseError.message : 'Invalid JSON' } }, 400); return; } console.error('STREAMABLE: Handling JSON-RPC request:', Array.isArray(body) ? `batch(${body.length})` : body.method, 'id:', Array.isArray(body) ? 'batch' : body.id); if (!Array.isArray(body)) { console.error('STREAMABLE: Request details - method:', body.method, 'params:', JSON.stringify(body.params)); } // Batch support if (Array.isArray(body)) { const results = []; for (const item of body) { const reply = await dispatchJsonRpcSingle(item, req); results.push(reply); } streamResponse(res, results); return; } // Validate JSON-RPC structure if (!body.jsonrpc || body.jsonrpc !== '2.0' || !body.method) { streamResponse(res, { jsonrpc: '2.0', id: body.id || null, error: { code: -32600, message: 'Invalid Request', data: 'Missing jsonrpc, method, or invalid format' } }, 400); return; } // Handle initialization (MCP handshake) if (body.method === 'initialize') { const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.socket?.remoteAddress || 'unknown'; const ua = req.get?.('User-Agent') || ''; lastHandshakeByIp.set(ip, Date.now()); console.error(`HANDSHAKE OK: initialize from ip=${ip} ua="${ua}" ttl=${HANDSHAKE_TTL_MS}ms`); // Return the protocolVersion requested by client if provided const requestedProto = body.params?.protocolVersion; // Generate a temporary session id header to align with Woo MCP behavior try { const sid = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); res.set({ 'Mcp-Session-Id': sid }); } catch { } // Get tools for n8n compatibility - include in initialize response const openSubtitlesServerForInit = createOpenSubtitlesServer(); const toolsForInit = await openSubtitlesServerForInit.getTools(); console.error('STREAMABLE: Including tools in initialize response for n8n compatibility, count:', toolsForInit.length); const initResponse = { jsonrpc: '2.0', id: body.id, result: { protocolVersion: requestedProto || '2024-11-05', capabilities: { tools: { listChanged: true, available: toolsForInit.map(t => t.name) }, resources: { subscribe: true, listChanged: true } }, serverInfo: { name: 'opensubtitles-mcp-server', version: serverVersion }, // Include tools directly for n8n compatibility tools: toolsForInit } }; console.error('STREAMABLE: Sending initialize response:', JSON.stringify(initResponse, null, 2)); streamResponse(res, initResponse); return; } // Handle notifications (no response needed) if (body.method === 'notifications/initialized') { console.error('STREAMABLE: Received initialized notification'); // No response for notifications res.status(204).end(); return; } // Create MCP server and handle request const openSubtitlesServer = createOpenSubtitlesServer(); // Warn when no recent handshake was seen for this client try { const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.socket?.remoteAddress || 'unknown'; const last = lastHandshakeByIp.get(ip); if (!last || (Date.now() - last) > HANDSHAKE_TTL_MS) { console.error(`HANDSHAKE MISSING: method=${body.method} ip=${ip} (no initialize in last ${HANDSHAKE_TTL_MS}ms) – proceeding anyway`); } } catch (e) { console.error('HANDSHAKE CHECK ERROR:', e); } if (body.method === 'tools/list' || body.method === 'tools/list/all') { console.error('STREAMABLE: Listing tools'); const tools = await openSubtitlesServer.getTools(); console.error('STREAMABLE: Tools retrieved, count:', tools.length, 'names:', tools.map(t => t.name)); streamResponse(res, { jsonrpc: '2.0', id: body.id, result: { tools } }); return; } if (body.method === 'tools/call') { console.error('STREAMABLE: Calling tool:', body.params?.name); // Propagate API key from Authorization/Api-Key headers if not provided in arguments try { const headers = req.headers || {}; const authHeader = (headers['authorization'] || headers['Authorization']); const apiKeyHeader = (headers['api-key'] || headers['Api-Key']); const incomingApiKey = apiKeyHeader || (authHeader && authHeader.toLowerCase().startsWith('bearer ') ? authHeader.slice(7).trim() : undefined); if (incomingApiKey) { body.params = body.params || {}; body.params.arguments = body.params.arguments || {}; if (!body.params.arguments.user_api_key) { body.params.arguments.user_api_key = incomingApiKey; console.error('STREAMABLE: Injected user_api_key from request headers'); } } } catch (e) { console.error('STREAMABLE: Failed to inject user_api_key from headers:', e); } const result = await openSubtitlesServer.handleToolCall(body.params); streamResponse(res, { jsonrpc: '2.0', id: body.id, result }); return; } if (body.method === 'resources/list') { console.error('STREAMABLE: Listing resources'); const resources = await openSubtitlesServer.getResources(); streamResponse(res, { jsonrpc: '2.0', id: body.id, result: { resources } }); return; } if (body.method === 'resources/read') { console.error('STREAMABLE: Reading resource:', body.params?.uri); const result = await openSubtitlesServer.readResource(body.params); streamResponse(res, { jsonrpc: '2.0', id: body.id, result }); return; } // Method not found console.error('STREAMABLE: Method not found:', body.method); streamResponse(res, { jsonrpc: '2.0', id: body.id, error: { code: -32601, message: 'Method not found', data: `Method ${body.method} is not supported` } }, 404); } catch (error) { const ip = req.headers?.['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.socket?.remoteAddress || 'unknown'; console.error('STREAMABLE: Error handling JSON-RPC request:', error, ` ip=${ip}`); streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' } }, 500); } } async function createMCPServer() { console.error("DEBUG: Creating MCP server"); const server = new Server({ name: "opensubtitles-mcp-server", version: serverVersion, }, { capabilities: { tools: { listChanged: true }, resources: { subscribe: true, listChanged: true }, }, }); console.error("DEBUG: Server instance created"); // Create and setup the OpenSubtitles server const openSubtitlesServer = createOpenSubtitlesServer(); console.error("DEBUG: OpenSubtitles server created"); const isTestMode = process.env.MCP_TEST_MODE === 'true'; // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("DEBUG: ListTools request received"); try { const tools = await openSubtitlesServer.getTools(); console.error("DEBUG: Returning tools:", tools.map(t => t.name)); if (isTestMode) { console.error("DEBUG: Test mode - exiting after ListTools"); setTimeout(() => process.exit(0), 100); } return { tools }; } catch (error) { console.error("DEBUG: Error in ListTools handler:", error); throw error; } }); // Call tool handler server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error("DEBUG: CallTool request received"); try { const result = await openSubtitlesServer.handleToolCall(request.params); if (isTestMode) { console.error("DEBUG: Test mode - exiting after CallTool"); setTimeout(() => process.exit(0), 100); } return result; } catch (error) { console.error("DEBUG: Error in CallTool handler:", error); throw error; } }); // List resources handler server.setRequestHandler(ListResourcesRequestSchema, async () => { console.error("DEBUG: ListResources request received"); try { const resources = await openSubtitlesServer.getResources(); console.error("DEBUG: Returning resources:", resources.map(r => r.name)); if (isTestMode) { console.error("DEBUG: Test mode - exiting after ListResources"); setTimeout(() => process.exit(0), 100); } return { resources }; } catch (error) { console.error("DEBUG: Error in ListResources handler:", error); throw error; } }); // Read resource handler server.setRequestHandler(ReadResourceRequestSchema, async (request) => { console.error("DEBUG: ReadResource request received"); try { const result = await openSubtitlesServer.readResource(request.params); if (isTestMode) { console.error("DEBUG: Test mode - exiting after ReadResource"); setTimeout(() => process.exit(0), 100); } return result; } catch (error) { console.error("DEBUG: Error in ReadResource handler:", error); throw error; } }); console.error("DEBUG: Request handlers set up"); return server; } async function runStdioMode() { console.error("STDIO MODE: Starting stdio mode"); try { const server = await createMCPServer(); console.error("STDIO MODE: Server created successfully"); const transport = new StdioServerTransport(); console.error("STDIO MODE: Transport created"); await server.connect(transport); console.error("STDIO MODE: Connected successfully - OpenSubtitles MCP server running on stdio"); } catch (error) { console.error("STDIO MODE: Error in runStdioMode:", error); process.exit(1); } } async function runHttpMode() { const port = parseInt(process.env.PORT || "1620"); const app = express(); // Enable CORS for all origins app.use(cors({ origin: true, credentials: true })); app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'opensubtitles-mcp-server', version: serverVersion }); }); // Info endpoint app.get('/', (req, res) => { res.json({ name: 'OpenSubtitles MCP Server', version: serverVersion, description: 'MCP server for OpenSubtitles API integration', endpoints: { health: '/health', message: '/message', proxy: '/proxy', debug: '/debug' }, usage: { 'Claude Desktop (stdio)': 'Use stdio mode with npx @opensubtitles/mcp-server', 'Claude Desktop (remote)': 'Use remote proxy with npx mcp-opensubtitles-remote', 'MCP Clients (Streamable)': 'Connect to /message endpoint for Streamable HTTP transport' } }); }); // Debug endpoint for testing JSON responses app.get('/debug', (req, res) => { const userAgent = req.get('User-Agent') || ''; const testData = { jsonrpc: '2.0', id: 999, result: { message: 'Debug test response', userAgent: userAgent, timestamp: new Date().toISOString(), isN8nDetected: userAgent.includes('node') || userAgent.includes('n8n') } }; // Force plain JSON for this debug endpoint streamResponse(res, testData, 200, true); }); // HTTP Proxy endpoint for direct tool calls app.post('/proxy', async (req, res) => { try { console.error('PROXY: Incoming POST request to /proxy endpoint'); const { tool, arguments: args } = req.body; if (!tool) { return res.status(400).json({ error: 'Missing tool parameter', usage: 'POST {"tool": "search_subtitles", "arguments": {...}}' }); } console.error('PROXY: Tool call:', tool, 'with args:', args); // Create OpenSubtitles server to handle the tool call const openSubtitlesServer = createOpenSubtitlesServer(); // Call the tool using the same pattern as the MCP tools/call method const result = await openSubtitlesServer.handleToolCall({ name: tool, arguments: args || {} }); console.error('PROXY: Tool result:', result); // Return the result directly as JSON res.json(result); } catch (error) { console.error('PROXY: Error in /proxy endpoint:', error); res.status(500).json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Force JSON endpoint for n8n debugging app.all('/json', express.raw({ type: '*/*' }), async (req, res) => { try { console.error('JSON: Incoming request, method:', req.method); if (req.method === 'POST') { console.error('JSON: Handling POST request - forcing plain JSON'); // Force plain JSON response const backup = res.req; res.req = { get: (header) => header === 'User-Agent' ? 'n8n-force-json' : undefined, headers: { 'user-agent': 'n8n-force-json' } }; await handleJsonRpcRequest(req, res); res.req = backup; return; } res.json({ status: 'JSON endpoint active', timestamp: new Date().toISOString() }); } catch (error) { console.error('JSON: Error:', error); res.status(500).json({ error: 'Internal error' }); } }); // Streamable HTTP endpoint for MCP (modern) - Direct implementation like WooCommerce // Use raw middleware to avoid JSON parsing interference app.all('/message', express.raw({ type: '*/*' }), async (req, res) => { try { console.error('STREAMABLE: Incoming request to /message endpoint, method:', req.method); // Handle CORS preflight if (req.method === 'OPTIONS') { res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Accept, MCP-Session-Id, MCP-Protocol-Version, Authorization, Api-Key, X-Requested-With', 'Access-Control-Allow-Credentials': 'false', 'Access-Control-Max-Age': '86400' }); res.status(204).end(); return; } // Handle GET requests - health check with streaming if (req.method === 'GET') { console.error('STREAMABLE: Handling GET request'); const userAgent = req.get('User-Agent') || ''; console.error('STREAMABLE: User-Agent:', userAgent); streamResponse(res, { jsonrpc: '2.0', id: null, result: { status: 'ok', transport: 'streamable-http', endpoint: '/message', service: 'opensubtitles-mcp-server', version: serverVersion, headers: req.headers // Debug info } }); return; } // Handle HEAD requests - allow simple connectivity checks if (req.method === 'HEAD') { res.set({ 'Access-Control-Allow-Origin': '*', 'MCP-Protocol-Version': '2025-06-18', 'X-Transport-Type': 'streamable-http' }); res.status(204).end(); return; } // Handle POST requests - JSON-RPC MCP requests if (req.method === 'POST') { console.error('STREAMABLE: Handling POST request'); const userAgent = req.get('User-Agent') || ''; const acceptHeader = req.get('Accept') || ''; console.error('STREAMABLE: User-Agent:', userAgent); console.error('STREAMABLE: Accept header:', acceptHeader); await handleJsonRpcRequest(req, res); return; } // Method not allowed console.error('STREAMABLE: Method not allowed:', req.method); streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32601, message: `Method ${req.method} not allowed` } }, 405); } catch (error) { console.error('STREAMABLE: Error in /message endpoint:', error); if (!res.headersSent) { streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' } }, 500); } } }); // Alias route to match Woo MCP endpoint shape for compatibility with clients app.all('/wp-json/wp/v2/wpmcp/streamable', express.raw({ type: '*/*' }), async (req, res) => { try { console.error('STREAMABLE (alias): Incoming request to /wp-json/wp/v2/wpmcp/streamable, method:', req.method); // CORS preflight if (req.method === 'OPTIONS') { res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Accept, MCP-Session-Id, MCP-Protocol-Version, Authorization, Api-Key, X-Requested-With', 'Access-Control-Allow-Credentials': 'false', 'Access-Control-Max-Age': '86400' }); res.status(204).end(); return; } // Health-check style GET if (req.method === 'GET') { const ua = req.get('User-Agent') || ''; console.error('STREAMABLE (alias): Handling GET; UA:', ua); streamResponse(res, { jsonrpc: '2.0', id: null, result: { status: 'ok', transport: 'streamable-http', endpoint: '/wp-json/wp/v2/wpmcp/streamable', service: 'opensubtitles-mcp-server', version: serverVersion, headers: req.headers } }); return; } // HEAD probe if (req.method === 'HEAD') { res.set({ 'Access-Control-Allow-Origin': '*', 'MCP-Protocol-Version': '2025-06-18', 'X-Transport-Type': 'streamable-http' }); res.status(204).end(); return; } // POST JSON-RPC if (req.method === 'POST') { const ua = req.get('User-Agent') || ''; const accept = req.get('Accept') || ''; console.error('STREAMABLE (alias): Handling POST; UA:', ua); console.error('STREAMABLE (alias): Accept header:', accept); await handleJsonRpcRequest(req, res); return; } // Not allowed console.error('STREAMABLE (alias): Method not allowed:', req.method); streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32601, message: `Method ${req.method} not allowed` } }, 405); } catch (error) { console.error('STREAMABLE (alias): Error in endpoint:', error); if (!res.headersSent) { streamResponse(res, { jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' } }, 500); } } }); app.listen(port, () => { console.log(`OpenSubtitles MCP server running on http://localhost:${port}`); console.log(`Health check: http://localhost:${port}/health`); console.log(`MCP SSE endpoint: http://localhost:${port}/sse`); console.log(`MCP Streamable endpoint: http://localhost:${port}/message`); console.log(`HTTP Proxy endpoint: http://localhost:${port}/proxy`); console.log(`Web interface: http://localhost:${port}/web`); }); } async function main() { console.error("MAIN: Starting main function"); console.error("MAIN: Process args:", process.argv); console.error("MAIN: Environment MCP_MODE:", process.env.MCP_MODE); console.error("MAIN: Environment MCP_TEST_MODE:", process.env.MCP_TEST_MODE); console.error("MAIN: stdin.isTTY:", process.stdin.isTTY); const mode = process.env.MCP_MODE || (process.stdin.isTTY ? 'http' : 'stdio'); const isTestMode = process.env.MCP_TEST_MODE === 'true'; console.error("MAIN: Selected mode:", mode); console.error("MAIN: Test mode:", isTestMode); if (mode === 'http') { console.error("MAIN: Running HTTP mode"); await runHttpMode(); } else { console.error("MAIN: Running STDIO mode"); await runStdioMode(); } // Keep the process alive with multiple approaches for stdio mode if (mode === 'stdio') { console.error("MAIN: Setting up keep-alive mechanisms for stdio mode"); // Signal handlers process.on('SIGINT', () => { console.error("MAIN: Received SIGINT, shutting down gracefully"); process.exit(0); }); process.on('SIGTERM', () => { console.error("MAIN: Received SIGTERM, shutting down gracefully"); process.exit(0); }); // Prevent event loop from exiting const keepAlive = setInterval(() => { // Do nothing, just keep event loop alive }, 60000); // Every minute // Explicitly keep process running process.on('beforeExit', (code) => { console.error("MAIN: Process about to exit with code:", code); console.error("MAIN: Keeping process alive"); }); console.error("MAIN: Keep-alive mechanisms set up"); } } // Always call main - don't rely on ES module entry point check console.error("OPENSUBTITLES-MCP: Starting main function regardless of entry point check"); main().catch((error) => { console.error("Server error:", error); process.exit(1); }); //# sourceMappingURL=index.js.map

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/opensubtitles/mcp.opensubtitles.com'

If you have feedback or need assistance with the MCP directory API, please join our Discord server