#!/usr/bin/env node /** * MCP Paperclip - HTTP Server Mode (Streamable HTTP + SSE) * Dual transport: StreamableHTTP (primary) + SSE (legacy compatibility) * * @author Descomplicar | @link descomplicar.pt | @copyright 2026 */ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import * as dotenv from 'dotenv'; import * as http from 'http'; import { randomUUID } from 'crypto'; import { createServer } from './server.js'; import { allTools } from './tools/index.js'; import { logger } from './utils/logger.js'; dotenv.config(); const PORT = parseInt(process.env.HTTP_PORT || '3175'); const HOST = process.env.HTTP_HOST || '127.0.0.1'; // Track active StreamableHTTP sessions const sessions = new Map(); // Track SSE sessions const sseSessions = new Map(); async function main() { const httpServer = http.createServer(async (req, res) => { // CORS headers on all responses res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id'); res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = new URL(req.url || '/', `http://${HOST}:${PORT}`); // Health check endpoint if (url.pathname === '/health' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ status: 'ok', transport: 'http+sse', sessions: sessions.size, sse_sessions: sseSessions.size, tools: allTools.length, }) ); return; } // StreamableHTTP endpoint — POST/DELETE if (url.pathname === '/mcp') { const sessionId = req.headers['mcp-session-id'] as string | undefined; // Route to existing session if (sessionId && sessions.has(sessionId)) { const session = sessions.get(sessionId)!; await session.transport.handleRequest(req, res); return; } // DELETE — session termination if (req.method === 'DELETE' && sessionId) { const session = sessions.get(sessionId); if (session) { await session.transport.close(); sessions.delete(sessionId); res.writeHead(200); res.end(); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); } return; } // POST — new session if (req.method === 'POST') { const server = createServer(allTools); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true, onsessioninitialized: (newSessionId) => { sessions.set(newSessionId, { transport }); logger.info(`HTTP session initialized: ${newSessionId}`); }, }); transport.onclose = () => { if (transport.sessionId) { logger.info(`HTTP session closed: ${transport.sessionId}`); sessions.delete(transport.sessionId); } }; await server.connect(transport); await transport.handleRequest(req, res); return; } res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Method not allowed' })); return; } // SSE endpoint — GET (legacy transport) if (url.pathname === '/sse' && req.method === 'GET') { const server = createServer(allTools); const transport = new SSEServerTransport('/message', res); sseSessions.set(transport.sessionId, transport); logger.info(`SSE session started: ${transport.sessionId}`); transport.onclose = () => { logger.info(`SSE session closed: ${transport.sessionId}`); sseSessions.delete(transport.sessionId); }; await server.connect(transport); return; } // SSE message endpoint — POST if (url.pathname === '/message' && req.method === 'POST') { const sessionId = url.searchParams.get('sessionId'); if (!sessionId) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing sessionId query param' })); return; } const transport = sseSessions.get(sessionId); if (!transport) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'SSE session not found' })); return; } await transport.handlePostMessage(req, res); return; } // 404 for all other paths res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found', hint: 'Use /mcp (StreamableHTTP) or /sse (SSE)' })); }); httpServer.listen(PORT, HOST, () => { logger.info('MCP Paperclip HTTP Server started', { host: HOST, port: PORT, tools: allTools.length, endpoints: { health: `http://${HOST}:${PORT}/health`, mcp: `http://${HOST}:${PORT}/mcp`, sse: `http://${HOST}:${PORT}/sse`, }, }); console.log(`MCP Paperclip HTTP Server running at http://${HOST}:${PORT}/mcp`); }); // Graceful shutdown const shutdown = (signal: string) => { logger.info(`${signal} received, shutting down...`); for (const session of sessions.values()) { session.transport.close(); } sessions.clear(); for (const transport of sseSessions.values()) { transport.close(); } sseSessions.clear(); httpServer.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } main().catch((error) => { logger.error('Fatal error', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); process.exit(1); });