UNPKG

puppeteer-vision-mcp-server

Version:

MCP Server for scraping webpages and converting to markdown

278 lines (277 loc) 11.2 kB
import express from 'express'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { randomUUID } from 'crypto'; import { config } from '../config.js'; /** * Sets up the appropriate transport for the MCP server * @param server The MCP server instance * @param serverName Server name for logging * @param serverVersion Server version for logging */ export function setupTransport(server, serverName, serverVersion) { switch (config.transportType) { case 'stdio': setupStdioTransport(server, serverName, serverVersion); break; case 'sse': setupSSETransport(server, serverName, serverVersion); break; case 'http': setupHTTPTransport(server, serverName, serverVersion); break; default: console.error(`Unknown transport type: ${config.transportType}. Use 'stdio', 'sse', or 'http'.`); process.exit(1); } } /** * Sets up an SSE transport over HTTP * @param server The MCP server instance * @param serverName Server name for logging * @param serverVersion Server version for logging */ function setupSSETransport(server, serverName, serverVersion) { // Setup Express server for SSE mode const app = express(); // to support multiple simultaneous connections we have a lookup object from // sessionId to transport const transports = {}; app.get("/sse", async (_, res) => { console.error('Received SSE connection request'); const transport = new SSEServerTransport('/messages', res); transports[transport.sessionId] = transport; res.on("close", () => { console.error(`SSE connection closed for session ${transport.sessionId}`); delete transports[transport.sessionId]; }); await server.connect(transport); }); app.post("/messages", async (req, res) => { console.error('Received SSE message POST request'); const sessionId = req.query.sessionId; const transport = transports[sessionId]; if (transport) { await transport.handlePostMessage(req, res); } else { console.error(`No SSE transport found for sessionId: ${sessionId}`); res.status(400).send('No transport found for sessionId'); } }); const webserver = app.listen(config.serverPort, () => { console.error(`${serverName} v${serverVersion} is running on port ${config.serverPort} with SSE transport`); console.error(`Connect to: http://localhost:${config.serverPort}/sse`); if (config.apiBaseUrl) { console.error(`Using custom API endpoint: ${config.apiBaseUrl}`); } }); webserver.keepAliveTimeout = 3000; // Keep the process alive webserver.on('error', (error) => { console.error('HTTP server error:', error); }); // Handle server shutdown process.on('SIGINT', async () => { console.error('Shutting down SSE server...'); // Close all active SSE transports for (const [sessionId, transport] of Object.entries(transports)) { try { console.error(`Closing SSE transport for session ${sessionId}`); // SSE transports typically don't have a close method, cleanup happens via res.on("close") delete transports[sessionId]; } catch (error) { console.error(`Error cleaning up SSE transport for session ${sessionId}:`, error); } } console.error('SSE server shutdown complete'); process.exit(0); }); // Prevent the process from exiting process.stdin.resume(); } /** * Sets up an HTTP transport for web-based communication * @param server The MCP server instance * @param serverName Server name for logging * @param serverVersion Server version for logging */ function setupHTTPTransport(server, serverName, serverVersion) { console.error("Starting MCP server with HTTP transport..."); const app = express(); const transports = new Map(); // Handle POST requests for MCP initialization and method calls app.post('/mcp', async (req, res) => { console.error('Received MCP POST request'); try { // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports.has(sessionId)) { // Reuse existing transport transport = transports.get(sessionId); } else if (!sessionId) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID when session is initialized console.error(`Session initialized with ID: ${sessionId}`); transports.set(sessionId, transport); } }); // Set up onclose handler to clean up transport when closed transport.onclose = async () => { const sid = transport.sessionId; if (sid && transports.has(sid)) { console.error(`Transport closed for session ${sid}, removing from transports map`); transports.delete(sid); } }; // Connect the transport to the MCP server BEFORE handling the request await server.connect(transport); await transport.handleRequest(req, res); return; // Already handled } else { // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: req?.body?.id, }); return; } // Handle the request with existing transport await transport.handleRequest(req, res); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: req?.body?.id, }); } } }); // Handle GET requests for SSE streams app.get('/mcp', async (req, res) => { console.error('Received MCP GET request'); const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports.has(sessionId)) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: req?.body?.id, }); return; } // Check for Last-Event-ID header for resumability const lastEventId = req.headers['last-event-id']; if (lastEventId) { console.error(`Client reconnecting with Last-Event-ID: ${lastEventId}`); } else { console.error(`Establishing new SSE stream for session ${sessionId}`); } const transport = transports.get(sessionId); await transport.handleRequest(req, res); }); // Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports.has(sessionId)) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: req?.body?.id, }); return; } console.error(`Received session termination request for session ${sessionId}`); try { const transport = transports.get(sessionId); await transport.handleRequest(req, res); } catch (error) { console.error('Error handling session termination:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Error handling session termination', }, id: req?.body?.id, }); } } }); const webserver = app.listen(config.serverPort, () => { console.error(`${serverName} v${serverVersion} is running on port ${config.serverPort} with HTTP transport`); console.error(`Connect to: http://localhost:${config.serverPort}/mcp`); if (config.apiBaseUrl) { console.error(`Using custom API endpoint: ${config.apiBaseUrl}`); } }); webserver.keepAliveTimeout = 3000; // Keep the process alive webserver.on('error', (error) => { console.error('HTTP server error:', error); }); // Handle server shutdown process.on('SIGINT', async () => { console.error('Shutting down server...'); // Close all active transports to properly clean up resources for (const [sessionId, transport] of transports) { try { console.error(`Closing transport for session ${sessionId}`); await transport.close(); transports.delete(sessionId); } catch (error) { console.error(`Error closing transport for session ${sessionId}:`, error); } } console.error('Server shutdown complete'); process.exit(0); }); // Prevent the process from exiting process.stdin.resume(); } /** * Sets up a stdio transport for command-line usage * @param server The MCP server instance * @param serverName Server name for logging * @param serverVersion Server version for logging */ function setupStdioTransport(server, serverName, serverVersion) { // Use Stdio transport (default mode) const stdioTransport = new StdioServerTransport(); console.log(`${serverName} v${serverVersion} starting in stdio mode`); if (config.apiBaseUrl) { console.log(`Using custom API endpoint: ${config.apiBaseUrl}`); } // Connect the transport to the server server.connect(stdioTransport).catch((error) => { console.error("Error connecting transport:", error); process.exit(1); }); }