diff --git a/ruflo/src/mcp-bridge/index.js b/ruflo/src/mcp-bridge/index.js index 3c43c680a5..dce1723a11 100644 --- a/ruflo/src/mcp-bridge/index.js +++ b/ruflo/src/mcp-bridge/index.js @@ -835,10 +835,22 @@ const app = express(); app.use(express.json({ limit: "10mb" })); // ---------- CORS middleware ---------- + + +// ---------- MCP Streamable HTTP session ---------- +const MCP_SESSION_ID = randomUUID(); + +// ---------- Mcp-Session-Id middleware ---------- +app.use((req, res, next) => { + if (req.path.startsWith("/mcp")) { + res.setHeader("Mcp-Session-Id", MCP_SESSION_ID); + } + next(); +}); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id"); if (req.method === "OPTIONS") return res.sendStatus(204); next(); }); @@ -872,7 +884,7 @@ function createMcpHandler(groupName) { return res.json({ jsonrpc: "2.0", id, result: mcpResult }); } case "notifications/initialized": - return res.json({ jsonrpc: "2.0", id, result: {} }); + return res.status(202).end(); default: return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } }); } @@ -896,6 +908,7 @@ function createMcpSseHandler(groupName) { for (const groupName of Object.keys(TOOL_GROUPS)) { app.post(`/mcp/${groupName}`, createMcpHandler(groupName)); app.get(`/mcp/${groupName}`, createMcpSseHandler(groupName)); + app.delete(`/mcp/${groupName}`, (_, res) => res.sendStatus(204)); } // ---------- Catch-all /mcp — serves ALL enabled tools (backwards-compatible) ---------- @@ -926,7 +939,7 @@ app.post("/mcp", async (req, res) => { return res.json({ jsonrpc: "2.0", id, result: mcpResult }); } case "notifications/initialized": - return res.json({ jsonrpc: "2.0", id, result: {} }); + return res.status(202).end(); default: return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } }); } @@ -943,6 +956,8 @@ app.get("/mcp", (req, res) => { res.write(`data: ${JSON.stringify({ type: "endpoint", url: "/mcp" })}\n\n`); }); +app.delete("/mcp", (_, res) => res.sendStatus(204)); + // ---------- GET /mcp-servers — returns MCP_SERVERS JSON for Chat UI config ---------- app.get("/mcp-servers", (_, res) => { const servers = []; diff --git a/ruflo/src/mcp-bridge/test-harness.js b/ruflo/src/mcp-bridge/test-harness.js index efd46eecf2..91adaf4a1b 100644 --- a/ruflo/src/mcp-bridge/test-harness.js +++ b/ruflo/src/mcp-bridge/test-harness.js @@ -395,6 +395,26 @@ async function testSSE() { }); } +async function testSessionCleanup() { + console.log("\n── Session Cleanup ──"); + + await test("DELETE /mcp returns 204", async () => { + const res = await fetch(`${BASE}/mcp`, { + method: "DELETE", + headers: { "Mcp-Session-Id": "test-cleanup" }, + }); + assert(res.status === 204, `status ${res.status}`); + }); + + await test("DELETE /mcp/core returns 204", async () => { + const res = await fetch(`${BASE}/mcp/core`, { + method: "DELETE", + headers: { "Mcp-Session-Id": "test-cleanup" }, + }); + assert(res.status === 204, `status ${res.status}`); + }); +} + async function testModels() { console.log("\n── Models Endpoint ──"); @@ -410,13 +430,23 @@ async function testNotificationsInitialized() { console.log("\n── Notifications ──"); await test("notifications/initialized via /mcp", async () => { - const res = await mcpCall("/mcp", "notifications/initialized", {}); - assert(res.result, "no result"); + const res = await fetch(`${BASE}/mcp`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + }); + assert(res.status === 202, `status ${res.status}`); + assert((await res.text()) === "", "expected empty body"); }); await test("notifications/initialized via /mcp/core", async () => { - const res = await mcpCall("/mcp/core", "notifications/initialized", {}); - assert(res.result, "no result"); + const res = await fetch(`${BASE}/mcp/core`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + }); + assert(res.status === 202, `status ${res.status}`); + assert((await res.text()) === "", "expected empty body"); }); } @@ -449,6 +479,7 @@ async function main() { await testToolExecution(); await testCrossGroupExecution(); await testSSE(); + await testSessionCleanup(); await testModels(); await testNotificationsInitialized();