AGENTS.md
+20 −18
https://raw.githubusercontent.com/cyanheads/mcp-ts-core/HEAD/AGENTS.md
− **Version:** 0.13.18− **Engines:** Bun ≥1.4.0, Node ≥24.0.0− | `/mirror` | `defineMirror`, `sqliteMirrorStore`, `buildSchemaSql`, `openSqliteHandle`, `Mirror`, `MirrorStore`, `MirrorDefinition`, `SyncContext`, `SyncPage`, ... | MirrorService primitive (Tier 3, optional peer dep `better-sqlite3` on Node; `bun:sqlite` built-in on Bun); persistent self-refreshing local mirror of a bulk upstream dataset (embedded SQLite + FTS5). Node/Bun only |− **`skills`** — a folder of skill folders to serve through the SEP-2640 Skills extension, conventionally `'skills'`, the root tree a server publishes to the agents that use it (`add-skill`). A relative path resolves against the application root (`resolveAppRoot()`), the working directory only when none is found. Read once at startup with the SDK's non-strict loader: a missing or unreadable folder fails startup with a `ConfigurationError` naming the resolved path; a skill the SDK rejects is skippe− **Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), `input` (`InputHandlingOptions` — the tool-argument pre-validation switches `ignoreKeys` / `caseStyleAliases` / `coerce`; every stage on when unset, see Adding a Tool), `eventBus` (the `ServerEventBus` backing `subscriptions/listen`; defaults to − Per-request `McpServer` factory (security: SDK GHSA-345p-7cg4-v4c7). Requires `compatibility_flags = ["nodejs_compat"]` and `compatibility_date >= "2025-09-01"` in `wrangler.toml`. Only `in-memory`, `cloudflare-r2`, `cloudflare-kv`, `cloudflare-d1` storage in Workers. See `api-workers` skill for full details.− **`enrichment`** (optional): The success-path counterpart to `errors[]` — a `ZodRawShape` of agent-facing context (empty-result notices, query/filter echo, pagination totals, truncation disclosure) that must reach both client surfaces. Populate via `ctx.enrich(...)` (or `ctx.enrich.notice()` / `.total()` / `.echo()` / `.truncated()`) in the handler or service layer. The framework merges it into `structuredContent`, advertises `output.extend(enrichment)` as `outputSchema`, and mirrors it into a `− **Pre-validation:** an ordered step inside `parseToolArguments` rescues calls strict input would otherwise reject — drop client-added root keys (`_meta`, `tool_call_description`, `toolCallId`, any undeclared `_`-prefixed key) → rewrite key aliases (declared `inputAliases`, plus any undeclared key whose case-folded form names exactly one declared key) → parse → on failure, repair a JSON-stringified array or object, a safe integer where a string is expected (`8654467` → `"8654467"`), a string spel− **Advertised vs. parsed output:** `tools/list` advertises a widened schema (success fields optional, `error` declared) so an error envelope validates in strict clients; the framework still parses success results against the strict `output` (+ `enrichment`).− Handler receives `(params, ctx)` — URI on `ctx.uri` if needed. Optional `size` (bytes) for content size metadata. Large lists must use `extractCursor`/`paginateArray` from `/utils`.− **Keys match `^[a-zA-Z0-9_.\-/]+$` and may not contain `..`** — slashes are the namespace separator. A colon (`item:123`) throws `McpError(ValidationError)` on every call, in tests and in production alike.− **Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the wire hint for its reason: when a failure whose `data.reason` na− Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`, `requestCancelled`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.− For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { service, data })` from `/utils` — maps the full status table (401/403/408/422/429/5xx) and captures body + `Retry-After`.− **Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included; a handler that returns after the abort, partial result included, settles the same way) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maxi− **Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` is present; the numeric code and `data.issues` stay JSON-only. The message, hint, and reason render through `escapeMarkdownInline`, so a CommonMark parse of `content[]` reads each back exactly as `structur− Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()` overrides > env vars > `package.json` (reads `name` → `MCP_SERVER_NAME`, `version` → `MCP_SERVER_VERSION`). That manifest is the one at the application root — the nearest `package.json` above the entry module — never the launching client's working directory, and relative `logsPath`/`LOGS_DIR` values resolve against the same root.− **Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces — the declared `recovery` fill included — without transport auth, telemetry, or `data.requestId`. `toolContractSuite(definition, { success, errors })` from `/testing/vitest` registers reusable schema/handler/error-envelope conformance cases for a server's own tool definitions.+ **Version:** 0.13.22+ **Engines:** Bun ≥1.4.4, Node ≥24.0.0+ | `/mirror` | `defineMirror`, `sqliteMirrorStore`, `buildSchemaSql`, `openSqliteHandle`, `Mirror`, `MirrorStore`, `MirrorDefinition`, `SyncContext`, `SyncPage`, ... | MirrorService primitive (Tier 3, optional peer dep `better-sqlite3` on Node; `bun:sqlite` built-in on Bun ≥1.4.4); persistent self-refreshing local mirror of a bulk upstream dataset (embedded SQLite + FTS5). Node/Bun only |+ **`skills`** — a folder of skill folders to serve through the SEP-2640 Skills extension, conventionally `'skills'`, the root tree a server publishes to the agents that use it (`add-skill`). A relative path resolves against the application root (`resolveAppRoot()`) — the server's own package from any working directory, the installing project's root included — the working directory only when none is found. Read once at startup with the SDK's non-strict loader: a missing or unreadable folder fails + **Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), `input` (`InputHandlingOptions` — the tool-argument pre-validation switches `ignoreKeys` / `caseStyleAliases` / `coerce`; every stage on when unset, see Adding a Tool), `eventBus` (the `ServerEventBus` backing `subscriptions/listen`; defaults to + **Node.js floor** — on real Node.js, `createApp()` first compares `process.versions.node` with the framework's own `engines.node` (`package.json`) and, below it, fails through the startup `ConfigurationError` banner (stderr, exit 1) naming the running version and `process.execPath`, before any service, `setup()`, or transport. A prerelease is judged by its `X.Y.Z`, and a version string with no leading `X.Y.Z` is not checked. Bun, Deno, and Workers are never checked (each reports a `process.versi+ Per-request `McpServer` factory (security: SDK GHSA-345p-7cg4-v4c7). Requires `compatibility_flags = ["nodejs_compat"]` and `compatibility_date >= "2025-09-01"` in `wrangler.toml`. Only `in-memory`, `cloudflare-r2`, `cloudflare-kv`, `cloudflare-d1` storage in Workers. `DEV_MCP_AUTH_BYPASS` is honored on a Worker only when its environment is explicitly `development` or `testing`, declared by the `ENVIRONMENT` binding or by a `NODE_ENV` that reaches `process.env`; with the flag on and neither set,+ **`enrichment`** (optional): The success-path counterpart to `errors[]` — a `ZodRawShape` of agent-facing context (empty-result notices, query/filter echo, pagination totals, truncation disclosure) that must reach both client surfaces. Populate via `ctx.enrich(...)` (or `ctx.enrich.notice()` / `.total()` / `.echo()` / `.truncated()`) in the handler or service layer. The framework merges it into `structuredContent`, advertises `output.extend(enrichment)` as `outputSchema`, and mirrors it into a `+ **Pre-validation:** an ordered step inside `parseToolArguments` rescues calls strict input would otherwise reject — drop client-added root keys (`_meta`, `tool_call_description`, `toolCallId`, any undeclared `_`-prefixed key) → rewrite key aliases (declared `inputAliases`, plus any undeclared key whose case-folded form names exactly one declared key) → parse → on failure, repair a JSON-stringified array or object, a safe integer where a string is expected (`8654467` → `"8654467"`), a string spel+ **Advertised vs. parsed output:** `tools/list` advertises a widened schema (success fields optional, `error` declared) so an error envelope validates in strict clients. The framework's own parse is the only output check, and it runs once: the handler's return against the strict `output`, then the accumulated enrichment against the `enrichment` block, merged onto the parsed value. The SDK's post-handler re-validation of `structuredContent` is passed through, so a field whose parsed value is not v+ Handler receives `(params, ctx)` — URI on `ctx.uri` if needed. Optional `size` (bytes) for content size metadata. Large lists must use `extractCursor`/`paginateArray` from `/utils`. A template's `title` names the template in `resources/templates/list`; the entries its `list()` returns never inherit it (they keep their own `title`, or clients show their `name`), while every other template field still reaches them unless an entry sets its own.+ **Template variables arrive percent-encoded** — `§ 141.61` reaches `params` as `%C2%A7%20141.61` whether the client sent it encoded or raw. `createApp({ decodeResourceParams: true })` (also on `createWorkerHandler`) decodes each matched variable once before `params` validation, with or without a `params` schema: `decodeURIComponent` for `{v}`, `{.v}`, `{/v}`, `{?v}`, `{&v}`; `decodeURI` for `{+v}`, `{#v}`, so `%2F` stays encoded there; each element of an exploded variable; `+` stays `+`. A malfo+ **Keys match `^[a-zA-Z0-9_.\-/]+$` and may not contain `..`** — slashes are the namespace separator. A colon (`item:123`) throws `McpError(ValidationError)` on every call, in tests and in production alike. On `cloudflare-kv` and `cloudflare-r2` each value is stored under `<tenantId>:<key>`, capped at 512 and 1,024 bytes: a longer key throws `McpError(ValidationError)` naming the longest key the tenant can use, before any backend call (a batch, before its first). On `filesystem` a key's last `/`-+ **Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the wire hint for its reason: when a failure whose `data.reason` na+ Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`, `requestCancelled`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown; literals?: readonly string[] }` (`literals`: see Error-path parity).+ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { service, data })` from `/utils` — maps the full status table (401/403/408/422/429/5xx) and captures `Retry-After` and the body. The captured body (`data.body` / `data.responseBody`, from it or from `fetchWithTimeout`) stays server-side: server code reads it and the failure log records it, and every client envelope leaves it out unless the call passes `exposeBody: true` (`exposeErrorBody: true` on `fetchWithTimeout`).+ **Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included; a handler that returns after the abort, partial result included, settles the same way) → `McpError` code (preserved as-is; a tool, prompt, or resource handler's SDK `ProtocolError` keeps its code and object `data` the same way, so a resource's `Re+ **Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable · retry after … · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.retryAfter` / `data.requestId` is present — `retryAfter` as given, a finite non-negative number as itself and a one-line string as a code span, any other value left out; the numeric code and `data.iss+ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()` overrides > env vars > `package.json` (reads `name` → `MCP_SERVER_NAME`, `version` → `MCP_SERVER_VERSION`). That manifest is the one at the application root — the nearest `package.json` above the entry module — never the launching client's working directory, and relative `logsPath`/`LOGS_DIR` values resolve against the same root. A package declaring the framework in `dependencies`, `optionalDependencies`, or `peerD+ **Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces — the declared `recovery` fill included — without transport auth, telemetry, or `data.requestId`. The one failure it rejects with instead of returning is a handler's `UrlElicitationRequiredError` (`-32042`), which production hands back to the SDK as a protocol error: the rejection is that handed-back `ProtocolE