@codewithdan/dc-comics-mcp
Version:
DC Comics APIs MCP Server using Comic Vine API
188 lines (187 loc) • 7.89 kB
JavaScript
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(),
};
}
}