fix(mcp): support streamable http session cleanup

This commit is contained in:
djimit authored and Dutch Dim committed 2026-06-20 02:45:35 +02:00
1 parent d34f3f7875
commit b905c09f42
2 files changed
+54 -8

No files matched your search

+19 -4
View File
@@ -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 = [];
+35 -4
View File
@@ -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();