@cyanheads/mcp-ts-core 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENTS.md +559 -0
  2. package/CLAUDE.md +8 -4
  3. package/README.md +33 -44
  4. package/changelog/0.9.x/0.9.14.md +31 -0
  5. package/changelog/0.9.x/0.9.15.md +52 -0
  6. package/changelog/template.md +1 -1
  7. package/dist/core/context.d.ts +119 -14
  8. package/dist/core/context.d.ts.map +1 -1
  9. package/dist/core/context.js +70 -1
  10. package/dist/core/context.js.map +1 -1
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/linter/rules/enrichment-rules.d.ts +41 -0
  15. package/dist/linter/rules/enrichment-rules.d.ts.map +1 -0
  16. package/dist/linter/rules/enrichment-rules.js +204 -0
  17. package/dist/linter/rules/enrichment-rules.js.map +1 -0
  18. package/dist/linter/rules/index.d.ts +1 -0
  19. package/dist/linter/rules/index.d.ts.map +1 -1
  20. package/dist/linter/rules/index.js +1 -0
  21. package/dist/linter/rules/index.js.map +1 -1
  22. package/dist/linter/rules/schema-rules.d.ts +4 -0
  23. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  24. package/dist/linter/rules/schema-rules.js +2 -2
  25. package/dist/linter/rules/schema-rules.js.map +1 -1
  26. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  27. package/dist/linter/rules/tool-rules.js +4 -0
  28. package/dist/linter/rules/tool-rules.js.map +1 -1
  29. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/tool-registration.js +7 -7
  31. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  32. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +81 -7
  33. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  35. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +23 -1
  36. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +118 -9
  38. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  39. package/dist/testing/index.d.ts +13 -0
  40. package/dist/testing/index.d.ts.map +1 -1
  41. package/dist/testing/index.js +21 -1
  42. package/dist/testing/index.js.map +1 -1
  43. package/dist/utils/internal/performance.d.ts +5 -1
  44. package/dist/utils/internal/performance.d.ts.map +1 -1
  45. package/dist/utils/internal/performance.js +10 -1
  46. package/dist/utils/internal/performance.js.map +1 -1
  47. package/dist/utils/telemetry/attributes.d.ts +2 -0
  48. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  49. package/dist/utils/telemetry/attributes.js +2 -0
  50. package/dist/utils/telemetry/attributes.js.map +1 -1
  51. package/package.json +5 -3
  52. package/scripts/build-changelog.ts +3 -1
  53. package/scripts/check-skills-sync.ts +42 -8
  54. package/skills/add-app-tool/SKILL.md +2 -2
  55. package/skills/add-export/SKILL.md +2 -2
  56. package/skills/add-service/SKILL.md +2 -2
  57. package/skills/add-tool/SKILL.md +85 -32
  58. package/skills/api-context/SKILL.md +68 -3
  59. package/skills/api-linter/SKILL.md +73 -2
  60. package/skills/design-mcp-server/SKILL.md +2 -1
  61. package/skills/git-wrapup/SKILL.md +22 -15
  62. package/skills/maintenance/SKILL.md +8 -7
  63. package/skills/orchestrations/SKILL.md +9 -5
  64. package/skills/orchestrations/workflows/maintenance-release.md +1 -1
  65. package/skills/polish-docs-meta/SKILL.md +1 -1
  66. package/skills/polish-docs-meta/references/agent-protocol.md +2 -2
  67. package/skills/polish-docs-meta/references/readme.md +3 -3
  68. package/skills/report-issue-framework/SKILL.md +8 -3
  69. package/skills/report-issue-local/SKILL.md +8 -3
  70. package/skills/setup/SKILL.md +5 -10
  71. package/templates/AGENTS.md +2 -1
  72. package/templates/CLAUDE.md +2 -1
  73. package/templates/_.mcpbignore +2 -0
  74. package/templates/changelog/template.md +1 -1
  75. package/templates/package.json +2 -1
  76. package/templates/src/mcp-server/tools/definitions/echo.tool.ts +10 -0
  77. package/dist/logs/combined.log +0 -4
  78. package/dist/logs/error.log +0 -2
  79. package/dist/logs/interactions.log +0 -0
package/AGENTS.md ADDED
@@ -0,0 +1,559 @@
1
+ # Developer Protocol
2
+
3
+ **Package:** `@cyanheads/mcp-ts-core`
4
+ **Version:** 0.9.15
5
+ **Engines:** Bun ≥1.3.0, Node ≥24.0.0
6
+ **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
7
+ **Zod:** ^4.4.3
8
+ **GitHub:** [cyanheads/mcp-ts-core](https://github.com/cyanheads/mcp-ts-core)
9
+ **npm:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
10
+ **Docker:** [ghcr.io/cyanheads/mcp-ts-core](https://ghcr.io/cyanheads/mcp-ts-core)
11
+
12
+ > **Developer note:** Never assume. Read related files and docs before making changes. Read full file content for context. Never try to edit a file before reading it.
13
+
14
+ ---
15
+
16
+ ## Consumers
17
+
18
+ This package serves two consumer paths. When making changes, know which audience your change affects:
19
+
20
+ | Path | On-ramp | Affected by changes to |
21
+ |:--|:--|:--|
22
+ | **Direct package import** — existing project pulls in the package | `bun add @cyanheads/mcp-ts-core` → `import { createApp, tool, z } from '@cyanheads/mcp-ts-core'` | Public API surface (`src/`) — existing consumers feel changes immediately on upgrade |
23
+ | **Init-scaffolded server** — fresh project bootstrapped from this repo's templates | `bunx @cyanheads/mcp-ts-core init [name]` copies `templates/` into the new directory | `templates/` — only affects newly scaffolded servers, not existing ones |
24
+
25
+ Both paths share the same public API. Init copies starter `package.json`, configs (`tsconfig`, `biome.json`, `vitest.config.ts`), `.env.example`, `Dockerfile`, `CLAUDE.md`/`AGENTS.md`, and example definitions. `_`-prefixed files (e.g. `_.gitignore`) drop the prefix on copy. After init, consult the `setup` skill.
26
+
27
+ ---
28
+
29
+ ## Core Rules
30
+
31
+ - **Logic throws, framework catches.** Pure, stateless `handler` functions, no `try/catch`. Plain `Error` works — framework catches, classifies, formats. Use `McpError(code, message, data, options?)` only when you need a specific JSON-RPC code or structured data; 4th arg `{ cause }` chains.
32
+ - **Full-stack observability.** The framework automatically instruments every tool/resource call — OTel span, duration/payload/memory metrics, structured completion log. Use `ctx.log` for additional domain-specific logging within handlers (external API calls, multi-step operations, business events). `requestId`, `traceId`, `tenantId` auto-correlated. No `console` calls.
33
+ - **Unified Context.** Handlers receive `ctx` with logging (`ctx.log`), tenant-scoped storage (`ctx.state`), optional protocol capabilities (`ctx.elicit`, `ctx.sample`), and cancellation (`ctx.signal`).
34
+ - **Decoupled storage.** `ctx.state` for tenant-scoped KV. Never access persistence backends directly.
35
+ - **Canvas tokens are capabilities, not tenant-scoped state.** A `canvasId` is a 10-char URL-safe token; possession grants full read/write/drop. Shareable between agents and across users in single-tenant deployments. Tools accept token in `input` (omit to create fresh) and return in `output`; collaboration is opt-in via token exchange.
36
+ - **Runtime parity.** All features work across `stdio`/`http`/Worker. Guard non-portable deps via `runtimeCaps` from `/utils` (`isNode`, `isBun`, `isWorkerLike`, `hasBuffer`, `hasProcess`, etc.). Prefer runtime-agnostic abstractions (Hono, Fetch APIs).
37
+ - **Definition linting is build-time only.** Run `bun run lint:mcp` (standalone) or `bun run devcheck` (gate). Not invoked at server startup — new lint rules are additive and never break deployed servers. Every diagnostic links to the rule reference in `api-linter` skill; see that skill for the full rule catalog.
38
+ - **Elicitation for missing input.** Use `ctx.elicit` when the client supports it.
39
+ - **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
40
+
41
+ ---
42
+
43
+ ## Exports Reference
44
+
45
+ | Subpath | Key Exports | Purpose |
46
+ |:--------|:------------|:--------|
47
+ | `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `Context`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `z` | Main entry point |
48
+ | `/worker` | `createWorkerHandler`, `CloudflareBindings` | Cloudflare Workers entry |
49
+ | `/tools` | `ToolDefinition`, `AnyToolDefinition`, `ToolAnnotations` | Tool definition types |
50
+ | `/resources` | `ResourceDefinition`, `AnyResourceDefinition` | Resource definition types |
51
+ | `/prompts` | `PromptDefinition` | Prompt definition type |
52
+ | `/tasks` | `TaskToolDefinition`, `isTaskToolDefinition` | Task tool escape hatch |
53
+ | `/errors` | `McpError`, `JsonRpcErrorCode`, `notFound`, `validationError`, `unauthorized`, ... | Error types, codes, and factory functions |
54
+ | `/config` | `AppConfig`, `config`, `parseConfig`, `parseEnvConfig`, `resetConfig`, `ConfigSchema`, `FRAMEWORK_NAME`, `FRAMEWORK_VERSION` | Zod-validated config, framework identity, env-var helper |
55
+ | `/auth` | `checkScopes` | Dynamic scope checking |
56
+ | `/storage` | `StorageService` | Storage abstraction |
57
+ | `/storage/types` | `IStorageProvider` | Provider interface |
58
+ | `/canvas` | `DataCanvas`, `CanvasInstance`, `CanvasRegistry`, `IDataCanvasProvider`, `DuckdbProvider`, `spillover`, `inferSchemaFromRows`, `assertReadOnlyQuery`, `quoteIdentifier`, ... | DataCanvas primitive (Tier 3, optional peer dep `@duckdb/node-api`); SQL/analytical workspace + source-agnostic spillover helper |
59
+ | `/utils` | formatting, encoding, network, pagination, logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |
60
+ | `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
61
+ | `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
62
+ | `/testing` | `createMockContext`, `getEnrichment` | Test helpers |
63
+ | `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
64
+
65
+ All subpaths prefixed with `@cyanheads/mcp-ts-core`. **†Tier 3 modules** require optional peer dependencies — see `package.json` `peerDependencies`. Tier 3 methods that lazy-load deps are **async**.
66
+
67
+ ### Import conventions
68
+
69
+ ```ts
70
+ // Framework (from node_modules) — z is re-exported, no separate zod import needed
71
+ import { tool, z } from '@cyanheads/mcp-ts-core';
72
+ import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
73
+
74
+ // Server's own code (via path alias)
75
+ import { getMyService } from '@/services/my-domain/my-service.js';
76
+ ```
77
+
78
+ Build configs exported for consumer extension: `tsconfig.json` extends `@cyanheads/mcp-ts-core/tsconfig.base.json`, `biome.json` extends `@cyanheads/mcp-ts-core/biome`, `vitest.config.ts` spreads from `@cyanheads/mcp-ts-core/vitest.config`.
79
+
80
+ ---
81
+
82
+ ## Entry Points
83
+
84
+ ### Node.js — `createApp(options)`
85
+
86
+ ```ts
87
+ import { createApp } from '@cyanheads/mcp-ts-core';
88
+ import { allToolDefinitions } from './mcp-server/tools/index.js';
89
+ import { allResourceDefinitions } from './mcp-server/resources/index.js';
90
+ import { allPromptDefinitions } from './mcp-server/prompts/index.js';
91
+
92
+ await createApp({
93
+ name: 'my-mcp-server', // overrides package.json / MCP_SERVER_NAME
94
+ version: '0.1.0', // overrides package.json / MCP_SERVER_VERSION
95
+ tools: allToolDefinitions,
96
+ resources: allResourceDefinitions,
97
+ prompts: allPromptDefinitions,
98
+ instructions: // server-level orientation, sent on every initialize
99
+ 'Pre-configured shortcuts:\n- `default` → production API\n' +
100
+ 'Other endpoints reachable via `connect({ baseUrl })`.',
101
+ extensions: { // SEP-2133 extensions advertised in capabilities
102
+ 'vendor/my-extension': { /* extension config */ },
103
+ },
104
+ setup(core) { // runs after core services init, before transport starts
105
+ initMyService(core.config, core.storage);
106
+ },
107
+ });
108
+ ```
109
+
110
+ **`instructions`** — Optional server-level orientation, surfaced on every `initialize` response as session-level system context. Use for deployment-specific guidance (connection aliases, regional notes, scope hints) instead of repeating in tool descriptions. Client adoption uneven but no downside when set.
111
+
112
+ ### Cloudflare Workers — `createWorkerHandler(options)`
113
+
114
+ ```ts
115
+ import { createWorkerHandler } from '@cyanheads/mcp-ts-core/worker';
116
+
117
+ export default createWorkerHandler({
118
+ tools: allToolDefinitions,
119
+ resources: allResourceDefinitions,
120
+ prompts: allPromptDefinitions,
121
+ instructions: (env) => `Region: ${env.ENVIRONMENT ?? 'production'}`, // string | (env) => string
122
+ setup(core) { initMyService(core.config, core.storage); },
123
+ extraEnvBindings: [['MY_API_KEY', 'MY_API_KEY']], // string values → process.env
124
+ extraObjectBindings: [['MY_CUSTOM_KV', 'MY_CUSTOM_KV']], // KV/R2/D1 → globalThis
125
+ onScheduled: async (controller, env, ctx) => { /* cron */ },
126
+ });
127
+ ```
128
+
129
+ 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.
130
+
131
+ ### Interfaces
132
+
133
+ `createApp()` returns `Promise<ServerHandle>`. `createWorkerHandler()` returns an `ExportedHandler`.
134
+
135
+ ```ts
136
+ interface CoreServices {
137
+ config: AppConfig;
138
+ logger: Logger;
139
+ storage: StorageService;
140
+ rateLimiter: RateLimiter;
141
+ llmProvider?: ILlmProvider;
142
+ speechService?: SpeechService;
143
+ supabase?: SupabaseClient;
144
+ }
145
+
146
+ interface ServerHandle {
147
+ shutdown(signal?: string): Promise<void>;
148
+ readonly services: CoreServices;
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Server Structure
155
+
156
+ ```text
157
+ src/
158
+ index.ts # createApp() entry point
159
+ worker.ts # createWorkerHandler() (if using Workers)
160
+ config/
161
+ server-config.ts # Server-specific env vars (own Zod schema)
162
+ services/
163
+ [domain]/
164
+ [domain]-service.ts # Domain service (init/accessor pattern)
165
+ types.ts # Domain types
166
+ mcp-server/
167
+ tools/definitions/
168
+ [tool-name].tool.ts # Tool definitions
169
+ index.ts # allToolDefinitions barrel
170
+ resources/definitions/
171
+ [resource-name].resource.ts # Resource definitions
172
+ index.ts # allResourceDefinitions barrel
173
+ prompts/definitions/
174
+ [prompt-name].prompt.ts # Prompt definitions
175
+ index.ts # allPromptDefinitions barrel
176
+ ```
177
+
178
+ **File suffixes:** `.tool.ts` (standard or task), `.resource.ts`, `.prompt.ts`, `.app-tool.ts` (UI-enabled), `.app-resource.ts` (UI resource linked to app tool).
179
+
180
+ ---
181
+
182
+ ## Adding a Tool
183
+
184
+ ```ts
185
+ import { tool, z } from '@cyanheads/mcp-ts-core';
186
+
187
+ export const myTool = tool('my_tool', {
188
+ description: 'Does something useful.',
189
+ annotations: { readOnlyHint: true },
190
+ input: z.object({ query: z.string().describe('Search query') }),
191
+ output: z.object({
192
+ items: z.array(z.object({
193
+ id: z.string().describe('Item ID'),
194
+ name: z.string().describe('Item name'),
195
+ status: z.string().describe('Current status'),
196
+ description: z.string().optional().describe('Item description'),
197
+ })).describe('Matching items'),
198
+ totalCount: z.number().describe('Total matches before pagination'),
199
+ }),
200
+ auth: ['tool:my_tool:read'],
201
+
202
+ async handler(input, ctx) {
203
+ const data = await fetchFromApi(input.query);
204
+ ctx.log.info('Query resolved', { query: input.query, resultCount: data.items.length });
205
+ return data;
206
+ },
207
+
208
+ format: (result) => {
209
+ const lines = [`**${result.totalCount} results**\n`];
210
+ for (const item of result.items) {
211
+ lines.push(`### ${item.name}`);
212
+ lines.push(`**ID:** ${item.id} | **Status:** ${item.status}`);
213
+ if (item.description) lines.push(item.description);
214
+ }
215
+ return [{ type: 'text', text: lines.join('\n') }];
216
+ },
217
+ });
218
+ ```
219
+
220
+ **Steps:** Create `src/mcp-server/tools/definitions/[name].tool.ts` (kebab-case) → use `tool('snake_case', {...})` with Zod `.describe()` on all fields → implement `handler(input, ctx)` (pure, throws on failure) → add `auth`/`format` if needed → register in `definitions/index.ts` → `bun run devcheck` → smoke-test with `bun run rebuild && bun run start:stdio` (or `start:http`).
221
+
222
+ **Schema constraint:** Input/output schemas must use JSON-Schema-serializable Zod types only. The MCP SDK converts schemas to JSON Schema for `tools/list` — non-serializable types (`z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`) cause a hard runtime failure. Use structural equivalents instead (e.g., `z.string()` with `.describe('ISO 8601 date')` instead of `z.date()`). The linter validates this at startup.
223
+
224
+ **Form-client safety:** Form-based clients (MCP Inspector, web UIs) send optional fields as empty strings, not `undefined`. Don't reject with `.min(1)` on optional fields — guard for meaningful values in the handler (`if (input.dateRange?.minDate && input.dateRange?.maxDate)`). Test with both omitted and empty-value payloads. When schema-level constraints (regex/length) need to surface in the JSON Schema, wrap in a union with a `z.literal('')` sentinel: `z.union([z.literal(''), z.string().regex(...).describe(...)])` — the linter exempts the literal variant from `describe-on-fields`.
225
+
226
+ **`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
227
+
228
+ - **Parity is enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or startup fails with a `format-parity` lint error.
229
+ - **Primary fix:** render the missing field in `format()`. Use `z.discriminatedUnion` for list/detail variants — each branch is validated separately.
230
+ - **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
231
+ - **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
232
+
233
+ **`enrichment`** (optional): The success-path counterpart to `errors[]` — a `ZodRawShape` of agent-facing context (empty-result notices, query/filter echo, pagination totals) that must reach both client surfaces. Populate via `ctx.enrich(...)` (or `ctx.enrich.notice()` / `.total()` / `.echo()`) in the handler or service layer. The framework merges it into `structuredContent`, advertises `output.extend(enrichment)` as `outputSchema`, and mirrors it into a `content[]` trailer — so it reaches `structuredContent`-only and `content[]`-only clients alike, with no `format()` entry. Keys must be disjoint from `output`; a required field never populated fails the effective-output parse. See `api-context`'s `ctx.enrich`.
234
+
235
+ **Task tools:** Add `task: true` for long-running async operations. Framework manages lifecycle: creates task → returns ID immediately → runs handler in background with `ctx.progress` → stores result/error → `ctx.signal` for cancellation. See `add-tool` skill for full example.
236
+
237
+ ---
238
+
239
+ ## Adding a Resource
240
+
241
+ **Tool coverage.** Not all MCP clients expose resources — many are tool-only. Verify that resource data is also reachable via the tool surface before relying on resources as an access path.
242
+
243
+ ```ts
244
+ import { resource, z } from '@cyanheads/mcp-ts-core';
245
+
246
+ export const myResource = resource('myscheme://{itemId}/data', {
247
+ description: 'Retrieve item data by ID.',
248
+ mimeType: 'application/json',
249
+ params: z.object({ itemId: z.string().describe('Item identifier') }),
250
+ auth: ['item:read'],
251
+ async handler(params, ctx) {
252
+ return { id: params.itemId, status: 'active' };
253
+ },
254
+ list: async () => ({
255
+ resources: [{ uri: 'myscheme://all', name: 'All Items', mimeType: 'application/json' }],
256
+ }),
257
+ });
258
+ ```
259
+
260
+ 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`.
261
+
262
+ ---
263
+
264
+ ## Context
265
+
266
+ ```ts
267
+ interface Context {
268
+ readonly requestId: string;
269
+ readonly timestamp: string;
270
+ readonly tenantId?: string;
271
+ readonly traceId?: string;
272
+ readonly spanId?: string;
273
+ readonly auth?: AuthContext;
274
+ readonly log: ContextLogger; // auto-correlated: requestId, traceId, tenantId
275
+ readonly state: ContextState; // tenant-scoped KV storage
276
+ readonly elicit?: (message: string, schema: z.ZodObject<any>) => Promise<ElicitResult>;
277
+ readonly sample?: (messages: SamplingMessage[], opts?: SamplingOpts) => Promise<CreateMessageResult>;
278
+ readonly notifyResourceListChanged?: (() => void) | undefined; // resource list changed
279
+ readonly notifyResourceUpdated?: ((uri: string) => void) | undefined; // resource content changed
280
+ readonly signal: AbortSignal; // cancellation
281
+ readonly progress?: ContextProgress; // present when task: true
282
+ readonly uri?: URL; // present for resource handlers
283
+ readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
284
+ recoveryFor(reason: string): { recovery: { hint: string } } | {}; // opt-in contract resolver
285
+ }
286
+ ```
287
+
288
+ ### `ctx.log`
289
+
290
+ Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
291
+
292
+ ### `ctx.state`
293
+
294
+ Tenant-scoped KV. Accepts any serializable value — no manual `JSON.stringify`/`JSON.parse` needed.
295
+
296
+ ```ts
297
+ await ctx.state.set('item:123', { name: 'Widget', count: 42 });
298
+ await ctx.state.set('item:123', data, { ttl: 3600 }); // with TTL (seconds)
299
+ const item = await ctx.state.get<Item>('item:123'); // T | null
300
+ const safe = await ctx.state.get('item:123', ItemSchema); // Zod-validated T | null
301
+ await ctx.state.delete('item:123');
302
+ const values = await ctx.state.getMany<Item>(['item:1', 'item:2']); // Map<string, T>
303
+ const page = await ctx.state.list('item:', { cursor, limit: 20 }); // { items, cursor? }
304
+ ```
305
+
306
+ Throws `McpError(InvalidRequest)` if `tenantId` missing. Tenant ID resolution:
307
+
308
+ | Mode | `tenantId` source |
309
+ |:-----|:------------------|
310
+ | stdio (any auth) | `'default'` |
311
+ | HTTP + `MCP_AUTH_MODE=none` | `'default'` (single-tenant by design) |
312
+ | HTTP + `MCP_AUTH_MODE=jwt`/`oauth` | JWT `'tid'` claim — fail-closed if absent |
313
+
314
+ ### `ctx.elicit` / `ctx.sample`
315
+
316
+ Check for presence before calling:
317
+
318
+ ```ts
319
+ if (ctx.elicit) {
320
+ const result = await ctx.elicit('What format?', z.object({
321
+ format: z.enum(['json', 'csv']).describe('Output format'),
322
+ }));
323
+ if (result.action === 'accept') useFormat(result.content.format);
324
+ }
325
+ ```
326
+
327
+ ### `ctx.progress`
328
+
329
+ Present when `task: true`. Methods: `setTotal(n)`, `increment(amount?)`, `update(message)`.
330
+
331
+ See `api-context` skill for full details.
332
+
333
+ ---
334
+
335
+ ## Error Handling
336
+
337
+ **Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable? }]` 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 single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters.
338
+
339
+ ```ts
340
+ errors: [
341
+ { reason: 'no_match', code: JsonRpcErrorCode.NotFound,
342
+ when: 'No PMID returned data',
343
+ recovery: 'Try pubmed_search_articles to discover valid PMIDs first.' },
344
+ { reason: 'queue_full', code: JsonRpcErrorCode.RateLimited,
345
+ when: 'Queue at capacity', retryable: true,
346
+ recovery: 'Wait 30 seconds before retrying or reduce batch size.' },
347
+ ],
348
+ async handler(input, ctx) {
349
+ // Static recovery — pulled from the contract via ctx.recoveryFor.
350
+ if (queue.full()) throw ctx.fail('queue_full', undefined, { ...ctx.recoveryFor('queue_full') });
351
+ // Dynamic recovery — interpolate runtime context, override the contract default.
352
+ if (!matched) throw ctx.fail('no_match', `No data for ${input.pmids.length} PMIDs`, {
353
+ pmids: input.pmids,
354
+ recovery: { hint: `Use pubmed_search_articles to discover valid PMIDs.` },
355
+ });
356
+ }
357
+ ```
358
+
359
+ **`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`. Works in services: `throw validationError(msg, { reason: 'X', ...ctx.recoveryFor('X') })`. Opt-in — author spreads explicitly.
360
+
361
+ **Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
362
+
363
+ **Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code): use error factories.
364
+
365
+ ```ts
366
+ import { notFound, validationError } from '@cyanheads/mcp-ts-core/errors';
367
+ throw notFound('Item not found', { itemId: '123' });
368
+ throw validationError('Missing required field: name', { field: 'name' });
369
+ ```
370
+
371
+ Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.
372
+
373
+ 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`.
374
+
375
+ **Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: `McpError` code (preserved as-is) → JS constructor name (`TypeError` → `ValidationError`) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name → `InternalError` fallback.
376
+
377
+ **Error-path parity.** Tool errors: `content[]` carries markdown with `data.recovery.hint`; `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope.
378
+
379
+ **Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`. See `api-linter` skill.
380
+
381
+ See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
382
+
383
+ ---
384
+
385
+ ## Auth
386
+
387
+ Inline `auth` on definitions (primary pattern): `auth: ['tool:my_tool:read']`. Handler factory checks scopes before calling handler. Dynamic scopes via `checkScopes(ctx, [...])` from `/auth`.
388
+
389
+ **Scope naming:** colon-delimited strings. Conventions used in this codebase:
390
+
391
+ | Surface | Pattern | Example |
392
+ |:--------|:--------|:--------|
393
+ | Tools | `tool:<snake_name>:<verb>` | `tool:inventory_search:read` |
394
+ | Resources | `resource:<kebab-name>:<verb>` *or* domain-led `<domain>:<verb>` | `resource:echo-app-ui:read`, `inventory:read` |
395
+
396
+ Pick one convention per server and stay consistent. Verbs are typically `read`, `write`, `admin`.
397
+
398
+ **Modes** (`MCP_AUTH_MODE`): `none` (default) | `jwt` (local secret via `MCP_AUTH_SECRET_KEY`) | `oauth` (JWKS via `OAUTH_ISSUER_URL`, `OAUTH_AUDIENCE`). See `api-auth` skill for claims, CORS, and detailed config.
399
+
400
+ **Granted scopes** union `scp`, `scope`, and `mcp_tool_scopes` JWT claims. `mcp_tool_scopes` is the OIDC escape hatch (Authentik, Keycloak < 26.5, Zitadel). `MCP_AUTH_DISABLE_SCOPE_CHECKS=true` bypasses scope checks while preserving auth-context verification (signature/audience/issuer/expiry). Logs `WARNING` at startup.
401
+
402
+ ---
403
+
404
+ ## Configuration
405
+
406
+ ### Core config
407
+
408
+ 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`).
409
+
410
+ | Category | Key Variables |
411
+ |:---------|:-------------|
412
+ | Transport | `MCP_TRANSPORT_TYPE` (`stdio`\|`http`), `MCP_HTTP_PORT`, `MCP_HTTP_HOST`, `MCP_HTTP_ENDPOINT_PATH` |
413
+ | Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_*` |
414
+ | Storage | `STORAGE_PROVIDER_TYPE` (`in-memory`\|`filesystem`\|`supabase`\|`cloudflare-r2`\|`cloudflare-kv`\|`cloudflare-d1`) |
415
+ | LLM | `OPENROUTER_API_KEY`, `OPENROUTER_APP_URL/NAME`, `LLM_DEFAULT_*` |
416
+ | Telemetry | `OTEL_ENABLED`, `OTEL_SERVICE_NAME/VERSION`, `OTEL_EXPORTER_OTLP_*` |
417
+
418
+ ### Server config (separate schema)
419
+
420
+ Own Zod schema for domain-specific env vars. **Never merge with core's schema.** Lazy-parse — Workers inject env at request time via `injectEnvVars()`, so no top-level `process.env` reads. Prefer `parseEnvConfig(schema, envMap)` from `/config` over `schema.parse(...)` — it maps schema paths to env var names (`MY_API_KEY is missing` vs. `apiKey: expected string`). Raw `ZodError` from `setup()` is still caught and converted, but messages are worse. See `api-config` skill.
421
+
422
+ ---
423
+
424
+ ## Testing
425
+
426
+ ```ts
427
+ import { describe, expect, it } from 'vitest';
428
+ import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
429
+ import { myTool } from '@/mcp-server/tools/definitions/my-tool.tool.js';
430
+
431
+ describe('myTool', () => {
432
+ it('returns expected output', async () => {
433
+ const ctx = createMockContext();
434
+ const result = await myTool.handler(myTool.input.parse({ query: 'hello' }), ctx);
435
+ expect(result.result).toBe('Found: hello');
436
+ });
437
+ });
438
+ ```
439
+
440
+ **`createMockContext` options:** `createMockContext()` (minimal), `{ tenantId: 'test-tenant' }` (enables state), `{ sample: vi.fn() }`, `{ elicit: vi.fn() }`, `{ progress: true }` (task progress).
441
+
442
+ **Fuzz testing:** `fuzzTool`/`fuzzResource`/`fuzzPrompt` from `/testing/fuzz` generate valid and adversarial inputs from Zod schemas via `fast-check`, then assert handler invariants (no crashes, no prototype pollution, no stack trace leaks). Returns a `FuzzReport` for custom assertions.
443
+
444
+ ```ts
445
+ import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';
446
+
447
+ it('survives fuzz testing', async () => {
448
+ const report = await fuzzTool(myTool, { numRuns: 100 });
449
+ expect(report.crashes).toHaveLength(0);
450
+ expect(report.leaks).toHaveLength(0);
451
+ expect(report.prototypePollution).toBe(false);
452
+ });
453
+ ```
454
+
455
+ Options: `numRuns` (valid inputs, default 50), `numAdversarial` (adversarial inputs, default 30), `seed` (reproducibility), `timeout` (per-call ms, default 5000), `ctx` (`MockContextOptions` for stateful handlers). Also exports `zodToArbitrary(schema)` for custom property-based tests and `ADVERSARIAL_STRINGS` for targeted injection testing.
456
+
457
+ **Vitest config:** Extend core config, add `@/` alias: `resolve: { alias: { '@/': new URL('./src/', import.meta.url).pathname } }`. Construct deps in `beforeEach`. Re-init services per suite.
458
+
459
+ ---
460
+
461
+ ## API Quick References
462
+
463
+ Detailed method signatures, options, and examples live in skill files. Read the relevant skill before starting a task it covers.
464
+
465
+ ### Skill versioning
466
+
467
+ Each `skills/<name>/SKILL.md` carries `metadata.version` in frontmatter. The `maintenance` skill's Phase A uses this to sync consumer copies — replaces the **entire skill directory** as one unit. Without a version bump, Phase A skips the skill (content-hash backstop catches drift, but noisier).
468
+
469
+ **Policy:** Bump `metadata.version` when changing any file under `skills/<name>/` — SKILL.md is the single version knob for the directory. Typo/whitespace fixes exempt. One bump per release cycle suffices.
470
+
471
+ Skills live in `skills/<name>/SKILL.md`. Read the relevant skill before starting a task it covers. The full list is discoverable via the agent's skill registry at session start.
472
+
473
+ ---
474
+
475
+ ## Code Style & Checklist
476
+
477
+ - **Validation:** Zod schemas, all fields need `.describe()`. See Adding a Tool for the JSON-Schema-serializable constraint and form-client safety.
478
+ - **Logging:** Framework auto-instruments all handler calls. `ctx.log` for domain-specific logging in handlers, global `logger` for lifecycle/background
479
+ - **Errors:** handlers throw — error factories (`notFound()`, `validationError()`, etc.) when the code matters, plain `Error` for don't-care cases. Framework catches and classifies.
480
+ - **Secrets:** server config only — no hardcoded credentials
481
+ - **Naming:** kebab-case files, snake_case tool/resource/prompt names, correct suffix
482
+ - **JSDoc:** `@fileoverview` + `@module` required on every file
483
+ - **No fabricated signal:** Don't invent synthetic scores or arbitrary "confidence percentages." Surface real signal.
484
+ - **Builders:** `tool()`/`resource()`/`prompt()` with correct fields (`handler`, `input`, `output`, `format`, `auth`, `args`)
485
+ - **`format()` completeness:** must carry the same data as `output` (parity is lint-enforced — see Adding a Tool)
486
+ - **Auth:** via `auth: ['scope']` on definitions (not HOF wrapper)
487
+ - **Presence checks:** `ctx.elicit`/`ctx.sample` checked before use
488
+ - **Task tools:** use `task: true` flag
489
+ - **Pagination:** large resource lists use `extractCursor`/`paginateArray`
490
+ - **Registration:** definitions exported in `definitions/index.ts` barrel
491
+ - **Tests:** `createMockContext()`, `.handler()` tested directly
492
+ - **Gate:** `bun run devcheck` passes (includes MCP definition linting)
493
+ - **Smoke-test:** `bun run rebuild && bun run start:stdio` (or `start:http`)
494
+
495
+ ---
496
+
497
+ ## Commands
498
+
499
+ | Command | Purpose |
500
+ |:--------|:--------|
501
+ | `bun run build` | Build library output (`scripts/build.ts`) |
502
+ | `bun run rebuild` | Clean and rebuild (`scripts/clean.ts` + `build`) |
503
+ | `bun run devcheck` | **Use often.** Lint, format, typecheck, MCP definition linting, `bun audit`, `bun outdated` |
504
+ | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
505
+ | `bun run lint:mcp` | Validate MCP definitions against spec |
506
+ | `bun run format` | Auto-fix Biome lint/format issues (safe fixes only) |
507
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
508
+ | `bun run test` | Unit/integration tests |
509
+ | `bun run start:stdio` | Production mode (stdio, after build) |
510
+ | `bun run start:http` | Production mode (HTTP, after build) |
511
+ | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
512
+ | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync with `changelog/` (used by devcheck) |
513
+
514
+ After `bun update --latest`, run the `maintenance` skill to investigate changelogs, adopt upstream changes, and sync project skills.
515
+
516
+ ---
517
+
518
+ ## Changelog
519
+
520
+ Directory-based. Source of truth: `changelog/<major.minor>.x/<version>.md` — one file per release (e.g. `changelog/0.5.x/0.5.4.md`), shipped in the npm package for direct agent access. `changelog/template.md` is the format reference (never edited).
521
+
522
+ `CHANGELOG.md` is a **navigation index** — clickable headers + one-line summaries from frontmatter. Regenerated by `bun run changelog:build`; `changelog:check` hard-fails on drift in devcheck. Never hand-edit — edit the per-version file and rerun the build.
523
+
524
+ ### Per-version file format
525
+
526
+ ```markdown
527
+ ---
528
+ summary: "One-line headline, ≤350 chars, no markdown" # required
529
+ breaking: false # optional, default false
530
+ security: false # optional, default false
531
+ ---
532
+
533
+ # 0.5.4 — 2026-04-20
534
+
535
+ ## Added
536
+
537
+ - ...
538
+ ```
539
+
540
+ **Frontmatter fields:**
541
+
542
+ | Field | Required | Purpose |
543
+ |:------|:---------|:--------|
544
+ | `summary` | yes | Rollup index line. ≤350 chars, no markdown, single line. Write like a GitHub Release title. |
545
+ | `breaking` | no (default `false`) | Flags releases with breaking changes. Renders as `· ⚠️ Breaking` badge in the rollup. Agents running the `maintenance` skill read this to prioritize review. |
546
+ | `security` | no (default `false`) | Flags releases with security fixes. Renders as `· 🛡️ Security` badge in the rollup so users can triage upgrade urgency. Pairs with the `## Security` body section. |
547
+ | `agent-notes` | no | Free-form adoption notes for downstream `maintenance` agents — new files to create, fields to populate, skills to re-run, one-time migration steps. Not rendered in `CHANGELOG.md`; consumed only by agents running the `maintenance` skill on consumer projects. Omit when there's nothing to say. |
548
+
549
+ Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 350 chars or malformed boolean fails `changelog:check`.
550
+
551
+ **Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security. Omit empty sections. Pre-release versions consolidate as sub-headers inside the final version's file — no separate files per pre-release.
552
+
553
+ ---
554
+
555
+ ## Publishing
556
+
557
+ If the user requests it, run the `release-and-publish` skill — it runs the verification gate (`devcheck`, `rebuild`, `test:all`), pushes commits and tags, and publishes to every applicable destination. After pushing, create a GitHub Release on the annotated tag (`gh release create v<VERSION> --verify-tag --notes-from-tag`) — no assets to attach, but the Release surfaces the tag's notes in the repo UI. **Skip the Docker build/push step** — this framework package is consumed via npm, not as a container image.
558
+
559
+ **Tag annotations render as GitHub Release bodies** via `--notes-from-tag`. They must be structured markdown — never a flat comma-separated string. Subject must omit the version number (GitHub prepends `v<VERSION>:`). Body uses Keep a Changelog sections with bullets. See `changelog/template.md` for the full format reference including an example.
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Package:** `@cyanheads/mcp-ts-core`
4
- **Version:** 0.9.13
4
+ **Version:** 0.9.15
5
5
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
6
6
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
7
7
  **Zod:** ^4.4.3
@@ -44,7 +44,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
44
44
 
45
45
  | Subpath | Key Exports | Purpose |
46
46
  |:--------|:------------|:--------|
47
- | `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `Context`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `z` | Main entry point |
47
+ | `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `Context`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `z` | Main entry point |
48
48
  | `/worker` | `createWorkerHandler`, `CloudflareBindings` | Cloudflare Workers entry |
49
49
  | `/tools` | `ToolDefinition`, `AnyToolDefinition`, `ToolAnnotations` | Tool definition types |
50
50
  | `/resources` | `ResourceDefinition`, `AnyResourceDefinition` | Resource definition types |
@@ -59,7 +59,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
59
59
  | `/utils` | formatting, encoding, network, pagination, logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |
60
60
  | `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
61
61
  | `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
62
- | `/testing` | `createMockContext` | Test helpers |
62
+ | `/testing` | `createMockContext`, `getEnrichment` | Test helpers |
63
63
  | `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
64
64
 
65
65
  All subpaths prefixed with `@cyanheads/mcp-ts-core`. **†Tier 3 modules** require optional peer dependencies — see `package.json` `peerDependencies`. Tier 3 methods that lazy-load deps are **async**.
@@ -230,6 +230,8 @@ export const myTool = tool('my_tool', {
230
230
  - **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
231
231
  - **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
232
232
 
233
+ **`enrichment`** (optional): The success-path counterpart to `errors[]` — a `ZodRawShape` of agent-facing context (empty-result notices, query/filter echo, pagination totals) that must reach both client surfaces. Populate via `ctx.enrich(...)` (or `ctx.enrich.notice()` / `.total()` / `.echo()`) in the handler or service layer. The framework merges it into `structuredContent`, advertises `output.extend(enrichment)` as `outputSchema`, and mirrors it into a `content[]` trailer — so it reaches `structuredContent`-only and `content[]`-only clients alike, with no `format()` entry. Keys must be disjoint from `output`; a required field never populated fails the effective-output parse. See `api-context`'s `ctx.enrich`.
234
+
233
235
  **Task tools:** Add `task: true` for long-running async operations. Framework manages lifecycle: creates task → returns ID immediately → runs handler in background with `ctx.progress` → stores result/error → `ctx.signal` for cancellation. See `add-tool` skill for full example.
234
236
 
235
237
  ---
@@ -278,6 +280,7 @@ interface Context {
278
280
  readonly signal: AbortSignal; // cancellation
279
281
  readonly progress?: ContextProgress; // present when task: true
280
282
  readonly uri?: URL; // present for resource handlers
283
+ readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
281
284
  recoveryFor(reason: string): { recovery: { hint: string } } | {}; // opt-in contract resolver
282
285
  }
283
286
  ```
@@ -500,7 +503,8 @@ Skills live in `skills/<name>/SKILL.md`. Read the relevant skill before starting
500
503
  | `bun run devcheck` | **Use often.** Lint, format, typecheck, MCP definition linting, `bun audit`, `bun outdated` |
501
504
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
502
505
  | `bun run lint:mcp` | Validate MCP definitions against spec |
503
- | `bun run format` | Auto-fix Biome lint/format issues |
506
+ | `bun run format` | Auto-fix Biome lint/format issues (safe fixes only) |
507
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
504
508
  | `bun run test` | Unit/integration tests |
505
509
  | `bun run start:stdio` | Production mode (stdio, after build) |
506
510
  | `bun run start:http` | Production mode (HTTP, after build) |