mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 22:13:39 +09:00
fix(mcp): support streamable http session cleanup
This commit is contained in:
1 parent
d34f3f7875
commit
b905c09f42
2 files changed
+54
-8
No files matched your search
@@ -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 = [];
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user