import { type McpServerDeclaration } from "@skaile/workspaces/core"; import type { CapabilityRegistry } from "./capability-registry.js"; /** Thrown by {@link ExternalMcpManager.addServer} when the id is already registered. */ export declare class McpServerAlreadyRegisteredError extends Error { readonly id: string; constructor(id: string); } /** * Minimal interface for secret resolution — keeps callers and tests free to * pass any `{ resolve }` object without constructing a real SecretProvider. */ export interface SecretProviderLike { resolve(ref: string): string | undefined; } /** * Resolve secret references (`env:VAR`, `forge:KEY`, etc.) in a string-keyed * record. Returns a new record with resolved values; entries that fail to * resolve are passed through unchanged. */ export declare function resolveRecordSecrets(record: Record, secrets: SecretProviderLike | undefined): Record; /** * Strict variant of {@link resolveRecordSecrets} for transport construction: a * value that starts with a known secret-ref prefix (`env:`, `mcp:`, ...) but * fails to resolve is a hard error. Forwarding the literal ref would put it on * the wire (e.g. `Authorization: mcp:github`) and surface a misleading upstream * rejection instead of the real cause — the credential is unavailable. * Resolved refs and non-ref literals behave exactly like the lenient variant. */ export declare function resolveRecordSecretsStrict(record: Record, secrets: SecretProviderLike | undefined, ctx: { serverId: string; kind: "header" | "env"; }): Record; /** * Load the declarative MCP servers from `skaile.yaml` and resolve any * `recipe:` bindings into absolute `/nix/store` paths, substituting * `${recipe:...}` markers in `command`, `args`, and `env`. * * Mirrors the resilience of the original wiring: a single broken declaration * (reserved id, recipe resolution failure, unresolved/invalid substituted * path) is logged and dropped — it never aborts the caller. Secret references * in `env` / `headers` are NOT resolved here; the spawn path resolves them at * transport-construction time so resolved secrets never sit in the returned * declaration objects. * * @returns The recipe-substituted declarations ready for spawn (or SDK * delegation in the legacy fallback path). */ export declare function resolveExternalMcpDeclarations(projectDir: string, sessionId?: string, managedCodex?: boolean): Promise; /** A single tool advertised by an MCP server's `tools/list`. */ export interface McpToolDescriptor { name: string; description?: string; inputSchema?: unknown; } /** * Minimal MCP client surface the manager depends on. The real * `@modelcontextprotocol/sdk` {@link Client} satisfies this structurally; tests * inject a fake via the {@link ExternalMcpManager} constructor's `connect` * argument so they need no live subprocess and run under both Bun and Node. */ export interface McpClientLike { listTools(): Promise<{ tools: McpToolDescriptor[]; }>; callTool(params: { name: string; arguments?: Record; }, resultSchema?: undefined, options?: { signal?: AbortSignal; }): Promise<{ content?: Array<{ type?: string; text?: string; }>; isError?: boolean; }>; close(): Promise; } /** Connects (spawns/handshakes) the MCP client for one declaration. */ export type McpClientConnector = (decl: McpServerDeclaration) => Promise; /** * Re-mints the backend-issued bearer for one `auth: backend` MCP server and * rewrites its provisioned `MCP____AUTH` secret in place. Returns `true` * when a fresh token was provisioned (the manager then rebuilds the transport * and reconnects), `false` when re-mint was impossible (the manager gives up * and propagates the original 401). Injected by the runner; absent in CLI mode. */ export type McpReauthorizer = (decl: McpServerDeclaration) => Promise; /** * True when an MCP **transport** error looks like an auth rejection. Remote * transports surface a 401 either as a `code: 401` (or `status: 401`) on the * thrown error or as a message carrying the HTTP `401` / a `WWW-Authenticate` * OAuth error code (`invalid_token` / `invalid_grant`). The bare word * "unauthorized" is intentionally NOT matched on its own — a tool's * business-logic error ("user unauthorized to access document") must not * trigger a pointless re-mint; real transport 401s carry the numeric status. */ export declare function isMcpAuthError(err: unknown): boolean; /** * Spawns and owns the lifecycle of every external MCP server for a session, * registering each server's tools into the {@link CapabilityRegistry}. * * One instance per session (one per `buildAgentResources` pass). Spawn after * recipe resolution; the subprocesses stay alive for the session and are killed * by {@link dispose} at session end. A spawn / connect failure for one server is * logged and skipped — it never aborts session startup (mirrors the * recipe-resolution "drop the server, continue" behavior). * * @category Runtime * @since 3.5.0 */ export declare class ExternalMcpManager { private readonly registry; private readonly servers; private readonly log; private readonly connect; private readonly reauthorize?; /** * @param registry Session capability registry to register MCP tools into. * @param secrets Secret provider for resolving `env:` / `forge:` refs in the * stdio subprocess env and sse/http headers. * @param sessionId Used for the logger instance slice. * @param connect Optional connector override (tests inject a fake client so * no live subprocess is spawned). Defaults to the real SDK * client + transport. * @param reauthorize Optional re-mint callback for `auth: backend` remote * servers. When present, a 401 from a tool call (or the * initial connect) triggers a re-mint + reconnect. */ constructor(registry: CapabilityRegistry, secrets: SecretProviderLike | undefined, sessionId?: string, connect?: McpClientConnector, reauthorize?: McpReauthorizer); /** True when at least one external server connected and registered tools. */ hasServers(): boolean; /** * Connect every declaration and register its tools. Per-server failures are * logged and skipped so a single bad server never blocks the session. */ start(declarations: McpServerDeclaration[]): Promise; /** * Connect one MCP server and register its tools into the capability registry. * Returns the server id and the registered tool capability names. * Throws if the id is already registered or if connect/listTools fails. */ addServer(decl: McpServerDeclaration): Promise<{ id: string; toolNames: string[]; }>; /** * Deregister a running MCP server's tools and close its client. * No-op when the id is not found. * * Drains any in-flight reauth first: a doReauthAndReconnect resolving after * splice would swap server.client to a freshly-connected client that nothing * then closes, leaking the transport/subprocess. */ removeServer(id: string): Promise; /** Summary of every currently-connected MCP server. */ listServers(): Array<{ id: string; transport: "stdio" | "sse" | "http"; toolCount: number; }>; /** * Connect, re-minting once on an initial-connect 401: the provisioned bearer * may already be stale by the time the server is reached. Only retries for * `auth: backend` servers with a reauthorizer wired. */ private connectWithReauth; /** * Re-mint the bearer and reconnect one server in place after a 401. Concurrent * 401s for the same server share a single round-trip via `reauthInFlight`. * Returns `true` once `server.client` points at a freshly-authed connection. */ private reauthAndReconnect; private doReauthAndReconnect; /** * Wrap a single MCP tool as a {@link DefinedCapability}. The capability name * is `mcp____` (the naming claude-sdk wildcards and prompts * depend on). The handler proxies the call to the server's LIVE client (read * lazily so a 401 reconnect is transparent); a tool error (`isError`) is * rethrown so the capability dispatch path surfaces it to the LLM, and a 401 * triggers a re-mint + reconnect followed by a single retry. */ private buildToolCapability; /** * Deregister every tool and close every client (kills the subprocesses). * Best-effort by default: a failed close on one server does not prevent the rest. * With strict:true, report close failures after all owned cleanup attempts. * * Drains any in-flight 401 reconnect first: a `doReauthAndReconnect` resolving * after teardown would set `server.client` to a freshly-connected client that * nothing then closes (leak). Awaiting `reauthInFlight` guarantees we close the * LATEST client. */ dispose(options?: { strict?: boolean; }): Promise; } //# sourceMappingURL=external-mcp.d.ts.map