UNPKG

@codewithdan/dc-comics-mcp

Version:

DC Comics APIs MCP Server using Comic Vine API

188 lines (187 loc) 7.89 kB
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { randomUUID } from 'node:crypto'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { dcComicsTools } from './tools/tools.js'; const JSON_RPC = '2.0'; const JSON_RPC_ERROR = -32603; export class StreamableHTTPServer { mcpServer; logger; // Map to store transports by session ID transports = {}; constructor(mcpServer, logger) { this.mcpServer = mcpServer; this.logger = logger; this.setupServerRequestHandlers(); } setupServerRequestHandlers() { // Handle listing all available tools this.mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { const tools = Object.entries(dcComicsTools).map(([name, tool]) => ({ name, description: tool.description, inputSchema: zodToJsonSchema(tool.schema), })); this.logger.info(`🧰 Listing ${tools.length} tools`); return { jsonrpc: JSON_RPC, tools }; }); // Handle tool execution requests this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { const name = request.params.name; const args = request.params.arguments; this.logger.info(`🔧 Handling tool call: ${name}`); if (!(name in dcComicsTools)) { this.logger.error(`❓ Tool ${name} not found`); throw new Error(`Tool not found: ${name}`); } const tool = dcComicsTools[name]; try { const result = await tool.handler(args); this.logger.info(`🚀 Tool ${name} executed successfully`); return { jsonrpc: JSON_RPC, content: [{ type: 'text', text: JSON.stringify(result) }], }; } catch (error) { if (error instanceof Error) { this.logger.error(`💥 Error processing ${name}: ${error.message}`); throw new Error(`Error processing ${name}: ${error.message}`); } throw error; } }); } async handleGetRequest(req, res) { res.status(405).json(this.createRPCErrorResponse('Method not allowed.')); this.logger.info('🚫 Responded to GET with 405 Method Not Allowed'); } async handlePostRequest(req, res) { this.logger.info(`📩 POST ${req.originalUrl} - payload received`); try { // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && this.transports[sessionId]) { // Reuse existing transport transport = this.transports[sessionId]; this.logger.info(`🔄 Reusing existing session: ${sessionId}`); } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID this.transports[sessionId] = transport; this.logger.info(`🆕 New session initialized: ${sessionId}`); } }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { delete this.transports[transport.sessionId]; this.logger.info(`🗑️ Session removed: ${transport.sessionId}`); } }; // Connect transport to the MCP server this.logger.info('🔄 Connecting transport to server...'); await this.mcpServer.connect(transport); this.logger.info('🔗 Transport connected successfully'); } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); this.logger.error('❌ Invalid request: No valid session ID provided'); return; } // Handle the request await transport.handleRequest(req, res, req.body); this.logger.info(`✅ POST request handled successfully (status=${res.statusCode})`); } catch (error) { this.logger.error('💥 Error handling MCP request:', error); if (!res.headersSent) { res .status(500) .json(this.createRPCErrorResponse('Internal server error.')); this.logger.error('🔥 Responded with 500 Internal Server Error'); } } } // Handle DELETE requests for session termination async handleDeleteRequest(req, res) { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !this.transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); this.logger.info('🚫 DELETE request rejected: Invalid or missing session ID'); return; } const transport = this.transports[sessionId]; try { transport.close(); delete this.transports[sessionId]; res.status(200).send('Session terminated successfully'); this.logger.info(`🔒 Session ${sessionId} terminated successfully`); } catch (error) { this.logger.error(`💥 Error terminating session ${sessionId}:`, error); res.status(500).send('Error terminating session'); } } async close() { this.logger.info('🛑 Shutting down server...'); // Close all active transports for (const transport of Object.values(this.transports)) { try { transport.close(); this.logger.info(`🗑️ Transport closed for session ID: ${transport.sessionId}`); } catch (error) { this.logger.error('💥 Error closing transport:', error); } } await this.mcpServer.close(); this.logger.info('👋 Server shutdown complete.'); } async sendMessages(transport) { const message = { method: 'notifications/message', params: { level: 'info', data: 'SSE Connection established' }, }; this.logger.info('📬 Sending SSE connection established notification.'); this.sendNotification(transport, message); this.logger.info('✅ Notification sent successfully.'); } sendNotification(transport, notification) { const rpcNotification = { ...notification, jsonrpc: JSON_RPC, }; this.logger.info(`📢 Sending notification: ${notification.method}`); transport.send(rpcNotification).catch(error => { this.logger.error('📛 Error sending notification:', error); }); } createRPCErrorResponse(message) { return { jsonrpc: JSON_RPC, error: { code: JSON_RPC_ERROR, message: message, }, id: randomUUID(), }; } }