Bound shared MCP container lifecycle
This commit is contained in:
113
scripts/test-web-search-http.mjs
Normal file
113
scripts/test-web-search-http.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
import assert from "node:assert/strict";
|
||||
import http from "node:http";
|
||||
import { once } from "node:events";
|
||||
|
||||
import {
|
||||
createSecureMcpServer,
|
||||
mcpProxyArguments,
|
||||
superviseBackend
|
||||
} from "../docker/web-search/http-entrypoint.mjs";
|
||||
import { probeMcp } from "../docker/web-search/mcp-probe.mjs";
|
||||
|
||||
let backendAlive = true;
|
||||
assert(mcpProxyArguments.includes("--stateless"));
|
||||
const backend = http.createServer(async (request, response) => {
|
||||
if (request.url === "/status") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end('{"server_instances":{"default":"configured"}}');
|
||||
return;
|
||||
}
|
||||
if (request.method !== "POST" || request.url !== "/mcp") {
|
||||
response.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
let body = "";
|
||||
for await (const chunk of request) body += chunk;
|
||||
const message = JSON.parse(body);
|
||||
if (message.method === "initialize") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: message.id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "mock-web-search", version: "1" }
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (message.method === "tools/list" && backendAlive) {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: message.id,
|
||||
result: { tools: [{ name: "search_web" }, { name: "fetch_url" }] }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
response.writeHead(500, { "Content-Type": "text/plain" });
|
||||
response.end("backend dead");
|
||||
});
|
||||
backend.listen(0, "127.0.0.1");
|
||||
await once(backend, "listening");
|
||||
const backendPort = backend.address().port;
|
||||
const upstream = `http://127.0.0.1:${backendPort}`;
|
||||
|
||||
const front = createSecureMcpServer({ upstream });
|
||||
front.listen(0, "127.0.0.1");
|
||||
await once(front, "listening");
|
||||
const frontPort = front.address().port;
|
||||
|
||||
function rawRequest({ host, origin }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = {
|
||||
Accept: "application/json, text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
Host: host
|
||||
};
|
||||
if (origin !== undefined) headers.Origin = origin;
|
||||
const request = http.request({
|
||||
hostname: "127.0.0.1",
|
||||
port: frontPort,
|
||||
path: "/mcp",
|
||||
method: "POST",
|
||||
headers
|
||||
}, response => {
|
||||
response.resume();
|
||||
response.once("end", () => resolve(response.statusCode));
|
||||
});
|
||||
request.once("error", reject);
|
||||
request.end('{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}');
|
||||
});
|
||||
}
|
||||
|
||||
const tools = await probeMcp(`http://127.0.0.1:${frontPort}/mcp`);
|
||||
assert.deepEqual(tools, ["fetch_url", "search_web"]);
|
||||
assert.equal(await rawRequest({ host: "attacker.example" }), 421);
|
||||
assert.equal(await rawRequest({ host: `127.0.0.1:${frontPort}`, origin: "https://attacker.example" }), 403);
|
||||
assert.equal(await rawRequest({ host: `127.0.0.1:${frontPort}`, origin: `http://127.0.0.1:${frontPort}` }), 403);
|
||||
|
||||
backendAlive = false;
|
||||
assert.equal((await fetch(`${upstream}/status`)).status, 200);
|
||||
assert.equal((await fetch(`http://127.0.0.1:${frontPort}/healthz`)).status, 503);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("backend supervisor did not detect death")), 1000);
|
||||
const stop = superviseBackend({
|
||||
probe: () => probeMcp(`${upstream}/mcp`, { timeoutMs: 100 }),
|
||||
intervalMs: 10,
|
||||
onFailure: () => {
|
||||
clearTimeout(timeout);
|
||||
stop();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
front.close();
|
||||
front.closeAllConnections();
|
||||
backend.close();
|
||||
backend.closeAllConnections();
|
||||
console.log("pass web-search HTTP security and supervision tests");
|
||||
Reference in New Issue
Block a user