AGENTS.md
+22 −16
https://raw.githubusercontent.com/cyanheads/mcp-ts-template/HEAD/AGENTS.md
− **Version:** 0.13.15− **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1 (protocol revisions 2026-07-28 and 2025-*)− **Zod:** ^4.6.5− | `/utils` | formatting, encoding, network (`fetchWithTimeout`, `withRetry` + `deadlineMs`/`defaultIsTransient`, `createPacer`, `httpErrorFromResponse`), pagination, overflow (`outlineOnOverflow`, `OUTLINE_VARIANT`, `selectSections`, `formatOutline`), logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |− | `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |− lines.push(`### ${item.name}`);− lines.push(`**ID:** ${item.id} | **Status:** ${item.status}`);− if (item.description) lines.push(item.description);− **`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 `− **Header-mirrored input (2026-07-28):** `headerParam(z.string(), 'Region')` designates an input property with `x-mcp-header`, so its value also rides an `Mcp-Param-Region` request header and an intermediary can read it without parsing the body. Mirroring, not relocation — the handler still reads the argument from the body, and nothing else about the field changes. Only a primitive-typed (`string`/`integer`/`number`/`boolean`) property statically reachable through a chain of `properties` keys qua− **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) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maximum string size (→ `InternalError`) → JS constructor name (`SyntaxError` → `ValidationE− **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. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument re− Skills live in `framework-skills/<name>/SKILL.md`; the full list is discoverable via the agent's skill registry at session start. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, and these are development-time skills, not skills for the agents that use a server. `skills/` stays free for that second kind.− | `bun run test:all` | Release gate: `rebuild` → `test:coverage` → `test:node` → `test:worker` → `test:integration`. `test:package` and `test:leaks` are separate lanes |− Lane configs live in `tests/config/` (`vitest.integration.ts`, `vitest.worker.ts`, `vitest.worker-bundle.ts`, `vitest.package.ts`, `vitest.benchmark.ts`, `vitest.performance.ts`, `vitest.leaks.ts`, `vitest.leak-sentinels.ts`). Two configs stay at the repo root: `vitest.config.ts`, the root `projects` list a bare `vitest` invocation discovers, and `vitest.config.base.mjs`, the published `./vitest.config` export.− `release-and-publish` here: verification gate (`devcheck`, `rebuild`, `test:all`, `test:package`), merge, tag, push, `bun publish`, `bun run publish-mcp`, then a GitHub Release via `bun run release:github` — no `manifest.json` here so no assets to attach, but the Release surfaces the tag's notes with the correct `v<VERSION>: <subject>` title. **Skip the Docker build/push step** — this framework package is consumed via npm, not as a container image.+ **Version:** 0.13.18+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (protocol revisions 2026-07-28 and 2025-*)+ **Zod:** ^4.6.7+ | `/utils` | formatting (`markdown()`, `escapeMarkdownInline`, `markdownLink`, `markdownImage`), encoding, network (`fetchWithTimeout`, `withRetry` + `deadlineMs`/`defaultIsTransient`, `createPacer`, `httpErrorFromResponse`), pagination, overflow (`outlineOnOverflow`, `OUTLINE_VARIANT`, `selectSections`, `formatOutline`), logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |+ | `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity`, `validateSkillTree`, `SkillProblem` | Definition validation; strict check of a published skill tree (`lint:packaging`) |+ skills: 'skills', // serve the published root skills/ over MCP (SEP-2640)+ **`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+ import { escapeMarkdownInline } from '@cyanheads/mcp-ts-core/utils';+ lines.push(`### ${escapeMarkdownInline(item.name)}`);+ lines.push(`**ID:** ${escapeMarkdownInline(item.id)} | **Status:** ${escapeMarkdownInline(item.status)}`);+ if (item.description) lines.push(escapeMarkdownInline(item.description));+ - **Upstream text renders as itself:** interpolate a string the tool did not author through `escapeMarkdownInline()` and an image or link field through `markdownImage()` / `markdownLink()` (`/utils`), so a CommonMark parse reads back the `structuredContent` value. Never defang (`https[:]//`, HTML entities). See `add-tool`.+ **`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 `+ **Header-mirrored input (2026-07-28):** `headerParam(z.string(), 'Region')` designates an input property with `x-mcp-header`, so its value also rides an `Mcp-Param-Region` request header and an intermediary can read it without parsing the body. Mirroring, not relocation — the handler still reads the argument from the body, and nothing else about the field changes. Only a `string`, `integer`, or `boolean` property (`z.number()` emits `number`, which the spec excludes — use `z.int()`) statically r+ **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+ Skills live in `framework-skills/<name>/SKILL.md`; the full list is discoverable via the agent's skill registry at session start. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, and these are development-time skills, not skills for the agents that use a server. `skills/` stays free for that second kind: the tree a server publishes, authored with `add-skill` and served by the `createApp` `skills` option.+ | `bun run test:soak` | Rebuilds, then sends 25,000 stateless HTTP `tools/call` requests (both protocol eras, 16 concurrent) to a built-package fixture on Bun and fails when the server's post-GC heap grows more than 64 MiB across the measured rounds — catches per-request retention that every correctness lane passes (#786) |+ | `bun run test:all` | Release gate: `rebuild` → `test:coverage` → `test:node` → `test:worker` → `test:integration`. `test:package`, `test:soak`, and `test:leaks` are separate lanes |+ Lane configs live in `tests/config/` (`vitest.integration.ts`, `vitest.worker.ts`, `vitest.worker-bundle.ts`, `vitest.package.ts`, `vitest.benchmark.ts`, `vitest.performance.ts`, `vitest.leaks.ts`, `vitest.leak-sentinels.ts`, `vitest.soak.ts`). Two configs stay at the repo root: `vitest.config.ts`, the root `projects` list a bare `vitest` invocation discovers, and `vitest.config.base.mjs`, the published `./vitest.config` export.+ **Release-age window:** this repo's `bunfig.toml` and `templates/_bunfig.toml` hold installs for the same window (`minimumReleaseAge`, 1 day). Consumers exempt only `@cyanheads/mcp-ts-core`, so its dependencies still go through their guard; with the windows equal, every floor and template pin a release ships is installable the day it ships. Change one and change the other, and when the template's window changes, the release's `agent-notes` tell consumers to set theirs before they update. Assume + `release-and-publish` here: verification gate (`devcheck`, `rebuild`, `test:all`, `test:package`, `test:soak`), merge, tag, push, `bun publish`, `bun run publish-mcp`, then a GitHub Release via `bun run release:github` — no `manifest.json` here so no assets to attach, but the Release surfaces the tag's notes with the correct `v<VERSION>: <subject>` title. **Skip the Docker build/push step** — this framework package is consumed via npm, not as a container image.