import { type Server } from "node:http"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export interface HttpTransportOptions { host: string; port: number; path: string; stateless: boolean; enableJsonResponse: boolean; /** Idle timeout for stateful sessions, in ms. Defaults to 30 min. */ sessionTtlMs?: number; /** How often to sweep idle sessions, in ms. Defaults to 5 min. */ sessionSweepIntervalMs?: number; /** Max concurrent stateful sessions before new inits are rejected with 503. */ maxSessions?: number; /** * Allow-list of Host header hostnames (port-agnostic) for DNS rebinding * protection. Use `["*"]` to opt out explicitly. When undefined **or empty** * (an unset / blank env var), a localhost default is applied if bound to a * loopback interface, and validation is off otherwise — a blank value never * silently disables the check, `*` is the explicit opt-out. */ allowedHosts?: string[]; /** * Allow-list of `Origin` header values (scheme + host + port) accepted from * browser-based clients. A request with **no** `Origin` is always accepted * (curl, gateways, non-browser MCP clients); a request carrying an `Origin` * outside this list gets a `403` (MCP 2025-11-25 requirement). Use `["*"]` to * opt out explicitly. When undefined **or empty** (an unset / blank env var), * the loopback default of `resolveOriginPolicy` applies — same rationale as * `allowedHosts`: blank does not mean "disabled". */ allowedOrigins?: string[]; /** * Public URL clients use to reach this MCP endpoint — used as the * `resource` field of the protected-resource metadata and in the * `WWW-Authenticate` challenge. Defaults to `http://{host}:{port}{path}` * which is only correct for local / loopback deployments. Behind a * reverse proxy, set `MCP_HTTP_PUBLIC_URL` explicitly. */ publicUrl?: string; /** * Skip the OAuth2 Bearer check and use the env-based credentials configured * via `initClient()` (BOOND_USER_TOKEN + BOOND_CLIENT_TOKEN + BOOND_CLIENT_KEY, * or BOOND_API_TOKEN, or BasicAuth). Intended for single-tenant / self-hosted * deployments where the operator owns both the server and the credentials. * Set `BOOND_HTTP_STATIC_AUTH=true` to enable via `resolveHttpOptions()`. * * In this mode **nothing** authenticates the MCP client: whoever reaches the * port acts with the operator's BoondManager rights. `apiKey` is what closes * that (issue #230), and `assertStaticAuthPolicy` refuses to start without * one on a non-loopback bind. */ staticAuth?: boolean; /** * Shared secret the MCP client must present in static-auth mode, as * `Authorization: Bearer ` or `X-Api-Key: `. Compared in constant * time. Set via `MCP_HTTP_API_KEY`. Ignored (with a warning) in OAuth mode, * where the Bearer is the BoondManager access token. */ apiKey?: string; /** * Explicit opt-out of the "static auth off loopback requires an API key" * start-up refusal — for a deployment whose network is the boundary (a * private gateway). Set via `MCP_HTTP_INSECURE_STATIC_AUTH=1`. */ insecureStaticAuth?: boolean; /** * Node `server.keepAliveTimeout`: how long an idle keep-alive connection is * kept open. Must exceed the idle timeout of any load balancer in front * (60 s AWS ALB, 75 s nginx by default) — Node's 5 s default is below both, * which is how a LB reuses a connection Node just closed and answers `502` * (issue #237). Default 65 s; env `MCP_HTTP_KEEP_ALIVE_TIMEOUT_MS`. */ keepAliveTimeoutMs?: number; /** * Node `server.headersTimeout`: budget to receive a request's headers. Node * requires it above `keepAliveTimeout`; a lower value is bumped to * `keepAliveTimeoutMs + 1000` with a warning. Default 66 s; env * `MCP_HTTP_HEADERS_TIMEOUT_MS`. */ headersTimeoutMs?: number; /** * Node `server.requestTimeout`: budget to receive a whole request (headers + * body). Generous because it also bounds slow clients on long tool calls. * Default 5 min; env `MCP_HTTP_REQUEST_TIMEOUT_MS`. */ requestTimeoutMs?: number; /** * OAuth mode only (issue #234): validate each Bearer against BoondManager * (`GET /application/current-user`, cached per token for * `tokenValidationTtlMs`) **before** dispatching, so an expired or revoked * token is answered with HTTP `401` + `WWW-Authenticate: … error="invalid_token"` * (RFC 6750 §3.1) — the signal a spec-compliant MCP client turns into a new * authorization flow. Without it the 401 only surfaces inside a `tools/call` * result, which no client re-authorizes from. Off by default because it * costs one BoondManager call per token per TTL; env `MCP_HTTP_VALIDATE_TOKEN`. */ validateToken?: boolean; /** Positive/negative cache lifetime of a token validation, in ms. Default 60 s; env `MCP_HTTP_TOKEN_VALIDATION_TTL_MS`. */ tokenValidationTtlMs?: number; /** * Grace period `close()` gives in-flight connections (an open SSE stream, a * request mid-body) before destroying them with `closeAllConnections()`. * Idle keep-alive connections are closed immediately. Default 10 s; env * `MCP_HTTP_SHUTDOWN_TIMEOUT_MS`. */ shutdownTimeoutMs?: number; } export interface HttpServerHandle { /** * Stop accepting connections, close idle ones now and in-flight ones after * `shutdownTimeoutMs`, then resolve. Idempotent: a second call (a second * SIGTERM) returns the same promise instead of failing on an already-closed * server (issue #237). */ close: () => Promise; address: { host: string; port: number; path: string; }; /** The underlying Node server — for observability (applied timeouts) and tests. */ server: Server; /** Current count of live stateful sessions (always 0 in stateless mode). */ sessionCount: () => number; /** Manually trigger an idle sweep; returns the number of sessions reaped. */ sweepIdleSessions: () => Promise; } export declare const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 65000; export declare const DEFAULT_HEADERS_TIMEOUT_MS = 66000; export declare const DEFAULT_REQUEST_TIMEOUT_MS = 300000; export declare const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10000; export declare const DEFAULT_TOKEN_VALIDATION_TTL_MS = 60000; export declare const MAX_TOKEN_VALIDATION_ENTRIES = 500; /** * Resolves the effective Host header allow-list given user options and the * bound listen interface. Returns an empty array when validation is disabled * (either explicitly via `["*"]` or implicitly when bound to a non-loopback * interface without an explicit list). * * An empty `configured` array is treated as *unconfigured*, not as "disabled": * `MCP_HTTP_ALLOWED_HOSTS=` (or a value of only commas) must not quietly turn a * security control off. `["*"]` is the explicit opt-out. */ export declare function resolveAllowedHosts(configured: string[] | undefined, host: string): string[]; /** Resolved `Origin` validation policy — see `resolveOriginPolicy`. */ export interface OriginPolicy { /** `false` = validation disabled; every `Origin` is accepted. */ enabled: boolean; /** Exact-match allow-list, normalised (see `normalizeOrigin`). */ origins: string[]; /** * Also accept any `http`/`https` origin whose hostname is a loopback literal, * **on any port**. Set only by the loopback default, never by an explicit * allow-list (an operator who enumerates origins gets exact matching). */ allowAnyLoopback: boolean; } /** * Resolves the effective `Origin` policy, mirroring `resolveAllowedHosts`: an * explicit list wins (exact match, port-sensitive), a sole `*` disables * validation, a `*` mixed with real origins is dropped with a warning * (validation stays on), and the default only applies when bound to a loopback * interface. An empty `configured` array counts as unconfigured, not disabled. * * The loopback default accepts **any loopback origin on any port**, plus the * origin of `publicUrl` when one is configured. Pinning the bound port instead * (the obvious reading of "origins are port-sensitive") would 403 every real * browser client: nothing is ever *served* from this port — it answers JSON-RPC * — so the origins that legitimately show up are other local ports (MCP * Inspector on `:6274`, a dev server on `:5173`) or the proxy's public URL. * The anti-DNS-rebinding property is untouched: a remote page still gets a 403, * and an attacker already running code on the loopback interface can simply * omit the header, which is always accepted. */ export declare function resolveOriginPolicy(configured: string[] | undefined, host: string, publicUrl?: string): OriginPolicy; /** Applies a resolved policy to one `Origin` header value. */ export declare function isOriginAllowed(policy: OriginPolicy, origin: string): boolean; /** * Whether a request targets the public protected-resource metadata document. * Shared by the `Origin` exemption and the handler itself so the two cannot * drift apart. Takes the raw `req.url` — it runs before the `URL` parse. */ export declare function isDiscoveryPath(reqUrl: string | undefined, mcpPath: string): boolean; export declare function resolveHttpOptions(): HttpTransportOptions; /** * The socket timeouts applied to the Node server, with the one invariant Node * itself enforces made explicit: `headersTimeout` must be strictly greater * than `keepAliveTimeout`, otherwise a kept-alive connection can be timed out * while waiting for its next request's headers. A configuration that breaks it * is repaired (headers = keep-alive + 1 s) and logged rather than refused. */ export declare function resolveServerTimeouts(options: Pick): { keepAliveTimeout: number; headersTimeout: number; requestTimeout: number; coerced: boolean; }; /** * Refuses a static-auth deployment that would expose the operator's * BoondManager credentials to anyone who can reach the port (issue #230). * * Static auth skips the Bearer check entirely, and off loopback the `Host` / * `Origin` validations are disabled too — the Docker image binds `0.0.0.0` by * default — so with no `apiKey` the endpoint is an anonymous proxy carrying * the operator's read *and* write rights. Loopback is exempt (only local * processes can connect), and `insecureStaticAuth` is the explicit, * named opt-out for a deployment whose network is the boundary. Throwing here * rather than warning is deliberate: a warning on stderr is exactly what an * operator running `docker run -e BOOND_HTTP_STATIC_AUTH=true` does not read. */ export declare function assertStaticAuthPolicy(options: HttpTransportOptions): void; /** * Constant-time comparison of a presented API key against the configured one. * * `timingSafeEqual` throws on buffers of different lengths, so a length * mismatch is answered by comparing the expected key against itself first — * the call still costs one full comparison — and then returning `false`. The * key's *length* is therefore observable, which is acceptable: it is not * secret for a random key (the README generates 32 random bytes), and the * alternative — hashing both sides to equalise lengths — reads to static * analysers as a password stored under a fast hash, which it is not. */ export declare function isApiKeyMatch(presented: string | null | undefined, expected: string): boolean; /** Max accepted request body size (1 MiB). MCP initialize payloads are tiny; * this caps the memory a single authenticated request can force us to buffer. */ export declare const MAX_BODY_BYTES: number; export declare function startHttpTransport(createServerFactory: () => McpServer, options: HttpTransportOptions): Promise; //# sourceMappingURL=http.d.ts.map