import { spawnSync } from 'node:child_process'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; /** * The thin stdio adapter — a stdio-to-daemon proxy that owns nothing. * * An MCP host spawns this bin; it speaks MCP over stdio and forwards its two * methods to the background daemon over loopback HTTP. It binds no port and * holds no bridge, so many adapters (one per host) share one daemon. Every * edge — readiness probe, child spawn, HTTP fetch — is injectable, which is * what makes the proxy testable without a real socket or daemon. */ /** * The version the operator currently publishes as canonical. * * Asked of OUR gateway, never of npm directly. A floating `@latest` in the * host config would let whoever controls the registry push code next to a * logged-in browser session; this keeps the decision on a surface we own, with * a rollback lever behind it. A failure returns null and changes nothing. */ declare function canonicalVersion(fetchImpl?: typeof fetch): Promise; /** The `fetch` shape this module needs — injectable so tests need no real HTTP. */ type FetchImpl = (url: string, init: { method: string; headers: Record; body: string; /** Cancels the request once its ceiling passes. Tests may ignore it. */ signal?: AbortSignal; }) => Promise<{ text: () => Promise; }>; interface EnsureRunningDeps { host?: string; port?: number; /** Probe for an existing daemon. Injected in tests; defaults to `tryConnect`. */ connect?: (host: string, port: number, timeoutMs: number) => Promise<{ destroy: () => void; } | null>; /** Start the daemon. Injected in tests; defaults to `spawnDaemon`. */ spawn?: (daemonEntry: string, args: string[]) => void; /** Injected sleep so the ready-poll never waits real milliseconds in tests. */ sleep?: (ms: number) => Promise; /** * Absolute path the spawner runs. Defaults to this bin, which starts the * inlined daemon when run with the serve subcommand. */ daemonEntry?: string; log?: (message: string) => void; attempts?: number; intervalMs?: number; /** * Reads the running daemon's takeover secret from disk. Injected in tests; * defaults to the real file. */ readTakeover?: () => string | null; /** * Ends the process holding the port. Injected in tests; defaults to the real * one. Never reached before the holder is confirmed to be our connector. */ killOwner?: (port: number) => boolean; /** * This adapter's own version. When set, a running daemon older than this is * replaced; one at this version or newer is left alone, so an older adapter * never downgrades a newer daemon. Unset means no reconciliation at all. */ selfVersion?: string; /** Pairing token authorising the shutdown. Defaults to `DATALAB_MCP_TOKEN`. */ token?: string; /** Injected in tests; defaults to reading `/bridge/health`. */ readVersion?: (base: string) => Promise; /** Injected in tests; defaults to `POST /mcp/shutdown`. */ shutdown?: (base: string, token: string) => Promise; /** Injected in tests; defaults to the authenticated authority probe. */ checkAuthority?: (base: string, token: string) => Promise; } /** * Ensure a daemon is up before the adapter starts proxying. A daemon already * on the port ends it; otherwise spawn this bin as serve and poll. Racing is * fine — whichever binds first wins and the losers exit 0 on EADDRINUSE. * * Exceeding the budget logs and returns rather than throws: showing * disconnected beats crashing the host's whole MCP session over a slow boot. */ declare function ensureDaemonRunning(deps?: EnsureRunningDeps): Promise; interface AdapterServerDeps { /** * Marks a request as started (+1) and finished (-1). The promotion watcher * uses it to swap builds only while nothing is in flight — re-execing mid * request would drop that call on the floor. */ onActivity?: (delta: 1 | -1) => void; host?: string; port?: number; /** Injected in tests; defaults to the global `fetch`. */ fetchImpl?: FetchImpl; log?: (message: string) => void; /** MCP handshake identity reported to the host. */ name?: string; version?: string; /** * Brings the daemon back when a call finds nothing listening. Injected in * tests; defaults to `ensureDaemonRunning`. */ revive?: (deps: EnsureRunningDeps) => Promise; } /** * Build the low-level MCP Server whose two handlers proxy to the daemon. The * low-level form fits because we own no tools and declare no static schema — * the catalog is discovered from the daemon and changes while running. */ declare function createAdapterServer(deps?: AdapterServerDeps): Server; /** * Read the daemon's notification stream, invoking the callback with each data * payload. Injectable so the subscription loop is testable without a real * stream. Heartbeat comment lines are not data and never reach the callback. */ type SubscribeImpl = (url: string, onEvent: (data: string) => void, signal: AbortSignal) => Promise; interface RunAdapterDeps extends AdapterServerDeps { /** Injected in tests; how often the canonical version is re-checked. */ watchIntervalMs?: number; /** Injected in tests; defaults to asking the gateway. */ canonical?: typeof canonicalVersion; /** Injected in tests; defaults to the real spawn. */ spawnImpl?: typeof spawnSync; /** Injected in tests; defaults to `ensureDaemonRunning`. */ ensure?: (deps: EnsureRunningDeps) => Promise; /** Injected in tests; defaults to a real stdio transport. */ transport?: Transport; /** Injected in tests; defaults to the real SSE subscription. */ subscribe?: SubscribeImpl; /** false pins this build — `--no-self-update`. Default promotes to canonical. */ selfUpdate?: boolean; } declare function runAdapter(deps?: RunAdapterDeps): Promise; interface CliHandlers { install: (sub: "install" | "uninstall", argv: readonly string[]) => Promise; serve: () => void; adapter: () => Promise; /** What we ship and what is placed — reads only, runs no connector. */ skills: (argv: readonly string[]) => Promise; /** * A word we do not know. Injected so the refusal is testable without a * process; production prints usage and exits non-zero. */ unknown: (sub: string) => void; } /** * Route argv to one of the subcommand handlers. Kept out of the entry so the * routing is testable with injected handlers. The default, with no subcommand * at all, is the adapter — what an MCP host spawns. * * A word we do not recognise is refused rather than treated as the default. * Falling through would answer a typo by speaking the MCP protocol at a person's * terminal: silent, since diagnostics go to stderr, and it would start a * connector and schedule its own replacement on the way. * * Flags still fall through. Hosts pass those, and only those — every config * this installer writes is `["-y", ""]`, so nothing it produces reaches * the refusal. */ declare function dispatchCli(argv: readonly string[], handlers: CliHandlers): Promise; export { type AdapterServerDeps, type CliHandlers, type EnsureRunningDeps, type FetchImpl, type RunAdapterDeps, createAdapterServer, dispatchCli, ensureDaemonRunning, runAdapter };