mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
60bd646478
Add an explicit Streamable HTTP entry for environment/gateway hosts while keeping stdio registrations and default behavior. Preserve the shared provider/session owner across DELETE and replacement; fence and drain tool/completion work before handover or teardown. Bound HTTP input and plain-response admission, cancel refused bodies, join whole exchanges and asynchronous response cleanup before provider hooks, gate startup readiness, and reject queued HTTP/1.1 pipelining while retaining sequential keep-alive. Validation: 8 static/build tasks, 83 adapter tests and registry metadata pass. A real installed gh binary produces seven equivalent CLI/stdio/HTTP reports with DELETE/replacement and clean signal shutdown. Broader local native-provider/ownership failures reproduce on clean main and are documented with limitations in the PR. Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { Help, z } from "incur";
|
|
|
|
import {
|
|
MCP_HTTP_PORT_CONSTRAINT,
|
|
mcpHttpPortSchema,
|
|
} from "../config/mcpHttpEndpoint.js";
|
|
|
|
/**
|
|
* Canonical `rea mcp --http` options. The help renderer lists exactly this
|
|
* schema and the parser accepts exactly these flags plus the `-h` alias.
|
|
*/
|
|
const MCP_HTTP_OPTIONS = z.object({
|
|
http: z
|
|
.boolean()
|
|
.describe("Serve the MCP catalog over streamable HTTP at /mcp, not stdio"),
|
|
host: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Listener address; default MCP_HOST, else 127.0.0.1"),
|
|
port: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.max(65535)
|
|
.optional()
|
|
.describe("Listener port; default MCP_PORT, else 0 for an ephemeral port"),
|
|
help: z.boolean().optional().describe("Show help"),
|
|
});
|
|
|
|
const MCP_HTTP_ALIAS = { help: "h" } as const;
|
|
const MCP_HTTP_COMMAND = "rea mcp --http";
|
|
const MCP_HTTP_DESCRIPTION =
|
|
"Serve the MCP server over streamable HTTP for one local client";
|
|
const HELP_FLAGS: ReadonlySet<string> = new Set(["--help", "-h"]);
|
|
const VALUED_FLAGS: ReadonlySet<string> = new Set(["--host", "--port"]);
|
|
|
|
/** Render help for the dispatcher entry, which accepts no Incur global options. */
|
|
export const renderMcpHttpHelp = (): string =>
|
|
Help.formatCommand(MCP_HTTP_COMMAND, {
|
|
description: MCP_HTTP_DESCRIPTION,
|
|
hideGlobalOptions: true,
|
|
options: MCP_HTTP_OPTIONS,
|
|
alias: MCP_HTTP_ALIAS,
|
|
hint: "This entry accepts only the options above. The bound URL is logged as one JSON line on stderr.",
|
|
});
|
|
|
|
/** Parsed `rea mcp --http` arguments; omitted values defer to the environment. */
|
|
export type McpHttpArguments =
|
|
| { readonly ok: true; readonly host?: string; readonly port?: number }
|
|
| { readonly ok: false; readonly help: true }
|
|
| { readonly ok: false; readonly help: false; readonly message: string };
|
|
|
|
const invalid = (message: string): McpHttpArguments => ({
|
|
ok: false,
|
|
help: false,
|
|
message,
|
|
});
|
|
|
|
/** Split `--flag=value` into its flag and inline value. */
|
|
const splitFlag = (argument: string): readonly [string, string | undefined] => {
|
|
const separator = argument.indexOf("=");
|
|
return separator === -1
|
|
? [argument, undefined]
|
|
: [argument.slice(0, separator), argument.slice(separator + 1)];
|
|
};
|
|
|
|
/** Parse the dispatcher arguments after `mcp`; help wins only in flag position. */
|
|
export const parseMcpHttpArguments = (
|
|
arguments_: readonly string[],
|
|
): McpHttpArguments => {
|
|
const values = new Map<string, string>();
|
|
let http = false;
|
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
const argument = arguments_[index] ?? "";
|
|
if (HELP_FLAGS.has(argument)) return { ok: false, help: true };
|
|
if (argument === "--http") {
|
|
http = true;
|
|
continue;
|
|
}
|
|
const [flag, inline] = splitFlag(argument);
|
|
if (!VALUED_FLAGS.has(flag))
|
|
return invalid(`Unknown mcp --http argument: ${argument}`);
|
|
const value = inline ?? arguments_[index + 1];
|
|
if (inline === undefined) index += 1;
|
|
if (value === undefined || (inline === undefined && value.startsWith("-")))
|
|
return invalid(`Missing value for ${flag}`);
|
|
values.set(flag, value);
|
|
}
|
|
if (!http) return invalid("--host and --port require --http");
|
|
const host = values.get("--host")?.trim();
|
|
if (host !== undefined && host.length === 0)
|
|
return invalid("--host must be a non-empty host name or IP address");
|
|
const portText = values.get("--port");
|
|
const port =
|
|
portText === undefined ? undefined : mcpHttpPortSchema.safeParse(portText);
|
|
if (port !== undefined && !port.success)
|
|
return invalid(`Invalid --port: ${MCP_HTTP_PORT_CONSTRAINT}`);
|
|
return {
|
|
ok: true,
|
|
...(host === undefined ? {} : { host }),
|
|
...(port === undefined ? {} : { port: port.data }),
|
|
};
|
|
};
|