127 lines
5.0 KiB
JavaScript
127 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { startStreamableHttpServer } from "../src/streamable-http.js";
|
|
|
|
test("accepts a synchronous session cleanup callback during shutdown", async () => {
|
|
const closed = [];
|
|
const listener = await startStreamableHttpServer({
|
|
serverName: "test-postgres-mcp",
|
|
serverVersion: "1.0.0",
|
|
tools: [],
|
|
callTool: async () => ({}),
|
|
apiKeys: ["agent=0123456789abcdef0123456789abcdef"],
|
|
host: "127.0.0.1",
|
|
port: 0,
|
|
onSessionClose: ({ id }) => { closed.push(id); }
|
|
});
|
|
|
|
const response = await fetch(`http://127.0.0.1:${listener.port}/mcp`, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: "Bearer 0123456789abcdef0123456789abcdef",
|
|
"content-type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1" } }
|
|
})
|
|
});
|
|
const sessionId = response.headers.get("mcp-session-id");
|
|
assert.equal(response.status, 200);
|
|
assert.ok(sessionId);
|
|
|
|
await listener.close();
|
|
assert.deepEqual(closed, [sessionId]);
|
|
});
|
|
|
|
const token = "0123456789abcdef0123456789abcdef";
|
|
|
|
async function startPinned(options = {}) {
|
|
return startStreamableHttpServer({
|
|
serverName: "test-postgres-mcp",
|
|
serverVersion: "1.0.0",
|
|
tools: (context) => context.pinned["x-postgres-scope"] !== undefined ? [{ name: "internal_only" }] : [{ name: "external_only" }],
|
|
callTool: async (name, _args, context) => ({ name, pinned: context.pinned }),
|
|
apiKeys: [`agent=${token}`],
|
|
host: "127.0.0.1",
|
|
port: 0,
|
|
pinnedHeaders: ["x-postgres-scope"],
|
|
...options
|
|
});
|
|
}
|
|
|
|
function rpc(listener, body, headers = {}) {
|
|
return fetch(`http://127.0.0.1:${listener.port}/mcp`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...headers },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, ...body })
|
|
});
|
|
}
|
|
|
|
const initialize = { method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1" } } };
|
|
|
|
test("pins the bucket header at session open and offers that session's tools", async () => {
|
|
const initialized = [];
|
|
const listener = await startPinned({ onSessionInitialize: (context) => { initialized.push(context.pinned); } });
|
|
try {
|
|
const opened = await rpc(listener, initialize, { "x-postgres-scope": "exec-1" });
|
|
const sessionId = opened.headers.get("mcp-session-id");
|
|
assert.equal(opened.status, 200);
|
|
assert.deepEqual(initialized, [{ "x-postgres-scope": "exec-1" }]);
|
|
|
|
const listed = await (await rpc(listener, { method: "tools/list" }, { "mcp-session-id": sessionId })).json();
|
|
assert.deepEqual(listed.result.tools.map((tool) => tool.name), ["internal_only"]);
|
|
|
|
const called = await (await rpc(listener, { method: "tools/call", params: { name: "internal_only" } }, { "mcp-session-id": sessionId })).json();
|
|
assert.deepEqual(called.result.structuredContent.pinned, { "x-postgres-scope": "exec-1" });
|
|
|
|
const refused = await (await rpc(listener, { method: "tools/call", params: { name: "external_only" } }, { "mcp-session-id": sessionId })).json();
|
|
assert.match(refused.error.message, /Unknown tool/);
|
|
} finally {
|
|
await listener.close();
|
|
}
|
|
});
|
|
|
|
test("refuses a bucket header that changes within a session", async () => {
|
|
const listener = await startPinned();
|
|
try {
|
|
const opened = await rpc(listener, initialize, { "x-postgres-scope": "exec-1" });
|
|
const sessionId = opened.headers.get("mcp-session-id");
|
|
const changed = await rpc(listener, { method: "tools/list" }, { "mcp-session-id": sessionId, "x-postgres-scope": "exec-2" });
|
|
assert.equal(changed.status, 400);
|
|
assert.match((await changed.json()).error, /cannot change within a session/);
|
|
} finally {
|
|
await listener.close();
|
|
}
|
|
});
|
|
|
|
test("offers the external tools to a session opened without the bucket header", async () => {
|
|
const listener = await startPinned();
|
|
try {
|
|
const opened = await rpc(listener, initialize);
|
|
const listed = await (await rpc(listener, { method: "tools/list" }, { "mcp-session-id": opened.headers.get("mcp-session-id") })).json();
|
|
assert.deepEqual(listed.result.tools.map((tool) => tool.name), ["external_only"]);
|
|
} finally {
|
|
await listener.close();
|
|
}
|
|
});
|
|
|
|
test("does not open a session whose initialization fails", async () => {
|
|
const closed = [];
|
|
const listener = await startPinned({
|
|
onSessionInitialize: () => { throw Object.assign(new Error("not a valid bucket name"), { statusCode: 400 }); },
|
|
onSessionClose: ({ id }) => { closed.push(id); }
|
|
});
|
|
try {
|
|
const refused = await rpc(listener, initialize, { "x-postgres-scope": "Bad" });
|
|
assert.equal(refused.status, 400);
|
|
assert.match((await refused.json()).error, /Unable to initialize the session: not a valid bucket name/);
|
|
assert.equal(refused.headers.get("mcp-session-id"), null);
|
|
assert.equal(closed.length, 1);
|
|
} finally {
|
|
await listener.close();
|
|
}
|
|
});
|