mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
Add rea mcp --http [--host] [--port]: the same server rea speaks over stdio, served over MCP Streamable HTTP on a loopback listener by default, using the SDK's web-standard transport already in the pinned @modelcontextprotocol/server behind a small node:http bridge. One client and one session at a time; DELETE ends the session without running provider teardown, a new client's initialize replaces a session left open, foreign Host headers are refused on loopback binds, and shutdown is signals-only. Stdio stays the default and registered transport; server.json and client registrations are unchanged. Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
import { request as httpRequest, type IncomingHttpHeaders } from "node:http";
|
|
|
|
import { expect } from "vitest";
|
|
|
|
/** One buffered HTTP response. */
|
|
export interface RawHttpResponse {
|
|
readonly status: number;
|
|
readonly headers: IncomingHttpHeaders;
|
|
readonly body: string;
|
|
}
|
|
|
|
/** Send one request with caller-controlled headers, including Host, which fetch overwrites. */
|
|
export const rawHttpRequest = (
|
|
url: URL,
|
|
options: {
|
|
readonly method?: string;
|
|
readonly headers?: Readonly<Record<string, string>>;
|
|
readonly body?: string;
|
|
} = {},
|
|
): Promise<RawHttpResponse> =>
|
|
new Promise((resolve, reject) => {
|
|
// A fresh connection per request keeps a closed listener observable as ECONNREFUSED.
|
|
const request = httpRequest(
|
|
url,
|
|
{
|
|
method: options.method ?? "GET",
|
|
headers: { ...options.headers },
|
|
agent: false,
|
|
},
|
|
(response) => {
|
|
let body = "";
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk: string) => {
|
|
body += chunk;
|
|
});
|
|
response.on("end", () =>
|
|
resolve({
|
|
status: response.statusCode ?? 0,
|
|
headers: response.headers,
|
|
body,
|
|
}),
|
|
);
|
|
response.on("error", reject);
|
|
},
|
|
);
|
|
request.on("error", reject);
|
|
request.end(options.body);
|
|
});
|
|
|
|
/** Assert the MCP endpoint refuses a Host header outside the loopback allowlist. */
|
|
export const expectForeignHostRefused = async (
|
|
url: URL,
|
|
): Promise<RawHttpResponse> => {
|
|
const refused = await rawHttpRequest(url, {
|
|
method: "POST",
|
|
headers: {
|
|
host: "rea.invalid",
|
|
"content-type": "application/json",
|
|
accept: "application/json, text/event-stream",
|
|
},
|
|
body: "{}",
|
|
});
|
|
expect(refused.status).toBe(403);
|
|
expect(JSON.parse(refused.body)).toMatchObject({
|
|
jsonrpc: "2.0",
|
|
error: { code: -32000 },
|
|
});
|
|
return refused;
|
|
};
|