authorgravatar for ishansachu1@gmail.comIshan <ishansachu1@gmail.com> 2026-06-19 09:11:14+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-18 20:41:14-07:00
log0651af92b88728785a39bd16e8d411ad741b681f
tree3d1f5162ea1623d7eddecc84db30b9524342b9d4
parent52a24c890265967aa31fa03c4339210022ac403d
signature Signed by PGP key B5690EEEBB952194

fix(server): use bound host for MCP endpoint (#3114)


2 files changed, 40 insertions(+), 7 deletions(-)

apps/server/src/mcp/McpSessionRegistry.test.ts+28-6
......@@ -8,16 +8,18 @@ import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"
88import * as McpSessionRegistry from "./McpSessionRegistry.ts";
99
1010const environmentId = EnvironmentId.make("environment-1");
11const fakeHttpServer = HttpServer.HttpServer.of({
12 address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 43123 },
13 serve: (() => Effect.void) as HttpServer.HttpServer["Service"]["serve"],
14});
11const makeFakeHttpServer = (hostname: string, port = 43123) =>
12 HttpServer.HttpServer.of({
13 address: { _tag: "TcpAddress", hostname, port },
14 serve: (() => Effect.void) as HttpServer.HttpServer["Service"]["serve"],
15 });
16const fakeHttpServer = makeFakeHttpServer("127.0.0.1");
1517const fakeEnvironment = ServerEnvironment.of({
1618 getEnvironmentId: Effect.succeed(environmentId),
1719 getDescriptor: Effect.die("unused"),
1820});
1921
20const makeRegistry = (now: () => number) =>
22const makeRegistry = (now: () => number, httpServer = fakeHttpServer) =>
2123 McpSessionRegistry.__testing
2224 .make({
2325 now,
......@@ -25,7 +27,7 @@ const makeRegistry = (now: () => number) =>
2527 maximumLifetimeMs: 1_000,
2628 })
2729 .pipe(
28 Effect.provideService(HttpServer.HttpServer, fakeHttpServer),
30 Effect.provideService(HttpServer.HttpServer, httpServer),
2931 Effect.provideService(ServerEnvironment, fakeEnvironment),
3032 Effect.provide(NodeServices.layer),
3133 );
......@@ -53,6 +55,26 @@ it.effect("stores only a token hash, resolves the bearer token, and revokes by t
5355 }),
5456);
5557
58it.effect("builds MCP endpoints from the bound server host", () =>
59 Effect.gen(function* () {
60 const cases = [
61 ["100.64.0.40", "http://100.64.0.40:43123/mcp"],
62 ["0.0.0.0", "http://127.0.0.1:43123/mcp"],
63 ["localhost", "http://localhost:43123/mcp"],
64 ["127.0.0.1", "http://127.0.0.1:43123/mcp"],
65 ] as const;
66
67 for (const [hostname, expectedEndpoint] of cases) {
68 const registry = yield* makeRegistry(() => 1_000, makeFakeHttpServer(hostname));
69 const issued = yield* registry.issue({
70 threadId: ThreadId.make(`thread-${hostname}`),
71 providerInstanceId: ProviderInstanceId.make("codex"),
72 });
73 expect(issued.config.endpoint).toBe(expectedEndpoint);
74 }
75 }),
76);
77
5678it.effect("expires credentials after inactivity", () =>
5779 Effect.gen(function* () {
5880 let timestamp = 1_000;
apps/server/src/mcp/McpSessionRegistry.ts+12-1
......@@ -60,6 +60,17 @@ const bytesToHex = (bytes: Uint8Array): string =>
6060
6161const tokenFromBytes = (bytes: Uint8Array): string => Buffer.from(bytes).toString("base64url");
6262
63const getHttpMcpEndpointHost = (hostname: string): string => {
64 const normalized = hostname.toLowerCase();
65 const endpointHostname =
66 normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]"
67 ? "127.0.0.1"
68 : hostname;
69 return endpointHostname.includes(":") && !endpointHostname.startsWith("[")
70 ? `[${endpointHostname}]`
71 : endpointHostname;
72};
73
6374const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
6475 options: McpSessionRegistryOptions = {},
6576) {
......@@ -73,7 +84,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
7384 const maximumLifetimeMs = options.maximumLifetimeMs ?? DEFAULT_MAXIMUM_LIFETIME_MS;
7485 const endpoint =
7586 httpServer.address._tag === "TcpAddress"
76 ? `http://127.0.0.1:${httpServer.address.port}/mcp`
87 ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp`
7788 : "http://127.0.0.1/mcp";
7889
7990 const hashToken = (token: string) =>