Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface RunProxyOptions {

export interface ProxyHandle {
close: () => Promise<void>;
/** Set when inbound HTTP is enabled; actual bound port (config port 0 → OS-assigned). */
httpPort?: number;
}

export async function runProxy(opts: RunProxyOptions): Promise<ProxyHandle> {
Expand Down Expand Up @@ -65,13 +67,15 @@ export async function runProxy(opts: RunProxyOptions): Promise<ProxyHandle> {
const handle = await startStdioServer(aggregator.createServer());
closers.push(handle.close);
}
let httpPort: number | undefined;
if (cfg.inbound.http.enabled) {
const handle = await startHttpServer({
cfg,
createServer: () => aggregator.createServer(),
upstream,
audit,
});
httpPort = handle.port;
closers.push(handle.close);
}

Expand All @@ -85,6 +89,7 @@ export async function runProxy(opts: RunProxyOptions): Promise<ProxyHandle> {
);

return {
httpPort,
close: async () => {
for (const c of [...closers].reverse()) {
try {
Expand Down
17 changes: 13 additions & 4 deletions src/server/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { child as childLogger } from "../logger.js";

export interface InboundHttpHandle {
close: () => Promise<void>;
/** Actual bound port (useful when config port is 0). */
port: number;
}

/**
Expand All @@ -29,7 +31,9 @@ export async function startHttpServer(args: {
}): Promise<InboundHttpHandle> {
const { cfg, createServer: createMcp, upstream } = args;
const log = childLogger({ component: "inbound-http" });
const { host, port, path: mcpPath, sessions } = cfg.inbound.http;
const { host, path: mcpPath, sessions } = cfg.inbound.http;
const requestedPort = cfg.inbound.http.port;
let boundPort = requestedPort;

if (sessions !== "stateless") {
log.warn(
Expand All @@ -39,7 +43,7 @@ export async function startHttpServer(args: {
}

const handler = async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
const url = new URL(req.url ?? "/", `http://${host}:${boundPort}`);

if (url.pathname === "/healthz") {
const upstreams = [...upstream.connections.values()].map((c) => ({
Expand Down Expand Up @@ -101,14 +105,19 @@ export async function startHttpServer(args: {

await new Promise<void>((resolve, reject) => {
httpServer.once("error", reject);
httpServer.listen(port, host, () => {
httpServer.listen(requestedPort, host, () => {
httpServer.off("error", reject);
const addr = httpServer.address();
if (addr && typeof addr === "object") {
boundPort = addr.port;
}
resolve();
});
});
log.info({ host, port, path: mcpPath, sessions }, "inbound HTTP transport listening");
log.info({ host, port: boundPort, path: mcpPath, sessions }, "inbound HTTP transport listening");

return {
port: boundPort,
close: () =>
new Promise<void>((resolve) => {
httpServer.close(() => resolve());
Expand Down
13 changes: 5 additions & 8 deletions tests/integration/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ async function startHarness(opts: {
servers: Record<string, ReturnType<typeof echoStdioConfig>>;
filters?: { allow?: string[]; deny?: string[] };
shrink?: { mode?: "off" | "rules" | "llm" };
port?: number;
}): Promise<ProxyHarness> {
const port = opts.port ?? randomPort();
const cfg = buildTestConfig({
servers: opts.servers,
filters: opts.filters,
shrink: opts.shrink,
inboundHttp: true,
});
cfg.inbound.http.port = port;
// Port 0 → OS picks a free port. Random high ports flake on Windows CI (EACCES).
cfg.inbound.http.port = 0;
cfg.inbound.http.host = "127.0.0.1";
const handle = await runProxy({ cfg, disableInboundStdio: true });
const port = handle.httpPort;
if (!port) throw new Error("proxy HTTP port not available");

const url = `http://127.0.0.1:${port}/mcp`;
const client = new Client({ name: "test-client", version: "0.0.1" }, { capabilities: {} });
Expand All @@ -55,11 +56,7 @@ async function startHarness(opts: {
return harness;
}

function randomPort(): number {
return 30_000 + Math.floor(Math.random() * 20_000);
}

describe("end-to-end proxy", () => {
describe.sequential("end-to-end proxy", () => {
it(
"merges tool lists from two upstream stdio servers under namespaced names",
async () => {
Expand Down
Loading