fix: add /sse + /messages endpoints for Figma compatibility

This commit is contained in:
hermes committed 2026-07-09 19:51:35 +02:00
1 parent b10d3581aa
commit 24eff3a35a
1 file changed
+49 -8
+49 -8
View File
@@ -29,6 +29,7 @@ import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
@@ -594,17 +595,25 @@ function errResp(text) {
}
// ---------- HTTP transport w/ Bearer auth ----------
const transport = new StreamableHTTPServerTransport({
// Traditional SSE transport sessions (for Figma / older MCP clients)
const sseSessions = new Map();
// Stateful Streamable HTTP transport (for Cursor / modern clients)
const mcpTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);
await server.connect(mcpTransport);
const httpServer = http.createServer(async (req, res) => {
if (!req.url.startsWith('/health')) {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
// Auth gate — skip /health only
if (!url.pathname.startsWith('/health')) {
const auth = req.headers.authorization || '';
const expected = `Bearer ${MCP_TOKEN}`;
if (auth !== expected) {
process.stderr.write(`[auth] denied ${req.method} ${req.url}\n`);
process.stderr.write(`[auth] denied ${req.method} ${url.pathname}\n`);
res.statusCode = 401;
res.setHeader('www-authenticate', 'Bearer');
res.setHeader('content-type', 'application/json');
@@ -613,17 +622,49 @@ const httpServer = http.createServer(async (req, res) => {
}
}
if (req.url === '/health') {
// Lightweight /health for Coolify / Traefik probes
if (url.pathname === '/health') {
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ ok: true, version: '1.2.0', uptime: process.uptime() }));
return;
}
process.stderr.write(`[HTTP] ${req.method} ${req.url} -> incoming\n`);
process.stderr.write(`[HTTP] ${req.method} ${url.pathname} -> incoming\n`);
try {
await transport.handleRequest(req, res);
process.stderr.write(`[HTTP] ${req.method} ${req.url} -> ${res.statusCode}\n`);
// ---- Traditional SSE transport (for Figma) ----
if (url.pathname === '/sse' && req.method === 'GET') {
const sseTransport = new SSEServerTransport('/messages', res);
const sessionId = sseTransport.sessionId;
sseSessions.set(sessionId, sseTransport);
// Clean up on disconnect
res.on('close', () => {
sseSessions.delete(sessionId);
process.stderr.write(`[sse] session ${sessionId} closed\n`);
});
await server.connect(sseTransport);
process.stderr.write(`[sse] session ${sessionId} established\n`);
return;
}
if (url.pathname === '/messages' && req.method === 'POST') {
const sessionId = url.searchParams.get('sessionId');
if (!sessionId || !sseSessions.has(sessionId)) {
res.statusCode = 400;
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ error: `SSE session ${sessionId || '(missing)'} not found` }));
return;
}
const sseTransport = sseSessions.get(sessionId);
await sseTransport.handlePostMessage(req, res);
return;
}
// ---- Unified /mcp transport (Streamable HTTP) ----
// Must go last — catches any path that isn't /health /sse /messages
await mcpTransport.handleRequest(req, res);
process.stderr.write(`[mcp] ${req.method} ${url.pathname} -> ${res.statusCode}\n`);
} catch (e) {
process.stderr.write(`[HTTP-ERR] ${e.stack || e}\n`);
if (!res.headersSent) {