import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const srcPath = resolve(__dirname, "../../open-sse/mcp-server/httpTransport.ts"); const src = readFileSync(srcPath, "utf-8"); const mod = await import("../../open-sse/mcp-server/httpTransport.ts"); // ── Module exports ─────────────────────────────────────────────────────────── test("module exports handleMcpStreamableHTTP", () => { assert.equal(typeof mod.handleMcpStreamableHTTP, "function"); }); test("module exports handleMcpSSE", () => { assert.equal(typeof mod.handleMcpSSE, "function"); }); test("module exports getMcpHttpStatus", () => { assert.equal(typeof mod.getMcpHttpStatus, "function"); }); test("module exports shutdownMcpHttp", () => { assert.equal(typeof mod.shutdownMcpHttp, "function"); }); test("module exports isMcpHttpTransportReady", () => { assert.equal(typeof mod.isMcpHttpTransportReady, "function"); }); test("module exports isMcpHttpActive", () => { assert.equal(typeof mod.isMcpHttpActive, "function"); }); // ── Source-level invariant: StreamableSession type has lastActivityAt ───────── test("StreamableSession type includes lastActivityAt field", () => { const typeBlock = src.match(/type StreamableSession\s*=\s*\{([^}]+)\}/); assert.ok(typeBlock, "StreamableSession type definition must exist"); assert.ok( typeBlock[1].includes("lastActivityAt"), "StreamableSession must have lastActivityAt field" ); }); // ── Source-level invariant: sweep uses lastActivityAt not startedAt ─────────── test("sweep interval compares against lastActivityAt, not startedAt", () => { const sweepBlock = src.match( /_mcpSessionSweep\s*=\s*setInterval\(\(\)\s*=>\s*\{([\s\S]*?)\},\s*60_000\)/ ); assert.ok(sweepBlock, "sweep interval block must exist"); assert.ok( sweepBlock[1].includes("session.lastActivityAt"), "sweep must check session.lastActivityAt" ); assert.ok(!sweepBlock[1].includes("session.startedAt"), "sweep must NOT check session.startedAt"); }); // ── Source-level invariant: MCP_SESSION_IDLE_MS constant ───────────────────── test("MCP_SESSION_IDLE_MS is 5 minutes (5 * 60 * 1000)", () => { assert.ok(src.includes("5 * 60 * 1000"), "idle timeout should be 5 * 60 * 1000"); }); // ── Source-level invariant: createStreamableSession sets lastActivityAt ─────── test("createStreamableSession initializes lastActivityAt to Date.now()", () => { const fnBlock = src.match(/function createStreamableSession\(\)[\s\S]*?return session;\s*\}/); assert.ok(fnBlock, "createStreamableSession function must exist"); assert.ok( fnBlock[0].includes("lastActivityAt: Date.now()"), "createStreamableSession must set lastActivityAt: Date.now()" ); }); // ── Source-level invariant: handleStreamableRequest updates lastActivityAt ──── test("handleStreamableRequest updates lastActivityAt on every request", () => { const fnBlock = src.match( /async function handleStreamableRequest[\s\S]*?(?=\n(?:async )?function |\nexport )/ ); assert.ok(fnBlock, "handleStreamableRequest function must exist"); assert.ok( fnBlock[0].includes("session.lastActivityAt = Date.now()"), "handleStreamableRequest must update session.lastActivityAt on each request" ); }); // ── Behavioral: getMcpHttpStatus returns expected shape when idle ───────────── test("getMcpHttpStatus returns active-session state with no active sessions", () => { mod.shutdownMcpHttp(); const status = mod.getMcpHttpStatus(); assert.equal(typeof status.online, "boolean"); assert.equal(status.online, false); assert.equal(status.transport, null); assert.equal(status.startedAt, null); assert.equal(status.uptime, null); }); test("isMcpHttpTransportReady treats enabled lazy HTTP transports as ready", () => { mod.shutdownMcpHttp(); const status = mod.getMcpHttpStatus(); assert.equal(status.online, false); assert.equal(mod.isMcpHttpTransportReady(true, "streamable-http"), true); assert.equal(mod.isMcpHttpTransportReady(true, "sse"), true); assert.equal(mod.isMcpHttpTransportReady(true, "stdio"), false); assert.equal(mod.isMcpHttpTransportReady(false, "streamable-http"), false); }); // ── Behavioral: isMcpHttpActive is false after shutdown ────────────────────── test("isMcpHttpActive returns false when no transports are active", () => { mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); }); // ── Behavioral: shutdownMcpHttp is idempotent ──────────────────────────────── test("shutdownMcpHttp can be called multiple times without error", () => { mod.shutdownMcpHttp(); mod.shutdownMcpHttp(); mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); }); // ── Source-level invariant: sweep interval is 60 seconds ───────────────────── test("sweep interval runs every 60 seconds", () => { assert.ok( src.includes("}, 60_000)") || src.includes("}, 60000)"), "sweep interval must be 60_000ms (60 seconds)" ); }); // ── Source-level invariant: sweep timer is unref'd so it doesn't block exit ─── test("sweep timer is unref'd to avoid preventing process exit", () => { assert.ok( src.includes("_mcpSessionSweep") && src.includes(".unref?.()"), "sweep timer must be unref'd" ); }); // ── Source-level invariant: sweep calls closeStreamableSession for idle ─────── test("sweep closes idle sessions via closeStreamableSession", () => { const sweepBlock = src.match( /_mcpSessionSweep\s*=\s*setInterval\(\(\)\s*=>\s*\{([\s\S]*?)\},\s*60_000\)/ ); assert.ok(sweepBlock, "sweep block must exist"); assert.ok( sweepBlock[1].includes("closeStreamableSession(sessionId)"), "sweep must call closeStreamableSession for idle sessions" ); }); // ── Behavioral: shutdownMcpHttp clears all sessions ───────────────────────── test("shutdownMcpHttp clears all sessions and makes isMcpHttpActive false", () => { mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); const before = mod.getMcpHttpStatus(); assert.equal(before.online, false); assert.equal(before.transport, null); }); // ── Behavioral: handleMcpStreamableHTTP rejects request without session id ─── test("handleMcpStreamableHTTP rejects non-initialize request without session id", async () => { mod.shutdownMcpHttp(); const req = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 1 }), }); const res = await mod.handleMcpStreamableHTTP(req); assert.equal(res.status, 400); const body = await res.json(); assert.ok(body.error); assert.ok(body.error.message.includes("Mcp-Session-Id")); }); // ── Behavioral: handleMcpStreamableHTTP creates session on initialize ──────── test("handleMcpStreamableHTTP creates a session on initialize request", async () => { mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); const initReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1, params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" }, }, }), }); const res = await mod.handleMcpStreamableHTTP(initReq); assert.ok(res.status >= 200, "should get a response"); if (res.headers.get("mcp-session-id")) { assert.equal(mod.isMcpHttpActive(), true); const status = mod.getMcpHttpStatus(); assert.equal(status.online, true); assert.equal(status.transport, "streamable-http"); assert.ok(status.startedAt !== null); mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); } }); // ── Behavioral: getMcpHttpStatus reflects transport state ──────────────────── test("getMcpHttpStatus returns streamable-http transport when session exists", async () => { mod.shutdownMcpHttp(); const initReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1, params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" }, }, }), }); const res = await mod.handleMcpStreamableHTTP(initReq); const sessionId = res.headers.get("mcp-session-id"); if (sessionId) { const status = mod.getMcpHttpStatus(); assert.equal(status.online, true); assert.equal(status.transport, "streamable-http"); assert.ok(typeof status.uptime === "string"); assert.ok(status.uptime.endsWith("s")); } mod.shutdownMcpHttp(); }); // ── Behavioral: shutdownMcpHttp cleans up sessions created via initialize ──── test("shutdownMcpHttp removes sessions created via handleMcpStreamableHTTP", async () => { mod.shutdownMcpHttp(); const initReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1, params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" }, }, }), }); const res = await mod.handleMcpStreamableHTTP(initReq); const sessionId = res.headers.get("mcp-session-id"); if (sessionId) { assert.equal(mod.isMcpHttpActive(), true); mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); const status = mod.getMcpHttpStatus(); assert.equal(status.online, false); assert.equal(status.transport, null); const staleReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json", "mcp-session-id": sessionId, }, body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 2 }), }); const staleRes = await mod.handleMcpStreamableHTTP(staleReq); // MCP spec (2025-03-26 / 2025-11-25, Session Management): a terminated/unknown // session id MUST return 404 Not Found so the client re-initializes (issue #5169). assert.equal(staleRes.status, 404); } }); // ── Behavioral: handleMcpStreamableHTTP rejects unknown session id ─────────── test("handleMcpStreamableHTTP rejects request with unknown session id", async () => { mod.shutdownMcpHttp(); const req = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json", "mcp-session-id": "nonexistent-session-id", }, body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 1 }), }); const res = await mod.handleMcpStreamableHTTP(req); // Per MCP spec, a present-but-unknown session id MUST yield 404 (not 400), so // the client knows to start a fresh session rather than hard-fail (issue #5169). assert.equal(res.status, 404); const body = await res.json(); assert.ok(body.error); assert.ok(body.error.message.includes("Unknown")); }); // ── Regression #5169: unknown/expired session → HTTP 404 (not 400) ─────────── test("handleMcpStreamableHTTP returns 404 (not 400) for an unknown session id", async () => { mod.shutdownMcpHttp(); const req = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json", "mcp-session-id": "expired-or-unknown-session", }, body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 7 }), }); const res = await mod.handleMcpStreamableHTTP(req); // The 400-vs-404 distinction is the whole bug: clients only re-initialize on 404. assert.equal(res.status, 404); const body = await res.json(); assert.equal(body.jsonrpc, "2.0"); assert.equal(body.error.code, -32000); assert.ok(body.error.message.includes("Mcp-Session-Id")); }); // ── Guard: a *missing* session id on a non-initialize request stays 400 ─────── test("handleMcpStreamableHTTP keeps 400 for a missing session id (non-initialize)", async () => { mod.shutdownMcpHttp(); const req = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 8 }), }); const res = await mod.handleMcpStreamableHTTP(req); // Spec reserves 400 for a *missing* session id on non-initialize requests — // only the *present-but-unknown* case changed to 404. This must NOT regress. assert.equal(res.status, 400); }); test("handleMcpStreamableHTTP auto-recovers when stale session id is sent with initialize", async () => { mod.shutdownMcpHttp(); const initReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", }, body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1, params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" }, }, }), }); const firstRes = await mod.handleMcpStreamableHTTP(initReq); const staleSessionId = firstRes.headers.get("mcp-session-id"); if (!staleSessionId) { mod.shutdownMcpHttp(); return; } mod.shutdownMcpHttp(); assert.equal(mod.isMcpHttpActive(), false); const reinitReq = new Request("http://localhost/api/mcp/stream", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", "mcp-session-id": staleSessionId, }, body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 2, params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" }, }, }), }); const recoveryRes = await mod.handleMcpStreamableHTTP(reinitReq); assert.notEqual( recoveryRes.status, 404, "Stale session + initialize must NOT return 404 — server should auto-recover" ); assert.ok( recoveryRes.status >= 200 && recoveryRes.status < 300, `Expected 2xx response on auto-recovery, got ${recoveryRes.status}` ); const newSessionId = recoveryRes.headers.get("mcp-session-id"); assert.ok(newSessionId, "Server must issue a new mcp-session-id on auto-recovery"); assert.equal( mod.isMcpHttpActive(), true, "Server must have an active session after auto-recovery" ); mod.shutdownMcpHttp(); });