import type { MCPClientConfig, MCPClientState, MCPContentBlock, MCPEventListener, MCPInitializeResult, MCPPromptDefinition, MCPPromptMessage, MCPRequestOptions, MCPResource, MCPResourceTemplate, MCPToolDefinition, MCPToolResult, McpEra } from '../../types/connector/index.js'; import type { MCPClientId } from '../../types/ids/index.js'; export declare class MCPClient { readonly id: MCPClientId; private transport; private status; private serverInfo?; private serverCapabilities?; private era?; private connectedAt?; private error?; private pendingRequests; private nextRequestId; private notificationHandlers; private lifecycleListeners; /** Best-effort protocol cancellations still belong to this connection generation. */ private cancellationControllers; private log; private readonly config; private readonly requestTimeoutMs; private readonly eraCache; private readonly eraCacheKey; private readonly eraProbeTimeoutMs; /** * What each listed tool asked to mirror into `Mcp-Param-*` headers, * by tool name. * * Rebuilt by every `listTools()` and empty until the first one: a header * is only ever written from a schema this client has seen the server * publish, so a stale binding cannot outlive the listing that produced * it. */ private toolParamHeaders; constructor(config: MCPClientConfig); connect(): Promise; /** * One `initialize` round trip, offering the newest legacy version this * client speaks — never a per-version waterfall. The spec's own * backward-compatibility algorithm offers one version and honors * whatever the server answers with; a three-step retry loop would be * three times the latency for a path no server expects. * `protocol-negotiation.test.ts` counts `initialize` frames so this * stays true. * * Shared by `connect()`'s first handshake and by * {@link reinitializeLegacySession}'s recovery from a `404`d session — * the second call is not a special case, it is this same function run * again on a connection that already exists. */ private performLegacyInitializeHandshake; /** * A request answered `404` on a session-bearing legacy connection: the * legacy Streamable HTTP transports specify that a terminated session * answers this way, and the client's remedy is to drop it and run the * handshake again, exactly once, before giving up. * * Gated to legacy by construction, not by a flag: `hasSession()` is only * ever true on a connection that completed the legacy `initialize` * handshake in the first place — a modern connection never calls it (see * `probeDiscover`), so `resetSession`/`hasSession` have nothing to report * there. */ private isLegacySessionLostError; /** Drop the stale session and run the legacy handshake again, from scratch. */ private reinitializeLegacySession; /** * `request()`, with the legacy session recovery a live connection needs * that the initial handshake does not: `connect()`'s own `initialize` * call has no session yet to lose, so it goes through `request()` * directly and never through here. * * At most one recovery attempt. A retry that fails the same way is not * retried again — surfacing it is more honest than masking a second * genuine failure as a transient one. */ private requestWithSessionRecovery; /** * Which era this peer speaks, probed once and remembered per origin (or * per stdio command). * * Modern first. The wasted round trip against a legacy server is real * and is the reason the cache exists; the alternative is worse than * wasted latency, because a legacy server handed an era-ambiguous method * processes it under legacy semantics and fails confusingly, where a * probe fails cleanly and recovers. */ private resolveEra; /** * Ask the peer to describe itself, and report silence as an answer. * * Deliberately NOT `request()`. A probe differs from a request in the * two ways that matter: a timeout is a legitimate outcome rather than a * failure — the stdio spec says in so many words that a legacy server * may not respond at all — and a probe that gives up must not send * `notifications/cancelled`, because the peer it would be sent to is, by * hypothesis, one that did not understand the request in the first * place. Everything `request()` owns about cancellation ordering is left * exactly as it is rather than taught a second mode. */ private probeDiscover; /** * Finish a connection that resolved modern, with no handshake at all. * * The modern era has no `initialize` and no `notifications/initialized`, * so there is nothing here to await: the probe already carried the only * round trip a modern connection needs. `MCPInitializeResult` is * synthesised from the `DiscoverResult` so a host sees the same return * shape whichever era resolved — the era is this client's business, not * something every caller has to branch on. */ private completeModernConnection; /** * A `DiscoverResult` read as the `MCPInitializeResult` a host expects. * * `serverInfo` is a SHOULD on a discover result, not a MUST, and * `MCPInitializeResult.serverInfo` is required — so a server that does * not name itself is reported under the name the operator gave it. * Inventing a placeholder like "unknown" would put a word in the * server's mouth in the one field a person reads to identify it. */ private modernInitializeResult; disconnect(): Promise; isConnected(): boolean; /** * Which era and exact revision the last `connect()` negotiated. * * `undefined` before a connection has been negotiated. Which arm it * lands on is the peer's answer, not a configuration: `connect()` probes * for a modern server and falls back to the legacy handshake. */ getEra(): McpEra | undefined; getState(): MCPClientState; /** * Every tool this server publishes that this client is willing to expose. * * The second clause is new and it is the first place namzu refuses * something a server offered. A tool whose `inputSchema` carries an * invalid `x-mcp-header` annotation is excluded from the result, with the * tool name and the reason logged — the spec's requirement, and the * reason it is a requirement is that the annotation names a header this * client would otherwise write from a value it cannot vouch for. * * It lives HERE rather than in `MCPToolDiscovery` or the tool adapter * because the shipping CLI calls `listTools()` directly and never * touches discovery: validating one layer up would exempt the one caller * that matters most. */ listTools(options?: MCPRequestOptions): Promise; /** * Call one tool, and recover once from a server that says our mirrored * headers disagree with its current schema. * * `-32020` (`HeaderMismatch`) means the `Mcp-Param-*` headers this client * wrote are missing or wrong for the schema the server holds NOW — which * a server can legitimately cause by changing a tool between the listing * and the call. The spec's recovery is to re-read `tools/list` and retry * the request once. Exactly once: the retry goes through `request()` * rather than back through this method, so a second `-32020` surfaces to * the caller instead of starting a third round trip. */ callTool(name: string, args?: Record, options?: MCPRequestOptions): Promise; /** * Does this connection mirror tool parameters into `Mcp-Param-*` headers? * * Conditioned on the TRANSPORT, not on the era, because that is how the * spec conditions it: the feature belongs to Streamable HTTP, and a * client on another transport may ignore `x-mcp-header` entirely. stdio * has no headers to mirror into, and `http-sse` is the 2024-11-05 * transport, which predates the annotation by two years. * * The headers themselves are written only on a modern request — that is * `buildEnvelope`'s doing, not this predicate's — but the VALIDATION runs * in both eras on this transport, so a tool's admission does not silently * change shape the day the server behind it stops answering `initialize`. */ private mirrorsParamHeaders; /** * Drop the tools whose header annotations this client will not honour, * and remember what the rest asked to mirror. * * One malformed definition must not deny the others, which is why this * filters rather than throws: a server with fifty tools and one bad * annotation stays a server with forty-nine usable tools. */ private admitToolHeaderAnnotations; /** * `tools/call`, resolved past the MRTR `resultType` envelope. * * Absent or `"complete"` is the ordinary path — unchanged from before * this existed. `"input_required"` with nothing but a `requestState` is * retried exactly once, echoing that state byte-for-byte under a NEW * JSON-RPC id (a fresh `request()` call, which allocates one): the * spec's own words are that the client MAY retry immediately when there * is nothing for it to gather. Any other shape — `inputRequests` this * client cannot satisfy, or a second `input_required` after the one * retry — throws `MCPInputRequiredError` rather than looping or * returning something that looks like success; `mcpToolToToolDefinition` * catches it and turns it into a named, catchable `ToolResult` instead * of letting it reach a caller as an unexplained rejection. * * `requestState` travels back as a top-level `requestState` param, * alongside `name`/`arguments`, the same way `listAllPages` threads a * `cursor` — the one continuation-style field this codebase already has * a convention for. */ private callToolDecoded; listResources(options?: MCPRequestOptions): Promise; readResource(uri: string, options?: MCPRequestOptions): Promise; /** * The prompts a server publishes. * * A prompt is the server's own wording for a task it knows how to set * up — the half of MCP that is not tools. `MCPPromptDefinition` and * `MCPPromptArgument` were declared when the types were written and no * method ever asked for one, so a server offering prompts had them * silently ignored. * * Paged through the same reader as every other list, which is the point * of it being generic: a server that pages its prompts does not get * silently truncated to page one the way the tool list once was. */ listPrompts(options?: MCPRequestOptions): Promise; /** * Fetch one prompt, with its arguments filled in. * * Returns the messages the SERVER composed. They are data to be shown to * a model, never instructions to this client: a prompt arriving from a * remote server is exactly the untrusted-content case, and treating its * text as direction would let a server steer the agent by publishing a * prompt nobody asked to run. */ getPrompt(name: string, args?: Record, options?: MCPRequestOptions): Promise<{ description?: string; messages: MCPPromptMessage[]; }>; listResourceTemplates(options?: MCPRequestOptions): Promise; /** * Read a paged list to the end. * * The three list calls each sent an empty params object and returned * the first page, never sending a cursor and never reading the one * that came back. A server that pages its catalogue therefore * contributed only its first page: the rest were never registered, * never namespaced, never advertised — with no error, no warning and * no drift signal, because drift compares page one against page one. * The symptom is a model that does not use a tool it was told about, * which reads as model incompetence rather than a client bug. * * The page cap is a runaway guard, not a limit anyone should reach: a * server that keeps returning a cursor forever would otherwise loop * until the process dies. Hitting it is loud, because a silently * truncated catalogue is the failure being fixed here. */ private listAllPages; onNotification(handler: (method: string, params?: Record) => void): void; /** * Watch this client come up, go down, or fail. * * `MCPLifecycleEvent` and `MCPEventListener` were declared with the rest * of the MCP types and nothing ever emitted one, so a host could observe * a server dying only by noticing that calls had started failing. The * four transitions below already existed and already mutated `status`; * this adds no state, it just says out loud what the client already * knew. * * Returns an unsubscribe. `onNotification` above does not, which is the * bug this avoids repeating: a listener that cannot be removed keeps a * disposed host object alive for as long as the client lives. */ onLifecycle(listener: MCPEventListener): () => void; /** * A listener that throws must not take the transport down with it. * * These fire from inside transport callbacks and from the failure path * of `connect`, so an exception here would surface as a connection * error — blaming the server for a bug in the host's own observer. */ private emitLifecycle; private createTransport; /** * Send a JSON-RPC request and wait for its reply, bounded by a timer. * * There was no timer at all. On `streamable_http` the pending promise * happened to be bounded because `send()` awaits the fetch inside an * aborted scope, but on **stdio** — the default for local servers — * and on `http_sse` (whose reply arrives on a separate channel) a * server that wedged left the promise pending forever. Combined with * an executor that awaited tools unbounded, one unresponsive MCP * server hung the whole turn with no error and no `turn_failed`: not a * crash, just a process that stopped. */ private request; /** * `{ headers: {...} }` when the era produced any, `{}` otherwise — * spread into a `send()` options object so a send with no era headers * (a legacy era before 2025-06-18, and everything sent before an era is * resolved) gets no `headers` key at all rather than one holding an * empty object. */ private eraHeaderOptions; /** * `{ paramHeaders }` for a `tools/call` whose tool asked for mirrored * headers, `{}` for everything else — spread into the envelope input so * every other request is built from exactly the object it was built from * before this existed. * * Read from the last listing rather than passed down from `callTool`, so * the bindings are the ones that came with the schema the caller was * shown, whichever call site reached `request()`. */ private paramHeaderBindings; /** * `request()`'s full per-send header authority: the era's own headers * from `buildEnvelope`, this call's `MCPRequestOptions.headers` merged * over them — a collision resolves to the caller's value, except on the * headers the protocol itself owns — and this call's `bearerToken`, if * given, applied last as `Authorization` so it overrides a same-named * header from either of the other two sources. * * **The exception.** `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` * are not decoration: each mirrors a value the same request carries in * its body — the negotiated version in * `_meta['io.modelcontextprotocol/protocolVersion']`, the method, the * target's name — and a conforming modern server rejects a header that * disagrees with what it mirrors (`-32020`, HeaderMismatch). Letting a * caller's value win there would make the mismatched pair * {@link buildEnvelope} exists to render unconstructible constructible * again one layer up, and the failure would reach the host as an opaque * 400 with nothing pointing at the header that caused it. So a caller * header colliding with one of {@link CANONICAL_MCP_REQUEST_HEADERS} is * refused and warn-logged, naming it — in EVERY era, including a legacy * one old enough that {@link buildEnvelope} puts no headers of its own * on this request. Matching ignores case, because HTTP field names are * case-insensitive and `{ 'mcp-protocol-version': … }` alongside the * era's `MCP-Protocol-Version` would otherwise reach the wire as one * field holding both values, comma-joined. * * Every other header a caller sends is untouched, in both eras. * * `notify()` and `sendCancellation()` are internal, not caller-facing, * so they carry era headers alone — only a public request the caller * shaped can carry a per-call header or token. * * Returns `{}`, never `{ headers: undefined }`, when nothing applies, so * the zero-option path stays the exact object shape `request()` sent * before any of this existed. */ private requestAuthorityHeaders; /** * Does a cancelled request on THIS connection owe the peer a * `notifications/cancelled`? * * Everywhere except modern Streamable HTTP, yes. There, no: closing the * SSE response stream IS the cancellation signal, so the notification is * a second, redundant POST — and one the spec does not ask for. stdio * has no stream to close, so it still sends it, in every era. * * This predicate is the ONLY thing the modern era changes about * cancellation. The ordering guarantees in `request()` — who owns * cleanup, which cause wins, when the transport is aborted — are * untouched. */ private sendsCancellationNotification; /** Ask the peer to stop without letting cleanup become another hanging request. */ private sendCancellation; private abortCancellations; /** * Fail every in-flight request with the same reason. * * Previously only `disconnect()` did this, so a transport that dropped * on its own — process exit, socket reset, server crash — left callers * waiting on promises that could never settle. */ private rejectAllPending; private notify; private handleMessage; private requireConnected; } //# sourceMappingURL=client.d.ts.map