@cyanheads/mcp-ts-core 0.9.6 → 0.9.8

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 (49) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +12 -8
  3. package/biome.json +1 -1
  4. package/changelog/0.9.x/0.9.7.md +18 -0
  5. package/changelog/0.9.x/0.9.8.md +24 -0
  6. package/changelog/template.md +26 -0
  7. package/package.json +3 -4
  8. package/skills/add-app-tool/SKILL.md +6 -4
  9. package/skills/add-export/SKILL.md +10 -8
  10. package/skills/add-prompt/SKILL.md +15 -8
  11. package/skills/add-provider/SKILL.md +29 -12
  12. package/skills/add-resource/SKILL.md +20 -11
  13. package/skills/add-service/SKILL.md +15 -17
  14. package/skills/add-test/SKILL.md +50 -9
  15. package/skills/add-tool/SKILL.md +13 -6
  16. package/skills/api-auth/SKILL.md +3 -2
  17. package/skills/api-canvas/SKILL.md +43 -6
  18. package/skills/api-config/SKILL.md +6 -0
  19. package/skills/api-context/SKILL.md +9 -3
  20. package/skills/api-errors/SKILL.md +5 -5
  21. package/skills/api-linter/SKILL.md +32 -9
  22. package/skills/api-services/SKILL.md +1 -1
  23. package/skills/api-services/references/graph.md +1 -1
  24. package/skills/api-services/references/speech.md +1 -1
  25. package/skills/api-telemetry/SKILL.md +5 -5
  26. package/skills/api-testing/SKILL.md +9 -1
  27. package/skills/api-utils/SKILL.md +1 -1
  28. package/skills/api-workers/SKILL.md +12 -5
  29. package/skills/design-mcp-server/SKILL.md +20 -8
  30. package/skills/field-test/SKILL.md +9 -7
  31. package/skills/git-wrapup/SKILL.md +218 -0
  32. package/skills/maintenance/SKILL.md +8 -6
  33. package/skills/migrate-mcp-ts-template/SKILL.md +11 -7
  34. package/skills/multi-server-orchestration/SKILL.md +17 -5
  35. package/skills/multi-server-orchestration/references/greenfield-buildout.md +32 -2
  36. package/skills/multi-server-orchestration/references/maintenance-pass.md +11 -3
  37. package/skills/multi-server-orchestration/references/release-and-publish-pass.md +14 -25
  38. package/skills/multi-server-orchestration/references/wrapup-pass.md +15 -36
  39. package/skills/polish-docs-meta/SKILL.md +3 -1
  40. package/skills/polish-docs-meta/references/package-meta.md +1 -1
  41. package/skills/polish-docs-meta/references/readme.md +14 -1
  42. package/skills/release-and-publish/SKILL.md +20 -7
  43. package/skills/report-issue-framework/SKILL.md +5 -3
  44. package/skills/report-issue-local/SKILL.md +10 -5
  45. package/skills/setup/SKILL.md +13 -8
  46. package/skills/tool-defs-analysis/SKILL.md +6 -3
  47. package/templates/AGENTS.md +14 -6
  48. package/templates/CLAUDE.md +14 -6
  49. package/templates/changelog/template.md +26 -0
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.6
4
+ **Version:** 0.9.8
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
@@ -316,7 +316,7 @@ if (ctx.elicit) {
316
316
  const result = await ctx.elicit('What format?', z.object({
317
317
  format: z.enum(['json', 'csv']).describe('Output format'),
318
318
  }));
319
- if (result.action === 'accept') useFormat(result.data.format);
319
+ if (result.action === 'accept') useFormat(result.content.format);
320
320
  }
321
321
  ```
322
322
 
@@ -550,4 +550,4 @@ Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 3
550
550
 
551
551
  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.
552
552
 
553
- **Tag annotation subjects must omit the version number.** GitHub prepends `v<VERSION>:` to the release title when using `--notes-from-tag`. A subject like `0.9.5 some change` renders as `v0.9.5: 0.9.5 some change`. Write the subject as just the description: `mcpbignore recursive-match fix, zod to dependencies`.
553
+ **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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  [![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.9.6-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx)
10
+ [![Version](https://img.shields.io/badge/Version-0.9.8-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx)
11
11
 
12
12
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0%2B-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
13
 
@@ -23,6 +23,7 @@ The framework handles the plumbing: transports, auth, config, logging, telemetry
23
23
 
24
24
  ```ts
25
25
  import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
26
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
26
27
 
27
28
  const greet = tool('greet', {
28
29
  description: 'Greet someone by name and return a personalized message.',
@@ -38,7 +39,7 @@ const greet = tool('greet', {
38
39
  reason: 'name_blocked',
39
40
  code: JsonRpcErrorCode.Forbidden,
40
41
  when: 'The provided name is on the configured block list.',
41
- recovery: 'Use a different name.',
42
+ recovery: 'Use a different name that is not on the block list.',
42
43
  },
43
44
  ],
44
45
  handler: async (input, ctx) => {
@@ -66,7 +67,7 @@ Start your coding agent (i.e. Claude Code, Codex) and describe what you want. Th
66
67
 
67
68
  ### What you get
68
69
 
69
- Here's what tool definitions look like:
70
+ Here's what tool definitions look like. Add `format()` to render structured output as markdown — different MCP clients read different surfaces (`structuredContent` vs `content[]`), and `format()` ensures both carry the same data:
70
71
 
71
72
  ```ts
72
73
  import { tool, z } from '@cyanheads/mcp-ts-core';
@@ -151,13 +152,13 @@ It also works on Cloudflare Workers with `createWorkerHandler()` — same defini
151
152
  - **Unified Context** — one `ctx` for logging, tenant-scoped storage, elicitation, sampling, cancellation, and task progress.
152
153
  - **Auth** — `auth: ['scope']` on definitions, checked before dispatch (no wrapper code). Modes: `none`, `jwt`, or `oauth` (local secret or JWKS).
153
154
  - **Task tools** — `task: true` for long-running ops; framework manages create/poll/progress/complete/cancel.
154
- - **Definition linter** — validates names, schemas, auth scopes, annotations, format-parity, and cross-vendor JSON Schema portability at startup. Standalone via `lint:mcp` or devcheck.
155
+ - **Definition linter** — validates names, schemas, auth scopes, annotations, format-parity, and cross-vendor JSON Schema portability at build time. Run via `lint:mcp` or `devcheck` — not invoked at server startup.
155
156
  - **Typed error contracts** — declare `errors: [{ reason, code, when, recovery, retryable? }]` and handlers get a typed `ctx.fail(reason, …)`. Contracts publish in `tools/list` so clients preview failure modes; the linter cross-checks the handler. Factories (`notFound()`, `httpErrorFromResponse()`, …) cover ad-hoc throws; plain `Error` auto-classifies.
156
157
  - **Multi-backend storage** — `in-memory`, filesystem, Supabase, Cloudflare D1/KV/R2. Swap via env var; handlers don't change.
157
158
  - **DataCanvas (optional)** — Tier 3 SQL/analytical workspace backed by DuckDB. Register tabular data from upstream APIs, run SQL across registered tables, export CSV/Parquet/JSON. Token-sharing model (opaque `canvas_id`) for multi-agent collaboration; sliding TTL + per-tenant scoping. Opt-in via `CANVAS_PROVIDER_TYPE=duckdb`; fails closed on Workers.
158
159
  - **Observability** — Pino logging + optional OpenTelemetry traces/metrics. Request correlation and tool metrics automatic.
159
160
  - **Tiered dependencies** — parsers, OTEL SDK, Supabase, OpenAI as optional peers. Install what you use.
160
- - **Agent-first DX** — ships `CLAUDE.md` / `AGENTS.md` with the codebase documented throughout Agent Skills.
161
+ - **Agent-first DX** — ships `CLAUDE.md` / `AGENTS.md` and Agent Skills that give your coding agent full framework knowledge — it can scaffold tools, write tests, run security audits, and ship releases without you writing the boilerplate.
161
162
 
162
163
  ## Server structure
163
164
 
@@ -206,7 +207,7 @@ See [CLAUDE.md](CLAUDE.md) for the full configuration reference.
206
207
  | Function | Purpose |
207
208
  |:---------|:--------|
208
209
  | `createApp(options)` | Node.js server — handles full lifecycle |
209
- | `createWorkerHandler(options)` | Cloudflare Workers — returns `{ fetch, scheduled }` |
210
+ | `createWorkerHandler(options)` | Cloudflare Workers — returns an `ExportedHandler` |
210
211
 
211
212
  ### Builders
212
213
 
@@ -228,9 +229,12 @@ Handlers receive a unified `Context` object:
228
229
  | `ctx.state` | `ContextState` | Tenant-scoped key-value storage |
229
230
  | `ctx.elicit` | `Function?` | Ask the user for input (when client supports it) |
230
231
  | `ctx.sample` | `Function?` | Request LLM completion from the client |
232
+ | `ctx.fail` | `(reason, msg?, data?) => McpError` | Typed error throw — reason checked against `errors[]` contract at compile time |
231
233
  | `ctx.signal` | `AbortSignal` | Cancellation signal |
232
234
  | `ctx.notifyResourceUpdated` | `Function?` | Notify subscribed clients a resource changed |
233
235
  | `ctx.notifyResourceListChanged` | `Function?` | Notify clients the resource list changed |
236
+ | `ctx.notifyPromptListChanged` | `Function?` | Notify clients the prompt list changed |
237
+ | `ctx.notifyToolListChanged` | `Function?` | Notify clients the tool list changed |
234
238
  | `ctx.progress` | `ContextProgress?` | Task progress reporting (when `task: true`) |
235
239
  | `ctx.requestId` | `string` | Unique request ID |
236
240
  | `ctx.tenantId` | `string?` | Tenant ID (JWT `tid` claim, or `'default'` for stdio and HTTP+`MCP_AUTH_MODE=none`) |
@@ -296,9 +300,9 @@ Also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL_ST
296
300
 
297
301
  ## Documentation
298
302
 
299
- - **[CLAUDE.md/AGENTS.md](CLAUDE.md)** — Framework reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package.
303
+ - **[CLAUDE.md/AGENTS.md](CLAUDE.md)** — Framework reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package and is auto-accessible in your project after `init`.
300
304
  - **[docs/telemetry/](docs/telemetry/)** — OpenTelemetry: full catalog of spans, metrics, and attributes the framework emits ([observability.md](docs/telemetry/observability.md)), plus an example Grafana dashboard and vendor-agnostic query recipes for Datadog, New Relic, Honeycomb ([dashboards.md](docs/telemetry/dashboards.md)).
301
- - **[CHANGELOG.md](CHANGELOG.md)** — Version history - Directory based for easier parsing by agents. Each entry includes a summary, migration notes, and links to commits/issues.
305
+ - **[CHANGELOG.md](CHANGELOG.md)** — Version history. Each entry includes a summary, migration notes, and links to commits/issues.
302
306
 
303
307
  ## Development
304
308
 
package/biome.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "noImportCycles": "error",
46
46
  "noUnusedExpressions": "error",
47
47
  "noDeprecatedImports": "error",
48
- "noDuplicateDependencies": "warn"
48
+ "noDuplicateDependencies": "off"
49
49
  },
50
50
  "complexity": {
51
51
  "noUselessUndefined": "error"
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "biome noDuplicateDependencies off, structured tag annotation format, release artifact verification, skills updates"
3
+ breaking: false
4
+ ---
5
+
6
+ # 0.9.7 — 2026-05-23
7
+
8
+ ## Changed
9
+
10
+ - `biome.json`: `noDuplicateDependencies` warn → off (zod intentionally in both `dependencies` and `peerDependencies`)
11
+ - `CLAUDE.md`/`AGENTS.md`: tag annotation guidance rewritten — structured markdown format with `changelog/template.md` reference
12
+ - `changelog/template.md`: tag annotation format example added (subject, prose intro, sectioned bullets, dep arrows, test footer)
13
+ - `templates/CLAUDE.md`/`templates/AGENTS.md`: install badge config updated (split `command`/`args`, `env` for API keys), tag annotation guidance added
14
+ - `templates/changelog/template.md`: tag annotation format example synced from core
15
+ - `skills/multi-server-orchestration/references/wrapup-pass.md` v1.2: structured tag annotation format replaces flat-string guidance
16
+ - `skills/release-and-publish/SKILL.md`: Step 9 — verify published artifacts are reachable (npm, MCP Registry, GH Release, GHCR)
17
+ - `skills/multi-server-orchestration/references/greenfield-buildout.md`: recommended additional phases (design gate, test quality review, field test, pre-launch sweep) and 5 new gotchas
18
+ - `skills/polish-docs-meta/references/readme.md`: agent-friendly output subsection added to Features section guidance
@@ -0,0 +1,24 @@
1
+ ---
2
+ summary: "Docs fixes, README context table updates, skill sync, @hono/node-server ^2.0.3 → ^2.0.4"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.9.8 — 2026-05-24
8
+
9
+ ## Added
10
+
11
+ - **`git-wrapup` skill** — codifies the version-bump, changelog, commit, and tag workflow as a reusable agent skill.
12
+ - **`ctx.fail`** documented in README context table — typed error throw checked against the `errors[]` contract at compile time.
13
+ - **`ctx.notifyPromptListChanged`**, **`ctx.notifyToolListChanged`** documented in README context table.
14
+
15
+ ## Changed
16
+
17
+ - **README** — clarified `format()` purpose, corrected linter description (build-time, not startup), updated `createWorkerHandler` return type, expanded Agent-first DX description.
18
+ - **Skills** — bulk refresh across 37 skill files; version bumps and content updates from upstream.
19
+ - **`@hono/node-server`** ^2.0.3 → ^2.0.4.
20
+ - **`@cloudflare/workers-types`** ^4.20260523.1 → ^4.20260524.1.
21
+
22
+ ## Fixed
23
+
24
+ - **`ctx.elicit` example** in CLAUDE.md/AGENTS.md — `result.data.format` → `result.content.format` to match SDK's `ElicitResult.content` property.
@@ -66,6 +66,32 @@ security: false
66
66
  Never speculate on a future number — `#42` for an upcoming PR silently
67
67
  resolves to whatever real item already owns 42, and timeline previews pull
68
68
  in that unrelated item's metadata.
69
+
70
+ TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
71
+ via `gh release create --notes-from-tag`. The tag is a derivative of this
72
+ changelog entry — a condensed, scannable version, not a copy. Format:
73
+
74
+ <theme — omit version number, GitHub prepends it>
75
+ ← blank line
76
+ <1-2 sentence context: what this release does>
77
+ ← blank line
78
+ Dependency bumps: ← section header
79
+ ← blank line
80
+ - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
81
+ ← blank line
82
+ Changed: ← only sections with entries
83
+ ← blank line
84
+ - `format()` output includes `query` in text mode
85
+ ← blank line
86
+ Added:
87
+ ← blank line
88
+ - `manifest.json` scaffolded for MCPB bundle support
89
+ - Install badges (Claude Desktop, Cursor, VS Code)
90
+ ← blank line
91
+ <N> tests pass; `bun run devcheck` clean. ← footer
92
+
93
+ Never a flat comma-separated string. Always structured markdown with
94
+ sections. The tag must scan well as a rendered GitHub Release page.
69
95
  -->
70
96
 
71
97
  ## Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-core",
5
5
  "description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
6
6
  "main": "dist/core/index.js",
@@ -170,7 +170,7 @@
170
170
  "devDependencies": {
171
171
  "@biomejs/biome": "2.4.15",
172
172
  "@cloudflare/vitest-pool-workers": "^0.16.9",
173
- "@cloudflare/workers-types": "^4.20260523.1",
173
+ "@cloudflare/workers-types": "^4.20260524.1",
174
174
  "@duckdb/node-api": "^1.5.3-r.1",
175
175
  "@hono/otel": "^1.1.2",
176
176
  "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
@@ -228,7 +228,6 @@
228
228
  "cloudflare-workers",
229
229
  "declarative",
230
230
  "framework",
231
- "llm",
232
231
  "mcp",
233
232
  "mcp-server",
234
233
  "mcp-framework",
@@ -270,7 +269,7 @@
270
269
  },
271
270
  "dependencies": {
272
271
  "@hono/mcp": "^0.3.0",
273
- "@hono/node-server": "^2.0.3",
272
+ "@hono/node-server": "^2.0.4",
274
273
  "@modelcontextprotocol/ext-apps": "^1.7.2",
275
274
  "@modelcontextprotocol/sdk": "^1.29.0",
276
275
  "@opentelemetry/api": "^1.9.1",
@@ -30,11 +30,11 @@ MCP Apps extend the standard tool pattern with an interactive HTML UI rendered i
30
30
 
31
31
  Both builders are exported from `@cyanheads/mcp-ts-core`. They handle `_meta.ui.resourceUri`, the compat key (`ui/resourceUri`), and the correct MIME type (`text/html;profile=mcp-app`) automatically.
32
32
 
33
- For the full API, Context interface, and error codes, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`.
33
+ For the full API, Context interface, and error codes, read the framework's `CLAUDE.md` (loaded at session start).
34
34
 
35
35
  ## Steps
36
36
 
37
- 1. **Ask the user** for the tool's name, purpose, input/output shape, and what the UI should display
37
+ 1. **Confirm the three conditions** in "When to Use" apply — if any is uncertain, default to `add-tool` instead. Then **gather** the tool's name, purpose, input/output shape, and what the UI should display from the user's request — ask only if genuinely absent
38
38
  2. **Choose a URI** — convention: `ui://{{tool-name}}/app.html`
39
39
  3. **Create the app tool** at `src/mcp-server/tools/definitions/{{tool-name}}.app-tool.ts`
40
40
  4. **Create the app resource** at `src/mcp-server/resources/definitions/{{tool-name}}-ui.app-resource.ts`
@@ -233,10 +233,12 @@ If the repo already uses `definitions/index.ts` barrels, update those instead of
233
233
  - [ ] App resource created at `src/mcp-server/resources/definitions/{{tool-name}}-ui.app-resource.ts` using `appResource()`
234
234
  - [ ] `resourceUri` matches between tool and resource (`ui://{{tool-name}}/app.html`)
235
235
  - [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types
236
- - [ ] `format()` renders JSON first block (for UI) + human-readable, content-complete fallback blocks (for non-app hosts and LLMs)
237
- - [ ] App resource `_meta.ui.csp` covers any external iframe dependencies, or a custom `format()` adds equivalent per-read metadata
236
+ - [ ] `format()` first block is `JSON.stringify(result)` the full output object for the UI to parse via `app.ontoolresult`. Subsequent blocks are human-readable, content-complete fallback for non-app hosts and LLMs
237
+ - [ ] App resource `_meta.ui.csp.resourceDomains` lists every external domain loaded by the UI
238
238
  - [ ] UI bundles or inlines the client SDK for the shipped HTML, and handles `app.ontoolresult`
239
239
  - [ ] UI applies host context updates via `app.onhostcontextchanged`
240
+ - [ ] App resource has a `list` callback returning at least one URI so resource-aware clients can discover it
240
241
  - [ ] Both registered in the project's existing `createApp()` arrays (directly or via barrels)
242
+ - [ ] Handler tested directly via `createMockContext()`, or `add-test` skill run to scaffold the test file
241
243
  - [ ] `bun run devcheck` passes (linter validates `_meta.ui` and tool/resource pairing)
242
244
  - [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)
@@ -13,7 +13,7 @@ metadata:
13
13
 
14
14
  Subpath exports are defined in `package.json` under the `exports` field. Each subpath maps to a source entry point that gets compiled to `dist/`. The exports catalog in `CLAUDE.md` must stay in sync with `package.json`.
15
15
 
16
- The build uses `tsconfig.build.json` (not `tsconfig.json`) with `rootDir: ./src` and `include: ["src/**/*"]`. This means every source file at `src/foo/bar.ts` compiles to `dist/foo/bar.js` — the `dist/` paths in `exports` must mirror the source tree exactly, not flatten it.
16
+ The build uses `tsconfig.build.json` (not `tsconfig.json`) with `rootDir: ./src` and `include: ["src/**/*"]`. This means every source file at `src/foo/bar.ts` compiles to `dist/foo/bar.js` — the `dist/` path in each export entry must match wherever `tsc` produces the compiled output for the named source file. Choose your source file location to produce the `dist/` path you want in the export entry.
17
17
 
18
18
  ## Steps
19
19
 
@@ -28,16 +28,16 @@ The build uses `tsconfig.build.json` (not `tsconfig.json`) with `rootDir: ./src`
28
28
  }
29
29
  ```
30
30
 
31
- 3. **Update the exports catalog** in `CLAUDE.md` — add a row to the table
31
+ 3. **Update the exports catalog** in both `CLAUDE.md` and `AGENTS.md` — add a row to the table. These files must stay byte-identical; the simplest approach is `cp CLAUDE.md AGENTS.md` after editing
32
32
  4. **Build** with `bun run build` to generate `dist/` output
33
- 5. **Verify the export** by inspecting the compiled output directly:
33
+ 5. **Verify the export** resolves through the package's `exports` map:
34
34
 
35
35
  ```bash
36
36
  # Confirm the compiled file exists at the expected dist path
37
37
  ls dist/utils/new-util.js
38
38
 
39
- # Confirm the declared exports resolve to real files
40
- bun -e "import('./dist/utils/new-util.js').then(m => console.log(Object.keys(m)))"
39
+ # Confirm the subpath export resolves correctly (tests the exports map, not just the dist file)
40
+ bun -e "import('@cyanheads/mcp-ts-core/newutil').then(m => console.log(Object.keys(m)))"
41
41
  ```
42
42
 
43
43
  6. **Run `bun run devcheck`** to verify
@@ -46,7 +46,7 @@ The build uses `tsconfig.build.json` (not `tsconfig.json`) with `rootDir: ./src`
46
46
 
47
47
  | Convention | Rule |
48
48
  |:-----------|:-----|
49
- | Subpath | lowercase, no underscores (e.g., `utils/errorHandler`) |
49
+ | Subpath | all-lowercase, no underscores (e.g., `utils`, `storage/types`, `testing/fuzz`) |
50
50
  | Source file | kebab-case (e.g., `error-handler.ts`) |
51
51
  | Export name | camelCase for values, PascalCase for types |
52
52
 
@@ -54,7 +54,9 @@ The build uses `tsconfig.build.json` (not `tsconfig.json`) with `rootDir: ./src`
54
54
 
55
55
  - [ ] Source entry point file created with JSDoc header
56
56
  - [ ] Subpath added to `package.json` `exports` with `types` and `import` conditions
57
- - [ ] Exports catalog in `CLAUDE.md` updated with new row
57
+ - [ ] Exports catalog updated in both `CLAUDE.md` and `AGENTS.md` (must be byte-identical)
58
+ - [ ] If the new export has optional peer dependencies: entries added to both `peerDependencies` and `peerDependenciesMeta` in `package.json`
58
59
  - [ ] `bun run build` succeeds
59
- - [ ] Compiled file exists at expected `dist/` path and exports the expected symbols
60
+ - [ ] Compiled file exists at expected `dist/` path and subpath import resolves correctly
61
+ - [ ] Integration test at `tests/integration/package-consumer.int.test.ts` updated: new subpath added to the import spec list and `toHaveLength` count incremented
60
62
  - [ ] `bun run devcheck` passes
@@ -11,15 +11,13 @@ metadata:
11
11
 
12
12
  ## Context
13
13
 
14
- Prompts use the `prompt()` builder from `@cyanheads/mcp-ts-core`. Each prompt lives in `src/mcp-server/prompts/definitions/` with a `.prompt.ts` suffix and is registered into `createApp()` in `src/index.ts`. Some repos later add `definitions/index.ts` barrels; match the project's current pattern.
14
+ Prompts use the `prompt()` builder from `@cyanheads/mcp-ts-core`. Each prompt lives in `src/mcp-server/prompts/definitions/` with a `.prompt.ts` suffix. The standard registration pattern uses a `definitions/index.ts` barrel that collects all prompts into an `allPromptDefinitions` array for `createApp()`. Fresh scaffolds start with direct imports in `src/index.ts` the barrel is introduced as definitions grow. Match the pattern already used by the project you're editing.
15
15
 
16
- Prompts are pure message templates — no `Context`, no auth, no side effects.
17
-
18
- For the full `prompt()` API, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`.
16
+ Prompts are pure message templates — no `Context`, no auth, no side effects. `generate` can be sync or async (returns `PromptMessage[] | Promise<PromptMessage[]>`).
19
17
 
20
18
  ## Steps
21
19
 
22
- 1. **Ask the user** for the prompt's name, purpose, and arguments
20
+ 1. **Gather** the prompt's name, purpose, and arguments from the user's request — ask only if genuinely absent
23
21
  2. **Create the file** at `src/mcp-server/prompts/definitions/{{prompt-name}}.prompt.ts`
24
22
  3. **Register** the prompt in the project's existing `createApp()` prompt list (directly in `src/index.ts` for fresh scaffolds, or via a barrel if the repo already has one)
25
23
  4. **Run `bun run devcheck`** to verify
@@ -36,6 +34,8 @@ import { prompt, z } from '@cyanheads/mcp-ts-core';
36
34
 
37
35
  export const {{PROMPT_EXPORT}} = prompt('{{prompt_name}}', {
38
36
  description: '{{PROMPT_DESCRIPTION}}',
37
+ // args is optional — omit entirely for prompts with no parameters.
38
+ // When present, all fields need .describe(). Only JSON-Schema-serializable types allowed.
39
39
  args: z.object({
40
40
  // All fields need .describe()
41
41
  }),
@@ -86,14 +86,21 @@ await createApp({
86
86
  });
87
87
  ```
88
88
 
89
- If the repo already uses `src/mcp-server/prompts/definitions/index.ts`, update that barrel instead.
89
+ If the repo already uses `src/mcp-server/prompts/definitions/index.ts`, add the export to that barrel instead:
90
+
91
+ ```typescript
92
+ export { {{PROMPT_EXPORT}} } from './{{prompt-name}}.prompt.js';
93
+ ```
90
94
 
91
95
  ## Checklist
92
96
 
93
97
  - [ ] File created at `src/mcp-server/prompts/definitions/{{prompt-name}}.prompt.ts`
94
- - [ ] All Zod `args` fields have `.describe()` annotations
98
+ - [ ] Prompt name passed to `prompt()` uses snake_case
99
+ - [ ] `description` field set (lint warns if absent, but `devcheck` won't hard-fail — verify it's present)
100
+ - [ ] All Zod `args` fields have `.describe()` annotations — or `args` omitted entirely for no-parameter prompts
101
+ - [ ] `args` fields use only JSON-Schema-serializable Zod types (no `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.custom()`, etc.)
95
102
  - [ ] JSDoc `@fileoverview` and `@module` header present
96
- - [ ] `generate` function returns valid message array
103
+ - [ ] `generate` function present and returns at least one `{ role, content: { type: 'text', text } }` message
97
104
  - [ ] No side effects — prompts are pure templates
98
105
  - [ ] Registered in the project's existing `createApp()` prompt list (directly or via barrel)
99
106
  - [ ] `bun run devcheck` passes
@@ -15,6 +15,10 @@ Providers implement interfaces defined in core. They are selected at runtime via
15
15
  (e.g., `STORAGE_PROVIDER_TYPE`). Tier 3 providers lazy-load their dependencies to keep the
16
16
  core bundle small.
17
17
 
18
+ Providers live inside the package source tree — import the interface via relative path
19
+ (e.g., `import type { IStorageProvider } from '../core/IStorageProvider.js'`), not via the
20
+ package subpath exports (those are for consumers).
21
+
18
22
  ## Provider interfaces
19
23
 
20
24
  | Domain | Interface file |
@@ -32,9 +36,11 @@ these flags drive routing in `SpeechService`.
32
36
 
33
37
  Provider file location and naming differ by domain:
34
38
 
35
- - **Storage** — nested subdirectory, PascalCase file:
36
- `src/storage/providers/{{provider-name}}/{{provider-name}}Provider.ts`
37
- (e.g., `src/storage/providers/inMemory/inMemoryProvider.ts`)
39
+ - **Storage** — nested subdirectory, camelCase directory name, PascalCase-suffixed provider file.
40
+ Each provider gets its own subdirectory for the provider file plus any co-located types:
41
+ `src/storage/providers/{{providerName}}/{{providerName}}Provider.ts`
42
+ (e.g., `src/storage/providers/inMemory/inMemoryProvider.ts`,
43
+ `src/storage/providers/supabase/supabaseProvider.ts` + `supabase.types.ts`)
38
44
 
39
45
  - **LLM / Speech** — flat directory, kebab-case with `.provider.ts` suffix:
40
46
  `src/services/llm/providers/{{provider-name}}.provider.ts`
@@ -63,8 +69,11 @@ Provider file location and naming differ by domain:
63
69
 
64
70
  5. **Register the provider** — the registration point differs by domain:
65
71
 
66
- - **Storage** — add a `case` to the `switch` in `src/storage/core/storageFactory.ts`
67
- inside `createStorageProvider()`. Import the new provider class at the top of that file.
72
+ - **Storage** — two changes required:
73
+ 1. Add the new provider string to the `z.enum` for `STORAGE_PROVIDER_TYPE` in
74
+ `src/config/index.ts` — without this, the config schema rejects the env var at runtime.
75
+ 2. Add a `case` to the `switch` in `src/storage/core/storageFactory.ts`
76
+ inside `createStorageProvider()`. Import the new provider class at the top of that file.
68
77
 
69
78
  - **Speech** — two changes required:
70
79
  1. Add the new provider string literal to the `provider` union in
@@ -75,8 +84,10 @@ Provider file location and naming differ by domain:
75
84
 
76
85
  - **LLM** — currently only one provider exists (`OpenRouterProvider`); it is
77
86
  instantiated directly in `src/core/app.ts` rather than through a factory switch.
78
- Add a factory function or extend `app.ts` as needed, then update `ILlmProvider`
79
- consumers accordingly.
87
+ There is no factory pattern yet adding a second provider requires introducing
88
+ one (a selector env var, a factory function, and a conditional in `app.ts`).
89
+ Read `src/core/app.ts` to understand the current instantiation site before
90
+ designing the wiring.
80
91
 
81
92
  6. **Update the Worker-compatible provider list** if the new storage provider runs in
82
93
  Cloudflare Workers. The list is an inline array in `storageFactory.ts` at the
@@ -90,8 +101,11 @@ Provider file location and naming differ by domain:
90
101
  Add the new provider string to this array. Non-storage providers have no equivalent
91
102
  gate.
92
103
 
93
- 7. **Add the dependency** as an optional peer dependency in `package.json` if Tier 3.
94
- 8. **Run `bun run devcheck`** to verify.
104
+ 7. **Add the dependency** if Tier 3: add to both `peerDependencies` and
105
+ `peerDependenciesMeta` (with `{ "optional": true }`) in `package.json`.
106
+ Without the `peerDependenciesMeta` entry, the dep appears required rather than optional.
107
+ 8. **Run `bun run rebuild`** — since this is package source, verify the build output compiles.
108
+ 9. **Run `bun run devcheck`** to verify.
95
109
 
96
110
  ## Checklist
97
111
 
@@ -99,8 +113,11 @@ Provider file location and naming differ by domain:
99
113
  - [ ] Interface fully implemented (including `name`, `supportsTTS`/`supportsSTT` for speech)
100
114
  - [ ] Tier 3 dependencies lazy-loaded (not top-level imports)
101
115
  - [ ] Registered in the correct factory for the domain (see Step 5)
102
- - [ ] Speech: `provider` literal added to `SpeechProviderConfig` union in `types.ts`
116
+ - [ ] Storage: provider string added to `z.enum` in `src/config/index.ts`
103
117
  - [ ] Storage: Worker-compatible array in `storageFactory.ts` updated if applicable
104
- - [ ] Optional peer dependency added to `package.json` if Tier 3
118
+ - [ ] Speech: `provider` literal added to `SpeechProviderConfig` union in `types.ts`
119
+ - [ ] LLM: `src/core/app.ts` instantiation logic updated if adding a second LLM provider
120
+ - [ ] Optional peer dependency added to both `peerDependencies` and `peerDependenciesMeta` in `package.json` if Tier 3
121
+ - [ ] `bun run rebuild` succeeds
105
122
  - [ ] `bun run devcheck` passes
106
- - [ ] Integration tested with the target backend
123
+ - [ ] Test file created at `src/storage/providers/{{name}}/{{name}}Provider.test.ts` (or equivalent path for the domain) and `bun run test` passes
@@ -11,15 +11,13 @@ metadata:
11
11
 
12
12
  ## Context
13
13
 
14
- Resources use the `resource()` builder from `@cyanheads/mcp-ts-core`. Each resource lives in `src/mcp-server/resources/definitions/` with a `.resource.ts` suffix and is registered into `createApp()` in `src/index.ts`. Some repos later add `definitions/index.ts` barrels; follow the pattern already used by the project.
14
+ Resources use the `resource()` builder from `@cyanheads/mcp-ts-core`. Each resource lives in `src/mcp-server/resources/definitions/` with a `.resource.ts` suffix. The standard registration pattern uses a `definitions/index.ts` barrel that collects all resources into an `allResourceDefinitions` array for `createApp()`. Fresh scaffolds start with direct imports in `src/index.ts` the barrel is introduced as definitions grow. Match the pattern already used by the project you're editing.
15
15
 
16
16
  **Tool coverage.** Not all MCP clients expose resources — many are tool-only (Claude Code, Cursor, most chat UIs). Before adding a resource, verify the same data is reachable via the tool surface — either through a dedicated tool, included in another tool's output, or bundled into a broader tool. A resource whose data has no tool path is invisible to a large share of agents.
17
17
 
18
- For the full `resource()` API, pagination utilities, and `Context` interface, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`.
19
-
20
18
  ## Steps
21
19
 
22
- 1. **Ask the user** for the resource's URI template, purpose, and data shape
20
+ 1. **Gather** the resource's URI template, purpose, and data shape from the user's request — ask only if genuinely absent
23
21
  2. **Design the URI** — use `{paramName}` for path parameters (e.g., `myscheme://{itemId}/data`)
24
22
  3. **Create the file** at `src/mcp-server/resources/definitions/{{resource-name}}.resource.ts`
25
23
  4. **Register** the resource in the project's existing `createApp()` resource list (directly in `src/index.ts` for fresh scaffolds, or via a barrel if the repo already has one)
@@ -105,7 +103,11 @@ await createApp({
105
103
  });
106
104
  ```
107
105
 
108
- If the repo already uses `src/mcp-server/resources/definitions/index.ts`, update that barrel instead of changing the registration style.
106
+ If the repo already uses `src/mcp-server/resources/definitions/index.ts`, add the export to that barrel instead:
107
+
108
+ ```typescript
109
+ export { {{RESOURCE_EXPORT}} } from './{{resource-name}}.resource.js';
110
+ ```
109
111
 
110
112
  ### Optional: declarative `errors[]` contract
111
113
 
@@ -118,11 +120,14 @@ export const articleResource = resource('article://{pmid}', {
118
120
  description: 'Read an article by PMID.',
119
121
  errors: [
120
122
  { reason: 'no_pmid_match', code: JsonRpcErrorCode.NotFound,
121
- when: 'PMID not found in the index.' },
123
+ when: 'PMID not found in the index.',
124
+ recovery: 'Use pubmed_search_articles to discover valid PMIDs first.' },
122
125
  { reason: 'withdrawn', code: JsonRpcErrorCode.NotFound,
123
- when: 'Article was withdrawn upstream.' },
126
+ when: 'Article was withdrawn upstream.',
127
+ recovery: 'Check PubMed directly for retraction or withdrawal notices.' },
124
128
  { reason: 'upstream_throttled', code: JsonRpcErrorCode.RateLimited,
125
- when: 'Upstream PubMed quota hit.', retryable: true },
129
+ when: 'Upstream PubMed quota hit.', retryable: true,
130
+ recovery: 'Wait a few seconds and retry the request.' },
126
131
  ],
127
132
  params: z.object({ pmid: z.string().describe('PubMed ID') }),
128
133
  async handler(params, ctx) {
@@ -142,21 +147,25 @@ Beyond `description`, `params`, `handler`, and `list`, the builder also supports
142
147
 
143
148
  | Field | Purpose |
144
149
  |:------|:--------|
150
+ | `name` | Short human-readable name for `resources/list`. Defaults to a slug derived from the URI template if omitted. |
145
151
  | `output` | Optional Zod schema for runtime validation of the handler return value (parity with `tool()`'s `output`). |
146
152
  | `format` | Optional formatter mapping the handler's return to the `ReadResourceResult.contents[]` shape. Default: string passthrough; objects serialized to JSON. Override when you need to attach permissions, custom encodings, or split into multiple content items. |
147
153
  | `annotations` | Resource annotations (e.g., `audience`, `priority`) — see `ResourceAnnotations`. |
148
154
  | `title` | Human-readable display title (defaults to `name`). |
155
+ | `examples` | Array of `{ name, uri }` example entries surfaced in `resources/list` for discoverability. |
149
156
 
150
157
  ## Checklist
151
158
 
152
159
  - [ ] File created at `src/mcp-server/resources/definitions/{{resource-name}}.resource.ts`
153
- - [ ] URI template uses `{paramName}` syntax for path parameters
160
+ - [ ] Resource name passed to `resource()` uses a valid URI template with `{paramName}` syntax
154
161
  - [ ] All Zod `params` fields have `.describe()` annotations
162
+ - [ ] `output` schema added if the handler returns structured data that benefits from runtime validation
155
163
  - [ ] JSDoc `@fileoverview` and `@module` header present
156
164
  - [ ] `handler(params, ctx)` is pure — throws on failure, no try/catch
157
- - [ ] Data is reachable via the tool surface (dedicated tool, another tool's output, or not needed for tool-only agents)
165
+ - [ ] If `errors[]` contract declared: every entry has a `recovery` field (≥5 words, lint-enforced)
166
+ - [ ] Data is reachable via the tool surface — confirm by checking `src/mcp-server/tools/definitions/` for a tool that exposes this data, or document why this resource is resources-only
158
167
  - [ ] `list()` function provided if the resource is discoverable
159
- - [ ] Pagination used for large result sets (`extractCursor`/`paginateArray`)
168
+ - [ ] Pagination used for large result sets (`extractCursor`/`paginateArray`) — applies to both `handler` data and `list()` catalogs with many entries
160
169
  - [ ] Registered in the project's existing `createApp()` resource list (directly or via barrel)
161
170
  - [ ] `bun run devcheck` passes
162
171
  - [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)
@@ -15,11 +15,11 @@ Services use the init/accessor pattern: initialized once in `createApp`'s `setup
15
15
 
16
16
  Service methods receive `Context` for correlated logging (`ctx.log`) and tenant-scoped storage (`ctx.state`). Convention: `ctx.elicit` and `ctx.sample` should only be called from tool handlers, not from services.
17
17
 
18
- For the full service pattern, `CoreServices`, and `Context` interface, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`.
18
+ For the full service pattern, `CoreServices`, and `Context` interface, read the framework's `CLAUDE.md` (loaded at session start).
19
19
 
20
20
  ## Steps
21
21
 
22
- 1. **Ask the user** for the service domain name and what it integrates with
22
+ 1. **Gather** the service domain name and what it integrates with from the user's request — ask only if genuinely absent
23
23
  2. **Create the directory** at `src/services/{{domain}}/`
24
24
  3. **Create the service file** at `src/services/{{domain}}/{{domain}}-service.ts`
25
25
  4. **Create types** at `src/services/{{domain}}/types.ts` if needed
@@ -71,19 +71,16 @@ export function get{{ServiceName}}(): {{ServiceName}} {
71
71
 
72
72
  ### Entry point registration
73
73
 
74
+ Add the `setup()` callback and import to the existing `createApp()` call — preserve the existing tool/resource/prompt arrays:
75
+
74
76
  ```typescript
75
- // src/index.ts
76
- import { createApp } from '@cyanheads/mcp-ts-core';
77
+ // In src/index.ts (or src/worker.ts for Worker-only servers)
77
78
  import { init{{ServiceName}} } from './services/{{domain}}/{{domain}}-service.js';
78
79
 
79
- await createApp({
80
- tools: [/* existing tools */],
81
- resources: [/* existing resources */],
82
- prompts: [/* existing prompts */],
83
- setup(core) {
84
- init{{ServiceName}}(core.config, core.storage);
85
- },
86
- });
80
+ // Add setup() alongside existing options:
81
+ setup(core) {
82
+ init{{ServiceName}}(core.config, core.storage);
83
+ },
87
84
  ```
88
85
 
89
86
  ### Usage in tool handlers
@@ -133,13 +130,13 @@ async fetchItem(id: string, ctx: Context): Promise<Item> {
133
130
  ### Key principles
134
131
 
135
132
  1. **Calibrate backoff to the upstream.** 200–500ms for ephemeral failures, 1–2s for rate-limited APIs, 2–5s for service degradation. The default `baseDelayMs: 1000` suits most APIs.
136
- 2. **Check HTTP status before parsing.** `fetchWithTimeout` already throws `ServiceUnavailable` on non-OK responses — this prevents feeding HTML error pages into XML/JSON parsers.
133
+ 2. **Check HTTP status before parsing.** `fetchWithTimeout` already throws on non-OK responses with granular status mapping (401→`Unauthorized`, 403→`Forbidden`, 404→`NotFound`, 408/425→`Timeout`, 422→`ValidationError`, 429→`RateLimited`, 5xx→`ServiceUnavailable`/`InternalError`) — this prevents feeding HTML error pages into XML/JSON parsers.
137
134
  3. **Classify parse failures by content.** If the upstream returns HTTP 200 with an HTML error page, detect it and throw `ServiceUnavailable` (transient) instead of `SerializationError` (non-transient).
138
135
  4. **Exhausted retries say so.** `withRetry` automatically enriches the final error with attempt count — callers know retries were already attempted.
139
136
 
140
137
  ### When you need finer-grained HTTP error classification
141
138
 
142
- `fetchWithTimeout` collapses every non-2xx into `ServiceUnavailable`. That's the safe default but it isn't always right — a `401` should be `Unauthorized`, a `429` should be `RateLimited` (and is retryable), a `408` should be `Timeout` (and is retryable). When you need the nuance, drop down to raw `fetch` + `httpErrorFromResponse`:
139
+ `fetchWithTimeout` already maps status codes to appropriate error codes (see key principle 2 above). Use `httpErrorFromResponse` instead when you need `Retry-After` header capture, request body passthrough in error data, or custom `service`/`data` fields on the thrown error:
143
140
 
144
141
  ```typescript
145
142
  import { httpErrorFromResponse, withRetry } from '@cyanheads/mcp-ts-core/utils';
@@ -289,11 +286,12 @@ Silent truncation is a data integrity bug — the caller thinks it has all resul
289
286
  ## Checklist
290
287
 
291
288
  - [ ] Directory created at `src/services/{{domain}}/`
292
- - [ ] Service file created with init/accessor pattern
289
+ - [ ] Service file created `init` function accepts `(config: AppConfig, storage: StorageService)` and stores the instance
290
+ - [ ] Accessor function exported — throws `Error` if not initialized
293
291
  - [ ] JSDoc `@fileoverview` and `@module` header present
292
+ - [ ] No `console` calls — use `ctx.log` for service-level logging
294
293
  - [ ] Service methods accept `Context` for logging and storage
295
- - [ ] `init` function registered in `setup()` callback in `src/index.ts`
296
- - [ ] Accessor throws `Error` if not initialized
294
+ - [ ] `init` function registered in `setup()` callback in the server's entry point (`src/index.ts` or `src/worker.ts`)
297
295
  - [ ] If wrapping external API: retry covers full pipeline (fetch + parse), backoff calibrated
298
296
  - [ ] If wrapping external API: raw/domain types reflect real upstream sparsity; missing values are preserved as unknown, not fabricated into concrete facts
299
297
  - [ ] If wrapping external API: batch endpoints used where available, field selection applied, pagination handled