fix(mcp): preserve repeated HTTP host allowlists (#8164)

This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-10-31 03:13:07 +08:00
1 parent 47fa6ab379
commit d48e472fa6
4 files changed
+148 -4

No files matched your search

+6 -3
View File
@@ -29,7 +29,7 @@ const MCP_HTTP_OPTIONS = z.object({
.array(z.string())
.optional()
.describe(
"Host/Origin names a non-loopback bind may serve; default MCP_ALLOWED_HOSTS, else the bind is refused",
"Host/Origin names a non-loopback bind may serve; repeat or separate names with commas; default MCP_ALLOWED_HOSTS, else the bind is refused",
),
help: z.boolean().optional().describe("Show help"),
});
@@ -85,6 +85,7 @@ export const parseMcpHttpArguments = (
arguments_: readonly string[],
): McpHttpArguments => {
const values = new Map<string, string>();
const allowedHostValues: string[] = [];
let http = false;
for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index] ?? "";
@@ -100,7 +101,8 @@ export const parseMcpHttpArguments = (
if (inline === undefined) index += 1;
if (value === undefined || (inline === undefined && value.startsWith("-")))
return invalid(`Missing value for ${flag}`);
values.set(flag, value);
if (flag === "--allowed-hosts") allowedHostValues.push(value);
else values.set(flag, value);
}
if (!http) return invalid("--host and --port require --http");
const host = values.get("--host")?.trim();
@@ -111,7 +113,8 @@ export const parseMcpHttpArguments = (
portText === undefined ? undefined : mcpHttpPortSchema.safeParse(portText);
if (port !== undefined && !port.success)
return invalid(`Invalid --port: ${MCP_HTTP_PORT_CONSTRAINT}`);
const allowedHostsText = values.get("--allowed-hosts");
const allowedHostsText =
allowedHostValues.length === 0 ? undefined : allowedHostValues.join(",");
const allowedHosts =
allowedHostsText === undefined
? undefined
@@ -21,6 +21,7 @@ import {
} from "./serveStreamableHttp.js";
import { StreamableHttpListenError } from "./streamableHttpListenError.js";
import { mcpAllowedHostsTextSchema } from "../../config/mcpHttpEndpoint.js";
import { parseMcpHttpArguments } from "../../main/mcpHttpArguments.js";
import { RequestTrackingMcpServer } from "../RequestTrackingMcpServer.js";
const servers: StreamableHttpServer[] = [];
@@ -346,6 +347,49 @@ it.each([
expect(JSON.parse(response.body)).toEqual({ allowed: true });
});
it("serves every declared repeated host while refusing undeclared hosts and origins", async () => {
const parsed = parseMcpHttpArguments([
"--http",
"--allowed-hosts",
"a.example",
"--allowed-hosts=b.example",
]);
if (!parsed.ok || parsed.allowedHosts === undefined)
throw new Error("Expected valid repeated host lists");
const { served } = await start({
host: "0.0.0.0",
allowedHosts: parsed.allowedHosts,
routes: [
{
method: "GET",
path: "/probe",
handle: () => Response.json({ allowed: true }),
},
],
});
const url = new URL(served.url);
url.hostname = "127.0.0.1";
url.pathname = "/probe";
for (const host of ["a.example", "b.example"]) {
const response = await rawHttpRequest(url, {
headers: { host, origin: `http://${host}` },
});
expect(response.status, host).toBe(200);
expect(JSON.parse(response.body)).toEqual({ allowed: true });
}
expect(
(await rawHttpRequest(url, { headers: { host: "undeclared.example" } }))
.status,
).toBe(403);
expect(
(
await rawHttpRequest(url, {
headers: { host: "a.example", origin: "http://undeclared.example" },
})
).status,
).toBe(403);
});
it("announces an admitted non-loopback URL and gates it on the operator allowlist", async () => {
const { served } = await start({
host: "0.0.0.0",