commit 59d699b5577f1b85e6c43374548c8fc4d51a0cb6 Author: Emanuel Almeida Date: Wed Mar 4 19:59:19 2026 +0000 Initial commit: SSE wrapper for MCP stdio servers - sse-wrapper.js: converte qualquer MCP stdio em SSE server - .gitignore: Node.js standard - README.txt: documentacao basica diff --git a/.desk-project b/.desk-project new file mode 100644 index 0000000..9d34101 --- /dev/null +++ b/.desk-project @@ -0,0 +1,4 @@ +project=mcp-sse-wrapper +description=SSE wrapper for MCP stdio servers +type=mcp-server +language=nodejs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7253965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +build/ +.env +.env.* +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.claude-logs/ +coverage/ diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..83ee74f --- /dev/null +++ b/README.txt @@ -0,0 +1,10 @@ +# MCP SSE Wrapper + +Desk Task: #1545 +Project: #65 (StackWorkflow) + +URL: https://desk.descomplicar.pt/admin/tasks/view/1545 + +--- +Path: /home/ealmeida/mcp-servers/mcp-sse-wrapper +Updated: 2026-02-02 diff --git a/sse-wrapper.js b/sse-wrapper.js new file mode 100644 index 0000000..ddf53d8 --- /dev/null +++ b/sse-wrapper.js @@ -0,0 +1,223 @@ +#!/usr/bin/env node +/** + * Generic SSE Wrapper for MCP Servers + * Converts any stdio-based MCP to SSE server + * + * Usage: node sse-wrapper.js --port 3101 --name "my-mcp" -- node /path/to/mcp.js + * + * @author Descomplicar | @link descomplicar.pt | @copyright 2026 + */ + +const http = require('http'); +const { spawn } = require('child_process'); +const { URL } = require('url'); +const { randomUUID } = require('crypto'); + +// Parse arguments +const args = process.argv.slice(2); +let port = 3101; +let name = 'mcp-server'; +let mcpCommand = []; +let mcpEnv = { ...process.env }; + +// Parse our arguments +let i = 0; +while (i < args.length) { + if (args[i] === '--port' && args[i + 1]) { + port = parseInt(args[i + 1]); + i += 2; + } else if (args[i] === '--name' && args[i + 1]) { + name = args[i + 1]; + i += 2; + } else if (args[i] === '--env' && args[i + 1]) { + const [key, value] = args[i + 1].split('='); + mcpEnv[key] = value; + i += 2; + } else if (args[i] === '--') { + mcpCommand = args.slice(i + 1); + break; + } else { + i++; + } +} + +if (mcpCommand.length === 0) { + console.error('Usage: node sse-wrapper.js --port 3101 --name "my-mcp" -- [args...]'); + process.exit(1); +} + +const HOST = '127.0.0.1'; + +// Track active sessions +const sessions = new Map(); + +// Create a new MCP process for a session +function createMCPProcess(sessionId) { + const [cmd, ...cmdArgs] = mcpCommand; + + const mcpProcess = spawn(cmd, cmdArgs, { + stdio: ['pipe', 'pipe', 'pipe'], + env: mcpEnv, + cwd: mcpEnv.MCP_WORKING_DIR || process.cwd() + }); + + mcpProcess.stderr.on('data', (data) => { + // Log stderr but don't send to client (it's usually debug info) + if (process.env.DEBUG) { + console.error(`[${name}:${sessionId.slice(0, 8)}] stderr:`, data.toString()); + } + }); + + return mcpProcess; +} + +// HTTP Server +const server = http.createServer((req, res) => { + // CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + const url = new URL(req.url || '/', `http://${HOST}:${port}`); + + // Health check + if (url.pathname === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + status: 'ok', + name: name, + sessions: sessions.size, + port: port + })); + return; + } + + // SSE endpoint - new session + if (url.pathname === '/sse' && req.method === 'GET') { + const sessionId = randomUUID(); + + // Set SSE headers + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'Connection': 'keep-alive' + }); + + // Create MCP process for this session + const mcpProcess = createMCPProcess(sessionId); + + // Store session + sessions.set(sessionId, { res, mcpProcess, buffer: '' }); + + // Send endpoint event + res.write(`event: endpoint\ndata: /message?sessionId=${sessionId}\n\n`); + + // Handle MCP stdout -> SSE + mcpProcess.stdout.on('data', (data) => { + const session = sessions.get(sessionId); + if (!session) return; + + session.buffer += data.toString(); + + // Process complete JSON messages + const lines = session.buffer.split('\n'); + session.buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.trim()) { + try { + JSON.parse(line); // Validate JSON + res.write(`event: message\ndata: ${line}\n\n`); + } catch (e) { + // Not valid JSON, skip + } + } + } + }); + + // Handle MCP process exit + mcpProcess.on('close', (code) => { + console.log(`[${name}:${sessionId.slice(0, 8)}] MCP process exited with code ${code}`); + sessions.delete(sessionId); + res.end(); + }); + + // Handle client disconnect + req.on('close', () => { + console.log(`[${name}] Session closed: ${sessionId.slice(0, 8)}`); + const session = sessions.get(sessionId); + if (session) { + session.mcpProcess.kill(); + sessions.delete(sessionId); + } + }); + + return; + } + + // Message endpoint - POST messages to MCP + 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' })); + return; + } + + const session = sessions.get(sessionId); + if (!session) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Session not found' })); + return; + } + + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + try { + // Send to MCP stdin + session.mcpProcess.stdin.write(body + '\n'); + res.writeHead(202, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'accepted' })); + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: error.message })); + } + }); + return; + } + + // 404 + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Not found' })); +}); + +server.listen(port, HOST, () => { + console.log(`[${name}] SSE wrapper running at http://${HOST}:${port}/sse`); + console.log(`[${name}] Wrapping command: ${mcpCommand.join(' ')}`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log(`[${name}] SIGTERM received, shutting down...`); + // Kill all MCP processes + for (const [sessionId, session] of sessions) { + session.mcpProcess.kill(); + } + server.close(() => process.exit(0)); +}); + +process.on('SIGINT', () => { + console.log(`[${name}] SIGINT received, shutting down...`); + for (const [sessionId, session] of sessions) { + session.mcpProcess.kill(); + } + server.close(() => process.exit(0)); +});