Files
reverse-engineering-anything/src/main.ts
T
Dery Ferdikaandgensokyo-dev aa6c1d49fd fix(mcp)!: require allowed hosts for non-loopback HTTP binds (#7172)
Require operator-declared hostnames before serving MCP HTTP outside loopback, preserving Host/Origin gates. Parse hostname identities once, share typed startup recovery across CLI/MCP, and announce an admitted client URL. Generated AgentEnv bundles declare their client/service names and validate DNS/HTTP name identity before export.

Validated through real packaged HTTP requests, direct SDK connection, compiled export refusals, 184 focused tests, static and documentation checks on the refreshed main integration. Final CI and review evidence is recorded in the PR body.

BREAKING CHANGE: non-loopback deployments require MCP_ALLOWED_HOSTS or rea mcp --http --allowed-hosts; bundle names must be unchanged HTTP hostnames within the 63-octet DNS label bound.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-26 22:55:10 +08:00

217 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
import { CURRENT_ENTRYPOINT_PATH } from "./process/CurrentEntrypointPath.js";
import { pathToFileURL } from "node:url";
import { realpathSync } from "node:fs";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { parseConfig } from "./config/parseConfig.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
import type { RuntimeDependencies } from "./main/types.js";
import { runDeferred } from "./main/deferredRuntime.js";
import { RECORDED_MCP_CATALOG_SHA256 } from "./recordedMcpCatalog.js";
import { SERVER_START_FAILED } from "./main/messages.js";
import {
createExitStatus,
latchSignalShutdown,
registerProcessShutdown,
registerSignalShutdown,
type ExitStatus,
type SignalLatch,
} from "./main/processShutdown.js";
import {
parseMcpHttpEndpoint,
type McpHttpEndpoint,
} from "./config/mcpHttpEndpoint.js";
const runtimeDependencies = (): RuntimeDependencies => ({
env: process.env,
stdin: process.stdin,
serve: serveStdio,
writeStderr: (text) => process.stderr.write(text),
setExitCode: (code) => {
process.exitCode = code;
},
registerShutdown: registerProcessShutdown,
});
/** Replace stdio with the streamable HTTP listener; signals alone end it, never stdin. */
const httpRuntimeDependencies = (
endpoint: McpHttpEndpoint,
latch: SignalLatch,
exit: ExitStatus,
): RuntimeDependencies => {
const base = runtimeDependencies();
return {
...base,
transport: "http",
setExitCode: exit.set,
serve: async (factory, options) => {
// Loaded here so the default stdio entry never loads the HTTP listener.
const { serveStreamableHttp } =
await import("./server/http/serveStreamableHttp.js");
const { RequestTrackingMcpServer } =
await import("./server/RequestTrackingMcpServer.js");
// The transport's factory owner closes every instance it created; the listener must not race it.
return serveStreamableHttp({
...endpoint,
createMcpServer: async (context) => {
const server = await factory(context);
if (!(server instanceof RequestTrackingMcpServer))
throw new TypeError("HTTP MCP servers must track tool completion");
return server;
},
closeInstance: false,
...options,
});
},
registerShutdown: (request) => {
const unregister = registerSignalShutdown((exitStatus) => {
exit.signal(exitStatus);
request();
});
// A signal that arrived during startup is honoured now that the lifecycle owner exists;
// its status outlives the entry's own `0`, which startup returns right after this.
const pending = latch.release();
if (pending !== undefined) {
exit.signal(pending);
request();
}
return unregister;
},
};
};
/** Start discovery without constructing unused analysis validators. */
export const run = async (
dependencies: RuntimeDependencies = runtimeDependencies(),
): Promise<number> => {
const environment = snapshotEnvironment(dependencies.env);
const config = parseConfig(environment);
if (!config.ok) {
dependencies.writeStderr(`${projectAnalysisError(config.error).message}\n`);
return 1;
}
const fullOutputCatalog =
environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === undefined ||
environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === "full";
// The recorded catalog holds the complete inventory and its full-catalog
// identity, so only an unscoped registration can be served from it. A
// scoped one composes the canonical scope itself instead of advertising
// tools whose identity the runtime cannot verify.
const scopedToolFamilies = environment.REA_MCP_TOOL_FAMILIES !== undefined;
if (
RECORDED_MCP_CATALOG_SHA256 !== undefined &&
dependencies.transport !== "http" &&
config.value.hopperTargetPath === undefined &&
fullOutputCatalog &&
!scopedToolFamilies &&
dependencies.createServer === undefined &&
dependencies.loadOptionalProviders === undefined
) {
return runDeferred({ ...dependencies, env: environment }, config.value);
}
return (await import("./main/eagerRuntime.js")).runEager(
{ ...dependencies, env: environment },
config.value,
);
};
/** Run the executable boundary without exposing an unexpected startup cause. */
export const runEntrypoint = async (
start: () => Promise<number> = () => run(),
writeStderr: (text: string) => void = (text) => process.stderr.write(text),
setExitCode: (code: number) => void = (code) => {
process.exitCode = code;
},
): Promise<void> => {
try {
setExitCode(await start());
} catch (cause: unknown) {
// Intentionally redact the startup cause from caller-visible output;
// unexpected failures must not leak internal details.
void cause;
writeStderr(`${SERVER_START_FAILED}\n`);
setExitCode(1);
}
};
/** Process streams the dispatcher entries write to. */
export interface EntrypointIo {
readonly writeStdout: (text: string) => void;
readonly writeStderr: (text: string) => void;
readonly setExitCode: (code: number) => void;
}
const processIo: EntrypointIo = {
writeStdout: (text) => void process.stdout.write(text),
writeStderr: (text) => void process.stderr.write(text),
setExitCode: (code) => {
process.exitCode = code;
},
};
/** Run `rea mcp --http`: parse its flags, resolve MCP_HOST/MCP_PORT, then serve over HTTP. */
export const runMcpHttpEntrypoint = async (
arguments_: readonly string[],
io: EntrypointIo = processIo,
): Promise<void> => {
const { parseMcpHttpArguments, renderMcpHttpHelp } =
await import("./main/mcpHttpArguments.js");
const parsed = parseMcpHttpArguments(arguments_);
if (!parsed.ok) {
if (parsed.help) {
io.writeStdout(`${renderMcpHttpHelp()}\n`);
io.setExitCode(0);
return;
}
io.writeStderr(`${parsed.message}\n`);
io.setExitCode(1);
return;
}
const endpoint = parseMcpHttpEndpoint(snapshotEnvironment(process.env), {
...(parsed.host === undefined ? {} : { host: parsed.host }),
...(parsed.port === undefined ? {} : { port: parsed.port }),
...(parsed.allowedHosts === undefined
? {}
: { allowedHosts: [...parsed.allowedHosts] }),
});
if (!endpoint.ok) {
io.writeStderr(`${projectAnalysisError(endpoint.error).message}\n`);
io.setExitCode(1);
return;
}
// Latched before any asynchronous startup so a signal cannot end the process without cleanup.
const latch = latchSignalShutdown();
const exit = createExitStatus(io.setExitCode);
try {
await runEntrypoint(
() => run(httpRuntimeDependencies(endpoint.value, latch, exit)),
io.writeStderr,
exit.set,
);
} finally {
latch.release();
}
};
/** Return false when the runtime's argv path is synthetic or no longer exists. */
const isDirectEntrypoint = (
entryPath: string | undefined,
moduleUrl: string,
): boolean => {
if (entryPath === undefined) return false;
try {
return pathToFileURL(realpathSync(entryPath)).href === moduleUrl;
} catch {
return false;
}
};
if (isDirectEntrypoint(CURRENT_ENTRYPOINT_PATH, import.meta.url)) {
void runEntrypoint();
}