/** * `QarRegistryClient` — SDK-process-scoped local registries for consumer-built * agents and MCP servers. * * Surface: `client.qar.registerAgent(spec)` + `client.qar.registerMcp(name, config)`. * Returns branded handles (`RegisteredAgentRef` / `RegisteredMcpRef`) the * consumer can pass to `tasks.start` and slot into `spawnable_agents`. * * **Architectural posture.** The local agent body is never serialized to * QAR. Registration keeps the spec in-process so the SDK can execute the * agent locally (the dispatch loop is the SDK's; QAR remains the * runtime / orchestrator that issues `task.start` envelopes). For * `agent_id` referenced in a `task.start` payload, the SDK auto-fills * the wire's `agent_id` field from `ref.name` — that's the only thing * that crosses the wire. * * **Scope.** In-memory only; no persistence. The registries belong to a * `QodoClient` instance — a fresh client starts empty. Re-registering the * same name throws `QodoRegistryConflictError` unless `replace: true` is set, * so consumers can't silently shadow a previous registration. * * **No built-in MCPs.** The SDK ships with zero default MCP servers * (clean-slate departure from 1.x). The registries start empty; * `qar.registerMcp` is the canonical path for adding any consumer MCP. */ import type { ToolListEntry } from './mcp/McpClientPool.js'; import type { A2ARegistration, A2ARegistrationConfig, RegisteredA2ARef } from './a2a/registerA2A.js'; export interface RegisteredAgentRef { /** Discriminator — `'agent'` for refs minted by `registerAgent`. */ readonly kind: 'agent'; /** Stable name used as the wire `agent_id` when this ref is passed to `tasks.start`. */ readonly name: string; } export interface RegisteredMcpRef { readonly kind: 'mcp'; /** Stable name used to look up the MCP config. */ readonly name: string; } /** * Consumer-built agent shape registered locally via `qar.registerAgent`. * * `name` is the only required field — it doubles as the wire `agent_id` when * the ref is referenced in `tasks.start`. Every other field is optional and * forward-compatible — the inline-spec wire widens and the auto-fill matures * to populate matching wire fields. */ export interface AgentSpec { /** Unique-per-client identifier. Becomes the wire `agent_id` on dispatch. */ readonly name: string; /** Human-readable description for catalogs / dashboards. */ readonly description?: string; /** System / instruction prompt the SDK uses when executing the agent locally. */ readonly instructions?: string; /** Model selector hint. Final resolution is bound at execution time. */ readonly model?: string; /** * Tools available to the agent. Strings resolve against the consumer's * own MCP registry (`qar.registerMcp`) or are passed through as opaque * names the runtime resolves (e.g. server-managed tools). */ readonly tools?: readonly string[]; /** * Sub-agents this agent may spawn. Mixed shape: bare server-static agent * IDs (`'review-agent'`) sit alongside refs to locally-registered agents. */ readonly spawnable_agents?: readonly (string | RegisteredAgentRef)[]; /** * Whether the agent should use QAR's tenant-allowlisted managed tool * catalog. When `true`, server-side managed tools surface to the agent * without requiring consumer-side `qar.registerMcp` calls. */ readonly use_managed_tools?: boolean; /** Open metadata bag — keys not yet on the wire are stored locally only. */ readonly metadata?: { readonly [key: string]: unknown; }; } /** Subprocess MCP server — SDK spawns and pipes JSON-RPC over stdio. */ export interface StdioMcpConfig { readonly transport: 'stdio'; /** Executable to spawn (e.g. `'node'`, `'python'`, absolute binary path). */ readonly command: string; /** CLI arguments to pass to the executable. */ readonly args?: readonly string[]; /** * Environment for the spawned process. Values support `\${VAR}` substitution * against `process.env` at spawn time (mirrors the remote-transport headers * ergonomic). When omitted, the MCP SDK's `getDefaultEnvironment()` set * applies. */ readonly env?: { readonly [key: string]: string; }; /** Working directory for the spawned process. Defaults to the parent's CWD. */ readonly cwd?: string; } /** * Streamable HTTP MCP server — modern MCP wire protocol (HTTP POST for * sending; GET + SSE for receiving). The default choice for remote MCPs. * * Header values support `\${ENV_VAR}` substitution against `process.env` * at connect time (SDK1 ergonomic — `"Bearer ${HANDBOOK_API_TOKEN}"` * resolves to the live env value). */ export interface RemoteStreamableHttpMcpConfig { readonly transport: 'remote-streamable-http'; /** URL of the MCP endpoint. Pass-through to `StreamableHTTPClientTransport`. */ readonly url: string; /** * Extra headers applied to every outbound HTTP request. Merged into the * transport's `requestInit.headers`. Values support `\${VAR}` substitution. */ readonly headers?: { readonly [key: string]: string; }; /** * Escape hatch to forward arbitrary `RequestInit` options through to the * MCP SDK's fetch implementation (e.g. custom signal aggregation, mTLS * agent via undici dispatcher). Unset by default — most consumers only * need `url` + `headers`. */ readonly requestInit?: RequestInit; } /** * Legacy Server-Sent Events MCP server — separate POST endpoint for sending * and a GET SSE stream for receiving. Per the canonical MCP SDK docs this * transport is deprecated in favour of Streamable HTTP; preserved for * deployments that haven't migrated yet. * * Header values support `\${ENV_VAR}` substitution against `process.env`. */ export interface RemoteSseMcpConfig { readonly transport: 'remote-sse'; readonly url: string; readonly headers?: { readonly [key: string]: string; }; } /** * SDK1 `mcpServers` ergonomic — the URL-only shape consumers used in 1.x * (`mcpServers: { 'qodo-docs': { url, headers? } }`). Lacks the explicit * `transport` discriminator; the SDK normalizes it to * `'remote-streamable-http'` at register time. Preserves the SDK1 → SDK2 * migration ergonomic so consumers don't have to add * `transport: 'remote-streamable-http'` to every `mcpServers` block entry. * * The shape is discriminated by structural typing — presence of `url` * + ABSENCE of any `transport` field. Consumers wiring `transport: 'sdk'` * (in-process) still need the explicit form because the discriminator * is load-bearing for the handler vs. URL split. */ export interface UrlOnlyMcpConfig { /** `transport` is intentionally absent — normalized to `'remote-streamable-http'`. */ readonly transport?: undefined; readonly url: string; readonly headers?: { readonly [key: string]: string; }; /** Forwarded to the canonical MCP SDK's fetch implementation. */ readonly requestInit?: RequestInit; } /** * In-process MCP — consumer provides a handler. No subprocess, no network. * Useful for tests, custom in-app tools, or wrapping a library as an MCP. * * The auto-registered `qodo-skills` server uses this transport. A future * refactor may drive `qodo-skills` through `@modelcontextprotocol/sdk`'s * `Server` class. */ export interface SdkInProcessMcpConfig { readonly transport: 'sdk'; readonly handler: McpInProcessHandler; } /** JSON-RPC-style tool dispatch for in-process MCPs. */ export type McpInProcessHandler = (request: { readonly tool_name: string; readonly arguments: { readonly [key: string]: unknown; }; }) => Promise<{ readonly [key: string]: unknown; }> | { readonly [key: string]: unknown; }; /** * Discriminated union of supported MCP transports plus the SDK1 * URL-only shape that the SDK normalizes to * `'remote-streamable-http'` at register time. Five surface shapes; four * underlying dispatch paths. */ export type McpConfig = StdioMcpConfig | RemoteStreamableHttpMcpConfig | RemoteSseMcpConfig | SdkInProcessMcpConfig | UrlOnlyMcpConfig; export interface RegisterOptions { /** * When `true`, overwrite an existing registration with the same name. * Default `false` — same-name re-registration throws * `QodoRegistryConflictError`. Set `replace: true` for hot-reload flows * where the consumer rebuilds the registry from a fresh config snapshot. */ readonly replace?: boolean; } /** * Per-MCP registration options for {@link QarRegistryClient.registerMcp}. * Extends {@link RegisterOptions} with two projection-layer policies: * * - `prefixToolNames` — when `true` (default), tools discovered from * this MCP are projected to the LLM as `.` to avoid * collisions across registered MCPs. Set `false` for uniqueness- * guaranteed legacy cases (e.g., a single in-process MCP whose tool * names the consumer already pre-qualified). * - `unavailability` — cold-start behavior when the MCP's catalog * can't be fetched at `tasks.start*` time. Default `'fail'` raises * a typed `QodoMcpUnavailableError` (right for local sidecars where * unreachability = real config bug). `'warn'` omits the MCP's tools * from projection and emits an event (right for transient / third- * party remote MCPs where degradation is acceptable). */ export interface RegisterMcpOptions extends RegisterOptions { /** * Project tool names with the `.` prefix when sending the * agent's tool catalog to QAR. Default `true`. Set `false` to expose * upstream tool names verbatim (consumer guarantees uniqueness). */ readonly prefixToolNames?: boolean; /** * Cold-start unavailability policy. Default `'fail'` — `tasks.start*` * rejects with `QodoMcpUnavailableError` if the catalog isn't * populated by the projection-time settle window. `'warn'` omits the * MCP's discovered tools from projection without rejecting. */ readonly unavailability?: 'fail' | 'warn'; /** * Auto-bridge policy — controls whether the SDK's generic remote-MCP * bridge executes this MCP's tools. Default `true` (current behavior): * the bridge registers a fallback `tool.request` handler that resolves * this MCP's tools and dispatches them via {@link callMcpTool} when no * consumer-registered `onRequest` handler claims the call. * * Set `false` for **discovery-only** registration. The MCP's tool * schemas are still discovered and projected (slim-index / `mcpTools` * wire surface) exactly as with a normal registration — but the * auto-bridge does NOT route this MCP's tools. A consumer `onRequest` * handler returning `undefined` therefore falls through to the SDK's * deferred manual `respond()` path (the approval / HITL-pause contract) * instead of being claimed + executed by the bridge. * * Use this for consumer-owned local sidecars where the consumer — not * the SDK auto-bridge — owns execution: HITL-gated writes, approval-card * flows, write-gates. Without it, a consumer handler that returns * `undefined` to "hold for approval" is silently claimed and executed by * the bridge, bypassing the consumer's gate. The direct dispatch * surfaces ({@link callMcpTool}, {@link listMcpTools}, {@link ready}) * still work unchanged, so the consumer can execute the tool itself * once its gate clears. */ readonly bridge?: boolean; } export declare class QarRegistryClient { private readonly agents; /** * S9 — typed A2A endpoint registry. Lazy-allocated on first use so * consumers who never register an A2A endpoint pay no extra memory. * The bridge that consumes these registrations lives runtime-side in * QAR; the SDK keeps registration purely local. */ private a2aRegistry; private readonly mcps; /** * Per-MCP projection + bridge policy. Keyed by registered MCP name. * `prefixToolNames` defaults to `true`, `unavailability` defaults to * `'fail'`, and `bridge` defaults to `true` (auto-bridge ON). The map * stores a resolved entry per registered MCP; defaulting at read time * keeps the policy lookup branch-free for callers. Re-registration * with `replace: true` rewrites the entry; `unregisterMcp` deletes it. * * `bridge: false` is the discovery-only mode — the MCP's schemas still * project, but {@link resolveMcpForTool} excludes it from auto-bridge * routing so a consumer `undefined` falls through to deferred * `respond()` rather than bridge execution. */ private readonly mcpPolicies; /** * Connection pool for remote / stdio MCPs. Owns one * `@modelcontextprotocol/sdk` `Client` instance per registered MCP whose * config is NOT `transport: 'sdk'`. The pool is lazy — instantiated only * if/when a non-`sdk` config is registered, so consumers that never wire * a remote MCP pay no `@modelcontextprotocol/sdk` runtime cost beyond the * static import. * * SDK-transport (`'sdk'`) configs DO NOT enter the pool — their dispatch * route is the existing `client.skillsMcpServer` bridge in * `QodoClient.attachSkillsMcpBridge()` (and any consumer-built sibling). * Pooling them would double-register the qodo-skills tools against a * second dispatch path. */ private mcpPool; /** * Tally of registered **bridgeable** configs — transport is NOT `'sdk'` * AND not discovery-only (`bridge !== false`). Gates the generic * remote-MCP bridge attach. Discovery-only remote MCPs still enter the * pool for schema discovery but do not count here, so they never cause * the bridge to attach. */ private remoteMcpCount; /** * Register a locally-built agent. * * Returns a branded `RegisteredAgentRef` the consumer can pass into * `tasks.start({ agent_id: ref })` and other forward-looking surfaces * (`spawnable_agents`). * * @throws {QodoRegistryConflictError} when an agent with `spec.name` is * already registered and `opts.replace` is not `true`. */ registerAgent(spec: AgentSpec, opts?: RegisterOptions): RegisteredAgentRef; /** * Register an MCP server the SDK can dispatch tool calls into. * * Returns a branded `RegisteredMcpRef` the consumer can reference in * `AgentSpec.tools` (string form) or pass to forward-looking dispatch * surfaces (the `tool.request` → MCP routing handler). * * @throws {QodoRegistryConflictError} when an MCP with `name` is already * registered and `opts.replace` is not `true`. */ registerMcp(name: string, config: McpConfig, opts?: RegisterMcpOptions): RegisteredMcpRef; private applyRemoteMcpHook; /** * Remove a previously-registered agent. Returns `true` if the agent was * present, `false` if there was nothing to remove. Idempotent. */ unregisterAgent(name: string): boolean; /** Remove a previously-registered MCP. Idempotent (see `unregisterAgent`). */ unregisterMcp(name: string): boolean; /** Lookup — undefined when no agent is registered under that name. */ getAgent(name: string): AgentSpec | undefined; /** Lookup — undefined when no MCP is registered under that name. */ getMcp(name: string): McpConfig | undefined; /** Presence check — handy in tests and conditional-register flows. */ hasAgent(name: string): boolean; hasMcp(name: string): boolean; /** * Register an external A2A endpoint as a spawnable agent. * * Returns a {@link RegisteredA2ARef} the consumer slots into * graph spawnable_agents lists; QAR's outbound A2A bridge resolves * the ref to the registered endpoint on dispatch. * * @throws {QodoRegistryConflictError} on empty name, empty endpoint, * or duplicate name without `{ replace: true }`. */ registerA2A(name: string, config: A2ARegistrationConfig, opts?: RegisterOptions): RegisteredA2ARef; /** Lookup — undefined when no A2A endpoint is registered under that name. */ getA2A(name: string): A2ARegistration | undefined; /** Presence check. */ hasA2A(name: string): boolean; /** Snapshot of registered A2A endpoints. Stable order = registration order. */ listA2A(): readonly A2ARegistration[]; /** * Remove a previously-registered A2A endpoint. Idempotent — returns * `false` for unknown names and for callers that never registered an * A2A endpoint at all (the registry is lazy-allocated). */ unregisterA2A(name: string): boolean; /** Snapshot of currently-registered agents. Stable order = registration order. */ listAgents(): readonly AgentSpec[]; /** Snapshot of currently-registered MCPs (name + config). */ listMcps(): readonly { readonly name: string; readonly config: McpConfig; }[]; /** * Projection + bridge policy snapshot for the named MCP. Returns the * defaults (`prefixToolNames: true`, `unavailability: 'fail'`, * `bridge: true`) when the MCP is not registered — callers walk the * registered set first, so a missing entry would be a programming * error; defaulting keeps the signature `undefined`-free for the * common path. */ getMcpPolicy(name: string): { readonly prefixToolNames: boolean; readonly unavailability: 'fail' | 'warn'; readonly bridge: boolean; }; /** * Return the cached tool catalog for a registered remote / stdio * MCP. The catalog is populated during `register` (via the MCP * SDK's `Client.listTools()`); awaits any in-flight connect. * * `undefined` for unknown names and for `transport: 'sdk'` configs (the * in-process handler doesn't expose a structured tool catalog through * this surface — qodo-skills callers go through `client.skillsMcpServer`). */ listMcpTools(name: string): Promise; /** * Invoke a tool against a registered remote / stdio MCP via the * canonical `@modelcontextprotocol/sdk` `Client.callTool`. Awaits * the underlying connection; throws `QodoMcpClientError` on * connection failure and `QodoMcpToolError` on tool-side failure * (either transport error or `isError: true` in the MCP envelope). * * Returns the raw MCP `{ content, isError? }` envelope unchanged — the * SDK doesn't repackage; consumers handling the envelope already speak * MCP shape (`McpToolResult` mirror in `src/skills/mcp/types.ts`). */ callMcpTool(name: string, toolName: string, args: { readonly [key: string]: unknown; }): Promise<{ readonly [key: string]: unknown; }>; /** * Return the `mcpName` that owns a given tool name, or `undefined` * when no registered remote / stdio MCP advertises it. * * Resolution order: * 1. **Prefix form** — `mcpName.toolName` / `mcpName:toolName` / * `mcpName/toolName` returns `mcpName` verbatim when an MCP by * that name is registered (the suffix isn't validated against * the cached catalog — the round-trip itself surfaces unknown * tools as a typed `QodoMcpToolError`). * 2. **Bare form** — scans the cached tool indices across every * registered remote / stdio MCP and returns the first match. * Equal-name collisions across MCPs resolve to the * earlier-registered server (registration order). * * **Discovery-only MCPs are excluded.** MCPs registered with * `{ bridge: false }` are skipped in both the prefix and bare-name * paths, so the auto-bridge never claims their tools — a consumer * `undefined` falls through to deferred `respond()`. The bare-name scan * continues past a discovery-only MCP to a bridge-enabled MCP that * advertises the same tool name, so a same-name collision still routes * to the bridgeable server. Their schemas remain projected; only the * bridge's routing is affected (direct `callMcpTool` is untouched). * * Used by `QodoClient`'s generic MCP bridge to route `tool.request` * envelopes (carrying ToolCall.name strings) onto `callMcpTool`. */ resolveMcpForTool(toolNameOrPrefixed: string): { readonly mcpName: string; readonly toolName: string; } | undefined; resolveAgentId(idOrRef: string | RegisteredAgentRef): string; } //# sourceMappingURL=QarRegistryClient.d.ts.map