import type { McpCatalogCache } from "./catalog-cache.js"; import type { MCPConnectResult, MCPElicitHandler } from "./client.js"; import type { MCPServerConfig } from "./types.js"; /** * Process-wide pool of MCP connections that are safe for every session to * share, so one stdio child serves the whole daemon instead of one per session. * * The daemon hosts many `AgentSession`s at once — one per window, plus Ken chat * and Ken autopilot within each window — and each used to spawn its own child * for every configured stdio server. For a stateless proxy such as * `kencode-search` that is N identical processes doing identical work: measured * at 7 live children, ~43 MB each, purely duplicated. * * Sharing is the DEFAULT for stdio servers. A stdio connection has nothing * session-specific in it: every one is spawned with `cwd: os.homedir()`, so a * server cannot observe which project the caller is in even in principle, and * the pool key hashes command/args/env, so two configs differing in any way * that changes behaviour already get their own process. * * The two concerns that sharing has to answer are handled rather than avoided: * * - **Per-caller state.** Multiplexing many sessions over one connection would * let a server that remembers "the current project" leak one session's * context into another's answers. That is invisible from a config, so it * stays a declaration: `shared: false` opts such a server out and it keeps a * private child per session. * - **User prompting.** "Ask the user" has to name a window, so elicitation is * routed to the session with a tool call in flight (see `dispatchElicit`), * and cancelled rather than guessed when that is ambiguous. Because a * connection declares its elicitation capability once at initialize, callers * that can prompt and callers that cannot are keyed apart. * * The pool deliberately knows nothing about `MCPClientManager` — the connection * is supplied as a `SharedConnector`. That keeps the dependency pointing one * way (client → pool), and lets tests exercise refcounting against a fake. */ /** * Is this server shared across sessions in this process? * * Sharing is the DEFAULT for stdio servers, because a stdio MCP connection has * nothing session-specific in it: every one is spawned with `cwd: * os.homedir()` (see client.ts), so a server cannot observe which project the * caller is in even in principle, and the pool key hashes command/args/env, so * two configs that differ in any way that changes behaviour already get their * own process. What remained — elicitation, the one genuinely per-window * concern — is routed back to the calling session rather than dropped. * * `shared: false` opts a server out. That exists for a server that keeps * per-CALLER state across requests (a cursor, a selected workspace, an open * handle), where multiplexing two sessions over one connection would let one * session's state change the other's answers. That property is invisible from * a config, so it stays a declaration rather than a guess. * * HTTP servers are never shared. Sharing exists to collapse duplicate child * PROCESSES and an HTTP server has none, so it would save nothing while adding * real risk: OAuth tokens and `Mcp-Session-Id` are per-connection, and pooling * them would cross one session's authenticated identity with another's. */ export declare function isShareableServer(config: MCPServerConfig): boolean; /** One pooled connection, owned by the pool and torn down at zero references. */ export interface SharedConnector { connect(config: MCPServerConfig): Promise; dispose(): Promise; } export interface SharedAcquireOptions { catalogCache?: McpCatalogCache; modernProtocol?: boolean; /** Invoked by the connection when its server exits on its own. */ onClosed?: () => void; /** * The acquiring session's elicitation handler. Invoked only while that * session has a tool call in flight on this connection (see `dispatchElicit`). */ onElicit?: MCPElicitHandler; } /** * Builds the underlying connection the first time a config is pooled. The pool * supplies its own `onElicit`: a dispatcher that routes each request to the * session that asked for it, instead of any one session's handler. */ export type SharedConnectorFactory = (opts: SharedAcquireOptions) => SharedConnector; /** A session's claim on a shared connection. `release` is idempotent. */ export interface SharedServerHandle { result: MCPConnectResult; release: () => Promise; /** * Mark a tool call from this session as in flight, returning the function * that ends it. Elicitation arriving during the call is routed to this * session's window. */ beginCall: () => () => void; } export declare class SharedMcpPool { private entries; /** * Key on everything that changes what the connection IS, not merely what it * exposes: `hashServerConfig` covers command/args/env/url/transport, the name * keeps two differently-named aliases apart, and the protocol flag is * included because it changes the handshake — a session that opted into the * modern revision must not be handed a legacy-negotiated connection. * * Whether the caller can prompt is part of the key for the same reason. A * connection declares its elicitation capability once, at initialize, and * servers use that declaration to decide whether to ask at all — so a * headless caller (CLI, JSON mode) must not be handed a connection that * promises prompting nobody can deliver, and a windowed caller must not be * handed one that forecloses it. Splitting the key keeps each connection's * declaration honest; in practice a daemon's sessions are uniformly windowed * and the CLI's uniformly headless, so this still means one process. */ private keyFor; /** * Get-or-create the shared connection for `config` and claim a reference. * * Concurrent acquirers share one connect attempt: the entry and its pending * promise are registered synchronously before the first `await`, so a second * caller arriving mid-connect joins the in-flight attempt instead of spawning * a rival child. */ acquire(config: MCPServerConfig, createConnector: SharedConnectorFactory, opts?: SharedAcquireOptions): Promise; /** * Drop a connection whose server died, without touching its callers' claims. * * The entry is unregistered but NOT disposed: its transport is already gone, * and the sessions still holding handles will release them normally on their * own dispose. Guarded on identity so a death notice arriving after the entry * was already replaced cannot evict its successor. */ private evictDead; /** * Route an elicitation to the session that provoked it. * * A shared connection is held by many windows, so "ask the user" has to name * one. Elicitation only happens while a server is servicing a tool call, so * the session with a call in flight IS the one whose window should show the * form. When that is ambiguous — no call in flight, or two windows calling * the same server at once — the request is cancelled rather than guessed: * showing one project's consent form in another project's window would be a * worse failure than a declined prompt, and `cancel` is exactly what a server * already handles for a user who dismissed the dialog. */ private dispatchElicit; /** * Drop one session's claim. At zero the entry is unregistered SYNCHRONOUSLY * before the disposal await, so an `acquire` landing mid-teardown builds a * fresh connection instead of receiving a client that is already closing. */ private releaseKey; /** Live shared connections. Test/diagnostic use. */ get size(): number; /** Reference count for one config, or 0 when nothing is pooled for it. */ refCount(config: MCPServerConfig, opts?: SharedAcquireOptions): number; /** Tear every shared connection down regardless of refs (process shutdown). */ disposeAll(): Promise; } /** The daemon-wide pool. One per process, which is exactly the sharing scope. */ export declare const sharedMcpPool: SharedMcpPool; //# sourceMappingURL=shared-pool.d.ts.map