/** * `server` — MCP bootstrap & lifecycle. * * The single biggest dedup win in the fleet: every `src/index.ts` is described * as "byte-identical" — construct an {@link McpServer}, run a fixed list of * `registerXTools(server, client)` calls, print a stderr banner, wire * SIGINT/SIGTERM to tear the transport down, then connect over stdio. * * This module collapses that 30–120 lines/MCP into three calls: * - {@link createMcpServer} — build the server and apply the registrars. * - {@link withGracefulShutdown} — SIGINT/SIGTERM → cleanup → exit. * - {@link runMcp} — bootstrap + banner + connect + shutdown, the whole boot. * * It is deliberately transport- and domain-agnostic. The * deferred-config-error pattern (server boots before creds exist, so the host's * initial `tools/list` always succeeds and the first tool call surfaces the auth * error) is preserved by keeping client/transport construction in the caller's * `deps`: both Pattern-A (fetchproxy bridge) and Pattern-B (direct/bearer) MCPs * build their client themselves and pass it through, so neither is coupled in. */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; /** * Registers one or more tools onto a fresh {@link McpServer}. `deps` is whatever * the caller threaded through {@link createMcpServer} / {@link runMcp} — an API * client, a session registry, an app context, or `undefined` for tools (like * pure mortgage/affordability calculators) that need no shared state. * * May be async; {@link createMcpServer} awaits each registrar in order. */ export type ToolRegistrar = (server: McpServer, deps: TDeps) => void | Promise; /** Either the literal `'stdio'` (the default) or any SDK {@link Transport}. */ export type TransportSpec = 'stdio' | Transport; /** Options for {@link createMcpServer}. */ export interface CreateMcpServerOptions { /** Server name advertised to the host (e.g. `'splitwise-mcp'`). */ name: string; /** Server version advertised to the host (the `x-release-please-version`). */ version: string; /** The tool registrars to apply, in order. */ tools: ToolRegistrar[]; /** * Shared state passed as the second argument to every registrar — the API * client, app context, session registry, etc. Build it before calling so the * deferred-config-error pattern is preserved. Omit for registrar lists that * take no deps. */ deps?: TDeps; /** A one-line startup banner written to stderr (never stdout — stdout is the JSON-RPC channel). */ banner?: string; /** * Transport hint. Carried for API symmetry with {@link runMcp}; this function * never connects, so it only matters that the value is accepted. Defaults to * `'stdio'`. */ transport?: TransportSpec; /** * Append an {@link McpToolError}'s `hint` to the text a failing tool returns. * Default `true`. Set `false` only for a server that deliberately wants the * bare message. */ surfaceHints?: boolean; } /** * Wrap `server.registerTool` so every tool handler surfaces its error `hint`. * * Why this lives here rather than in each repo: the MCP tool boundary renders * only a thrown error's `message`. `McpToolError` has carried a `hint` — the * actionable half ("the available options are …", "set FOO_API_KEY") — since * the beginning, and {@link wrapToolError} is careful to preserve it, but * nothing ever rendered it, so every hint thrown from a tool handler was * invisible to the caller. Two repos had independently grown the same * hand-rolled wrapper before this landed. * * Handlers are invoked variadically because the SDK passes `(args, extra)` for * a tool with an `inputSchema` and `(extra)` for one without; forwarding * whatever arrived keeps both shapes intact. Both a synchronous throw and a * rejected promise are handled, since a handler may fail either way. * * Exported so `createTestHarness` can apply the same wrapper: a harness that * built a bare `McpServer` would show tests a different error surface than * production, which is the one thing a harness must never do. */ export declare function surfaceToolHints(server: McpServer): void; /** * Build an {@link McpServer}, print the optional stderr banner, and apply every * tool registrar (awaiting async ones) — but do **not** connect a transport. * Connecting is {@link runMcp}'s job (or the caller's), which keeps this usable * from tests and from custom boot sequences. */ export declare function createMcpServer(opts: CreateMcpServerOptions): Promise; /** Signals {@link withGracefulShutdown} listens for. */ export type ShutdownSignal = 'SIGINT' | 'SIGTERM'; /** Options for {@link withGracefulShutdown}. */ export interface GracefulShutdownOptions { /** * Extra cleanup to run on shutdown, before the server is closed — typically * `() => client.close()` to release the fetchproxy WebSocket bridge / direct * sockets so ports don't leak between host restarts. Receives the signal that * triggered shutdown. Errors are logged, never fatal. */ onSignal?: (signal: ShutdownSignal) => void | Promise; /** * Call `process.exit(0)` after cleanup completes. Default `true` (matches the * fleet's `process.exit(0)`). Set `false` in tests so the process survives. */ exit?: boolean; } /** * Wire SIGINT/SIGTERM to a one-shot graceful shutdown: run `onSignal` (e.g. * close the client/transport), close the server, then `process.exit(0)` (unless * `exit: false`). Idempotent — a second signal mid-shutdown is ignored, and a * throwing `onSignal`/`close` is logged but still exits cleanly so a wedged * cleanup can't hang the host. */ export declare function withGracefulShutdown(server: Pick, opts?: GracefulShutdownOptions): void; /** Options for {@link runMcp} — {@link createMcpServer}'s plus lifecycle wiring. */ export interface RunMcpOptions extends CreateMcpServerOptions { /** * Graceful-shutdown wiring. `true` (default) installs SIGINT/SIGTERM handlers * that close the server. `false` skips them. An object is passed straight to * {@link withGracefulShutdown} (e.g. `{ onSignal: () => client.close() }`). */ shutdown?: boolean | GracefulShutdownOptions; } /** * The whole boot in one call: build the server, apply registrars, print the * banner, install graceful-shutdown handlers, and connect the transport * (defaulting to a {@link StdioServerTransport}). Returns the connected server. * * Pattern-A and Pattern-B MCPs both build their client/transport in `deps` and * pass `onSignal: () => client.close()` via `shutdown`, so this stays agnostic * to how creds are resolved. */ export declare function runMcp(opts: RunMcpOptions): Promise; //# sourceMappingURL=index.d.ts.map