import type { AgentTool } from "../internal/harness.js"; import type { ImageContent, TextContent } from "../internal/llm.js"; import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js"; /** * The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP * tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored * and carries no axis fields) so `prepare-task` can fold them into the SAME irreversibleTools/egressTools * collection that `spec.tools` feeds — otherwise a destructive MCP tool would register no gate and a * no-policy deployment would silently AUTO-ALLOW it (MAJOR-1). The `name` is the namespaced tool name * (`__`), matching the keys the gate looks up. * * Trust note (design F): a server's `annotations` are SERVER-controlled and "not guaranteed faithful" (MCP * spec), so they may only ever TIGHTEN — `destructiveHint` adds an `ask`/suspend, `openWorldHint` adds an * egress tighten — never LOWER. A server's `readOnlyHint` does NOT lower `effect` (that would let a hostile * server lie on a mutating tool to escape repeat-safety + path-confinement). The ONLY trusted way to LOWER an * MCP tool below the fail-closed `write` default is the caller's {@link McpServerSpec.toolAxes} override * (caller = trust root). The synthetic first-party resource tools are the one exception — core sets their * `read` effect itself because it authored them. */ export interface McpToolAxis { /** Namespaced tool name (`mcp____` — design/108 ① CC parity). */ name: string; /** design/77 §4: `destructiveHint === true` → `"always"` (the gate always tightens allow→ask/suspend). */ irreversibility?: "always"; /** design/70: a non-readOnly tool with `openWorldHint === true` → egress (external write — never auto-allowed). */ egress?: true; /** * The repeat-safety / blast-radius class (NOT the data-trust axis — untrusted resource *content* is fenced * separately by delimitUntrusted). `read`/`idempotent` here means "not write-capable", which keeps the tool * off the LOUD ungated-write warning and out of path-confinement under a skill. Source is restricted by trust * (design F): the SYNTHETIC first-party resource tools set `read` as a fact core controls, and the caller's * {@link McpServerSpec.toolAxes} override may set any effect — a server's own `readOnlyHint` never sets it. */ effect?: ToolEffect; } /** Tools materialized from one or more MCP servers, plus a disposer to disconnect them. */ export interface MaterializedMcp { tools: AgentTool[]; /** * Per-tool safety axes derived from each tool's MCP `annotations` (design/77 §4 / design/70). One entry * per materialized tool that carries a tightening hint; `prepare-task` folds these into its * irreversibleTools/egressTools sets so a destructive/open-world MCP tool REGISTERS the gate + tightens * even on a no-policy deployment. Empty when no server advertised a relevant annotation. */ toolAxes: McpToolAxis[]; /** * MCP server `instructions` (design/64 §17.3, CC `# MCP Server Instructions`): the optional guidance a * connected server returns from `initialize`. The Runner injects these into the (stable) system prompt so * the model uses the server's tools correctly. One entry per server that provided non-empty instructions. */ serverInstructions: Array<{ server: string; text: string; }>; /** * G1 通告层续批 (CC `mcp_instructions_delta` parity) — the run-scoped instruction-delta ref the run * loop drains at turn boundaries (`spec.attachments.mcpInstructions` opt-in; `newTools` consumption * discipline: drained only when the announcement survived the byte cap intact). * - `pendingRemovals`: an INSTRUCTION-BEARING server whose transport closed mid-task (the CC * "servers have disconnected — their instructions above no longer apply" copy is instruction-scoped, * so instruction-less servers are not tracked). `dispose()` disarms first: task teardown is not a * disconnect. * - `pendingAdds`: the CC add lane ("# MCP Server Instructions" reminder). Structurally present for a * dynamic mid-task connect face; TODAY nothing populates it — sema connects all servers at prepare * and carries their initial instructions in the stable system prompt (design/64 §17.3, deliberate * CC deviation: prefix-cache-correct). */ instructionsDelta: { pendingAdds: Array<{ server: string; text: string; }>; pendingRemovals: string[]; }; /** * One entry per server that failed to connect / list its tools and was SKIPPED (fail-open, * design/29). The task proceeds with the healthy servers' tools — a single bad server (missing * stdio command, unreachable URL) must never brick every task in the scenario. The Runner forwards * these to `onError(phase:"mcp")`. Empty when every server connected. */ warnings: Error[]; /** * design/99 §E9 — per-server status, a PURE PROJECTION of THIS materialization (no new connection logic). * One entry per declared server. **Semantics = the moment of this task leg's materialize** — core never * persists MCP connections, so it is "did this leg connect", NOT a live session-health signal between tasks. * A consumer (service) surfacing it as session status must understand that. `toolNames` are the namespaced * (`__`) names exposed. */ statuses: McpServerStatus[]; dispose: () => Promise; } /** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo` * and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them * into a TUI/log must treat them as such (a hostile server could embed ANSI/break-out sequences). `toolNames` * are namespaced with a core-controlled `mcp____` prefix (design/108 ①). */ export interface McpServerStatus { name: string; status: "connected" | "failed"; serverInfo?: { name: string; version: string; }; toolNames?: string[]; error?: string; } /** * design/108 ① (CC 2.1.187 parity) — the GLOBAL `mcp__` marker prefix. CC names every MCP tool * `mcp____` so a consumer (permission allowlist, bypass-exempt, the flywheel's whitelist) can * identify an MCP-originated tool by prefix ALONE, without knowing the per-deployment server names. Exported so * the durable-resume canonicalizer (`canonicalToolName`) and downstream consumers share one source. */ export declare const MCP_PREFIX = "mcp__"; /** * F4-② (CC :320015/:320061): resolve a server's self-declared per-tool result-size threshold from its * `_meta`. Non-numeric/non-finite/non-positive → undefined (declaration ignored); valid → capped at * {@link MCP_META_RESULT_SIZE_CAP}. Exported for tests. */ export declare function resolveMcpDeclaredResultSize(meta: Record | undefined): number | undefined; /** * Apply the token gate to a mapped MCP content array (CC `$Mp`/`BMp` shape): text blocks consume a * cumulative char budget of `tokens×4`; past the budget they are sliced/dropped head-keep, and the CC * guidance note is appended as a final text block. Non-text blocks (images) pass through untouched — * same convention as the offload wrapper. Exported for tests. */ export declare function gateMcpOutput(content: Array, limitTokens?: number): Array; /** * design/116 W5-5 (CC 2.1.187 parity) — normalize a server/tool name segment to the API tool-name pattern * `^[a-zA-Z0-9_-]{1,64}$` by replacing every other character with `_`. CC does exactly this on BOTH segments * when composing `mcp____` (services/mcp/normalization.ts:17-23 `normalizeNameForMCP` + * mcpStringUtils.ts:50-52 `buildMcpToolName`), so a server advertising a dotted/spaced/unicode tool name can't * mint a tool name the provider rejects. NOTE: only the MODEL-FACING namespaced name is normalized — the raw * remote name is still used on the wire (`callTool`) and for `allowTools`/`toolAxes` keys (caller-facing keys * stay the server's own names). Same known collision property as CC: two remote names that normalize to the * same string collide (rare; CC accepts this). */ export declare function normalizeMcpName(name: string): string; /** * design/116 W5-1 — the max base64 size of ONE inline image (5MB). Value = CC 2.1.187 * `API_IMAGE_MAX_BASE64_SIZE` (constants/apiLimits.ts:22, "5 * 1024 * 1024 // 5 MB" — the hard Anthropic API * limit on a base64 image block; CC resizes/compresses down to fit UNDER it, imageResizer.ts). An MCP server * returning a bigger image previously passed straight through = a context/API bomb. */ export declare const MCP_IMAGE_MAX_BASE64: number; /** * 批③ image-pipeline — CC 2.1.x `constants/apiLimits.ts` values, shared by the MCP inline-image bound * AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit). * - `IMAGE_TARGET_RAW_SIZE` (apiLimits.ts:29): raw-byte target that guarantees the base64 encoding stays * under {@link MCP_IMAGE_MAX_BASE64} (raw × 4/3 = base64 → 3.75MB raw = 5MB base64). * - `IMAGE_MAX_WIDTH/HEIGHT` (apiLimits.ts:42-43): client-side resize box. The API internally resizes * above 1568px anyway; 2000px preserves a little extra quality while bounding token burn (~3x for a * full-resolution screenshot vs the box). */ export declare const IMAGE_TARGET_RAW_SIZE: number; export declare const IMAGE_MAX_WIDTH = 2000; export declare const IMAGE_MAX_HEIGHT = 2000; /** * design/116 W5-1 — bound an inline MCP image. HONEST DELTA vs CC: CC really resizes/downsamples an * oversized image with sharp (utils/imageResizer.ts `maybeResizeAndDownsampleImageBuffer`: ≤2000x2000px, * ≤3.75MB raw = 5MB base64, progressive JPEG/PNG quality ladder). Core deliberately takes NO image-processing * dependency (arch decision — sharp is a native module we won't force on every consumer), so instead of * resizing we BOUND: an image whose base64 exceeds {@link MCP_IMAGE_MAX_BASE64} is spilled to disk and * replaced by an explanatory text block with the size + path; a within-limit image passes through unchanged. */ /** * Optional image-resize seam (design/116 review CONFIRM-1, clay 拍 2026-07-02 加 seam 保体验): given an * over-limit image, return a smaller re-encoded one — or undefined when it can't. CC resizes with sharp * (imageResizer.ts: fit within 2000x2000, JPEG quality ladder) so the model still SEES a degraded image; * core must not hard-depend on a native image library, so the capability is injected (or auto-detected). */ export type McpImageResizer = (base64: string, mimeType: string) => Promise<{ base64: string; mimeType: string; } | undefined>; /** * 批③ image-pipeline — original vs displayed size of a processed image (CC `imageResizer.ts` * `ImageDimensions`). `display*` = what the model actually sees; the Read tool renders the CC coordinate- * mapping meta text from the ratio, so vision coordinate reasoning survives a downsample. */ export interface ImageDimensions { originalWidth: number; originalHeight: number; displayWidth: number; displayHeight: number; } /** Result of {@link ImageDownsampler}: re-encoded (or passed-through) image + dimensions when known. */ export interface DownsampledImage { base64: string; /** Full mime form (`image/jpeg`), ready for an ImageContent block. */ mimeType: string; dimensions?: ImageDimensions; } /** * 批③ — buffer-level image downsampler shared by the MCP inline-image bound AND the Read tool's image * branch. Returns undefined when the input can't be processed (corrupt/unsupported) — the caller keeps * its non-sharp path (spill for MCP, original-bytes for Read). A within-limits image passes through * UNCHANGED (with dimensions when known) — pass-through is not a failure. */ export type ImageDownsampler = (input: Buffer, mimeType: string) => Promise; /** Minimal structural slice of the sharp API the pipeline uses (fresh instance per operation — CC note: * reusing an instance after toBuffer() silently skips format conversions on some native builds). */ type SharpFactory = (input: Buffer) => SharpOps; interface SharpOps { metadata: () => Promise<{ width?: number; height?: number; format?: string; }>; resize: (w: number, h: number, o: { fit: "inside"; withoutEnlargement: boolean; }) => SharpOps; jpeg: (o: { quality: number; }) => SharpOps; png: (o: { compressionLevel: number; palette: boolean; }) => SharpOps; toBuffer: () => Promise; } /** * The CC downsample pipeline (imageResizer.ts `maybeResizeAndDownsampleImageBuffer`, called by * FileReadTool.ts:1097 `readImageWithTokenBudget`), ported over an injected sharp factory so the ladder * is unit-testable without the native dependency: * 1. fits already (raw ≤ 3.75MB AND ≤ 2000×2000) → pass through, dimensions attached; * 2. dims fit but bytes over → full-resolution compression first (PNG lossless/palette for PNGs, then * JPEG quality ladder 80/60/40/20) — preserves resolution when possible; * 3. dims over → constrain to the 2000×2000 box (aspect kept), then the same ladder on the resized * image if still over; last rung = ≤1000px wide JPEG q20 (returned without a further size check — * the caller's base64 gate stays the final arbiter, CC-identical shape); * 4. metadata without dimensions → JPEG q80 when over target, else pass through (CC branch). * Exported for tests (fake sharp factory); production entry = {@link sharpImageDownsampler}. */ export declare function makeImageDownsampler(sharp: SharpFactory): ImageDownsampler; export declare function sharpImageDownsampler(): Promise; /** * Default resizer factory: dynamic `import("sharp")` — a turnkey deployment with sharp installed gets the * CC experience automatically; without it this resolves to undefined and the spill path applies. Since * 批③ this is a thin base64 adapter over {@link sharpImageDownsampler} (single pipeline, CC ladder); * the contract is unchanged: undefined when sharp is missing, the input is corrupt, or even the lowest * ladder rung stays over the API limit. */ export declare function sharpImageResizer(): Promise; /** * design/116 W5-2 — compact, jq-friendly type signature for a `structuredContent` value, e.g. * `{title: string, items: [{id: number}]}`. Port of CC 2.1.187 `inferCompactSchema` (services/mcp/client.ts, * beside the structuredContent branch at client.ts:2676-2683): depth-2, first array element as the element * type, ≤10 object entries then `, ...`. */ export declare function inferCompactSchema(value: unknown, depth?: number): string; /** * Resolve the HTTP request headers for an MCP server, injecting the per-task end-user principal (design/62). * The principal is added **only** when the spec declares a `principalHeader` AND a `principal` is present; * it is injected **last** so it overrides any same-named static header; an absent principal sends no such * header (the MCP server must then default to deny/public, never admin). Pure + exported for testing and so a * consumer can verify what its config sends. The connection is task-scoped, so the principal is constant for * the connection's life and the HTTP transport carries these headers on every request. */ export declare function resolveMcpHttpHeaders(t: { headers?: Record; principalHeader?: string; }, principal?: string): Record | undefined; interface McpContentItem { type: string; text?: string; data?: string; mimeType?: string; /** `resource_link` block: a pointer to a server resource. */ uri?: string; name?: string; description?: string; /** `resource` block: an embedded resource (text or binary). */ resource?: { uri?: string; mimeType?: string; text?: string; blob?: string; }; } export declare function mapContent(content: Array, serverName?: string, imageResizer?: McpImageResizer): Promise>; /** * Connect to each MCP server, list its tools, and wrap them as AgentTools. * Tools are namespaced `mcp____` (CC parity) to avoid collisions. The model sees each * tool's real JSON-Schema (`inputSchema`), which the agent loop validates natively. * * Fail-open (design/29): a server that fails to connect or list tools is SKIPPED — its error is * returned in `warnings`, the healthy servers still materialize. A single misconfigured server * (bad stdio command, unreachable URL) never bricks the whole task. * * Call `dispose()` when the task finishes — connections are task-scoped, never persisted. */ export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer): Promise; export {}; //# sourceMappingURL=mcp.d.ts.map