@cyanheads/mcp-ts-core 0.9.7 → 0.9.9

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 (53) hide show
  1. package/CLAUDE.md +3 -2
  2. package/README.md +12 -8
  3. package/changelog/0.9.x/0.9.8.md +24 -0
  4. package/changelog/0.9.x/0.9.9.md +20 -0
  5. package/dist/testing/fuzz.d.ts +6 -1
  6. package/dist/testing/fuzz.d.ts.map +1 -1
  7. package/dist/testing/fuzz.js +93 -49
  8. package/dist/testing/fuzz.js.map +1 -1
  9. package/package.json +7 -6
  10. package/scripts/check-framework-antipatterns.ts +8 -4
  11. package/skills/add-app-tool/SKILL.md +6 -4
  12. package/skills/add-export/SKILL.md +10 -8
  13. package/skills/add-prompt/SKILL.md +15 -8
  14. package/skills/add-provider/SKILL.md +29 -12
  15. package/skills/add-resource/SKILL.md +20 -11
  16. package/skills/add-service/SKILL.md +15 -17
  17. package/skills/add-test/SKILL.md +50 -9
  18. package/skills/add-tool/SKILL.md +13 -6
  19. package/skills/api-auth/SKILL.md +3 -2
  20. package/skills/api-canvas/SKILL.md +43 -6
  21. package/skills/api-config/SKILL.md +6 -0
  22. package/skills/api-context/SKILL.md +9 -3
  23. package/skills/api-errors/SKILL.md +5 -5
  24. package/skills/api-linter/SKILL.md +32 -9
  25. package/skills/api-services/SKILL.md +1 -1
  26. package/skills/api-services/references/graph.md +1 -1
  27. package/skills/api-services/references/speech.md +1 -1
  28. package/skills/api-telemetry/SKILL.md +5 -5
  29. package/skills/api-testing/SKILL.md +9 -1
  30. package/skills/api-utils/SKILL.md +1 -1
  31. package/skills/api-workers/SKILL.md +12 -5
  32. package/skills/design-mcp-server/SKILL.md +20 -8
  33. package/skills/field-test/SKILL.md +9 -7
  34. package/skills/git-wrapup/SKILL.md +218 -0
  35. package/skills/maintenance/SKILL.md +8 -6
  36. package/skills/migrate-mcp-ts-template/SKILL.md +11 -7
  37. package/skills/multi-server-orchestration/SKILL.md +17 -5
  38. package/skills/multi-server-orchestration/references/greenfield-buildout.md +6 -3
  39. package/skills/multi-server-orchestration/references/maintenance-pass.md +11 -3
  40. package/skills/multi-server-orchestration/references/release-and-publish-pass.md +14 -25
  41. package/skills/multi-server-orchestration/references/wrapup-pass.md +13 -41
  42. package/skills/polish-docs-meta/SKILL.md +3 -1
  43. package/skills/polish-docs-meta/references/package-meta.md +1 -1
  44. package/skills/release-and-publish/SKILL.md +10 -9
  45. package/skills/report-issue-framework/SKILL.md +5 -3
  46. package/skills/report-issue-local/SKILL.md +10 -5
  47. package/skills/setup/SKILL.md +13 -8
  48. package/skills/tool-defs-analysis/SKILL.md +6 -3
  49. package/templates/CLAUDE.md +1 -0
  50. package/dist/logs/combined.log +0 -7
  51. package/dist/logs/error.log +0 -5
  52. package/dist/logs/interactions.log +0 -0
  53. package/scripts/split-changelog.ts +0 -133
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
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",
@@ -18,7 +18,6 @@
18
18
  "scripts/lint-mcp.ts",
19
19
  "scripts/lint-packaging.ts",
20
20
  "scripts/list-skills.ts",
21
- "scripts/split-changelog.ts",
22
21
  "scripts/tree.ts",
23
22
  "skills/",
24
23
  "templates/",
@@ -170,7 +169,7 @@
170
169
  "devDependencies": {
171
170
  "@biomejs/biome": "2.4.15",
172
171
  "@cloudflare/vitest-pool-workers": "^0.16.9",
173
- "@cloudflare/workers-types": "^4.20260523.1",
172
+ "@cloudflare/workers-types": "^4.20260524.1",
174
173
  "@duckdb/node-api": "^1.5.3-r.1",
175
174
  "@hono/otel": "^1.1.2",
176
175
  "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
@@ -198,7 +197,6 @@
198
197
  "depcheck": "^1.4.7",
199
198
  "diff": "^9.0.0",
200
199
  "execa": "^9.6.1",
201
- "fast-check": "^4.8.0",
202
200
  "fast-xml-parser": "^5.8.0",
203
201
  "ignore": "^7.0.5",
204
202
  "js-yaml": "^4.1.1",
@@ -228,7 +226,6 @@
228
226
  "cloudflare-workers",
229
227
  "declarative",
230
228
  "framework",
231
- "llm",
232
229
  "mcp",
233
230
  "mcp-server",
234
231
  "mcp-framework",
@@ -270,7 +267,7 @@
270
267
  },
271
268
  "dependencies": {
272
269
  "@hono/mcp": "^0.3.0",
273
- "@hono/node-server": "^2.0.3",
270
+ "@hono/node-server": "^2.0.4",
274
271
  "@modelcontextprotocol/ext-apps": "^1.7.2",
275
272
  "@modelcontextprotocol/sdk": "^1.29.0",
276
273
  "@opentelemetry/api": "^1.9.1",
@@ -296,6 +293,7 @@
296
293
  "chrono-node": "^2.9.0",
297
294
  "defuddle": "^0.18.1",
298
295
  "diff": "latest",
296
+ "fast-check": "^4.8.0",
299
297
  "fast-xml-parser": "^5.8.0",
300
298
  "js-yaml": "^4.1.1",
301
299
  "linkedom": "^0.18.12",
@@ -355,6 +353,9 @@
355
353
  "diff": {
356
354
  "optional": true
357
355
  },
356
+ "fast-check": {
357
+ "optional": true
358
+ },
358
359
  "fast-xml-parser": {
359
360
  "optional": true
360
361
  },
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @fileoverview Guards framework source against three SDK-coupling antipatterns
4
- * tempting to reach for when fixing tool input-validation error shape (#66) or
5
- * similar SDK-adjacent bugs. Each rule is narrow and mechanical — the goal is to
6
- * pin an architectural decision so it does not have to be re-litigated per PR.
3
+ * @fileoverview Guards against three SDK-coupling antipatterns. Scans `src/`
4
+ * via `git grep` all rules target framework-internal paths. Shipped to
5
+ * consumers via `package.json` `files:` because `devcheck` invokes it; in
6
+ * consumer projects the scanned paths (`src/mcp-server/tools/`,
7
+ * `src/mcp-server/transports/`) either don't exist or contain consumer code
8
+ * that follows different conventions, so the script exits cleanly with 0
9
+ * findings. Defense-in-depth: harmless when nothing matches, catches real
10
+ * regressions in the framework.
7
11
  *
8
12
  * Rules:
9
13
  * 1. Framework must not downgrade the Zod `inputSchema` passed to
@@ -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
@@ -21,10 +21,10 @@ For the full `createMockContext` API and testing patterns, read:
21
21
 
22
22
  1. **Identify the target** — which tool, resource, or service needs tests
23
23
  2. **Read the source file** — understand the handler's logic, input/output schemas, error paths, and which `ctx` features it uses
24
- 3. **Create the test file** in the repo's existing test layout
24
+ 3. **Create the test file** in the repo's existing test layout — search for existing `*.test.ts` files to confirm whether tests are colocated with source or under a root `tests/` directory
25
25
  4. **Write test cases** covering happy path, error paths, and edge cases
26
26
  5. **Run `bun run test`** to verify
27
- 6. **Run `bun run devcheck`** to verify types
27
+ 6. **Run `bun run devcheck`** to verify lint, types, and MCP definitions
28
28
 
29
29
  ## Determining What to Test
30
30
 
@@ -41,6 +41,7 @@ Read the handler and identify:
41
41
  | **`ctx.fail` (typed contract)** | Definitions with `errors[]` need `fail` attached to the mock ctx — `createMockContext({ errors: myTool.errors })` does it for you. Assert on `data.reason` (stable per-contract entry), not just `code`. |
42
42
  | **`format` function** | Test separately if defined — it's pure, no ctx needed. Verify it renders the IDs and fields the model needs, not just a count or title. For projection-style tools, test non-default field selections. |
43
43
  | **Sparse upstream payloads** | For third-party API integrations, build a fixture with omitted fields. Assert normalized output still validates and `format()` preserves unknown values instead of inventing facts. |
44
+ | **Form-client payloads** | If handler has optional fields: test with empty-string inner values (form clients send `""` instead of `undefined`). Assert handler doesn't break or produce invalid output. |
44
45
  | **Auth scopes** | Not tested at handler level (framework enforces) — skip |
45
46
 
46
47
  ## Templates
@@ -134,6 +135,8 @@ describe('{{RESOURCE_EXPORT}}', () => {
134
135
  // expect(err.code).toBe(JsonRpcErrorCode.NotFound);
135
136
  // expect(err.data.reason).toBe('no_match');
136
137
 
138
+ // Include this block only when the resource definition exports a `list` function.
139
+ // Check the source — `list` is optional on resource definitions.
137
140
  it('lists available resources', async () => {
138
141
  const listing = await {{RESOURCE_EXPORT}}.list!();
139
142
  expect(listing.resources).toBeInstanceOf(Array);
@@ -156,14 +159,16 @@ describe('{{RESOURCE_EXPORT}}', () => {
156
159
 
157
160
  import { beforeEach, describe, expect, it } from 'vitest';
158
161
  import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
162
+ import { StorageService } from '@cyanheads/mcp-ts-core/storage';
159
163
  import { get{{ServiceClass}}, init{{ServiceClass}} } from '@/services/{{domain}}/{{domain}}-service.js';
160
164
 
161
- import { createInMemoryStorage } from '@cyanheads/mcp-ts-core/testing';
165
+ // Derive the minimal mock config from src/config/server-config.ts — read
166
+ // the server's Zod schema to see which fields init{{ServiceClass}}() needs.
167
+ const mockConfig = { /* fields from server config schema */ } as AppConfig;
162
168
 
163
169
  describe('{{ServiceClass}}', () => {
164
- beforeEach(() => {
165
- // Re-initialize with fresh config/storage for each test
166
- const mockStorage = createInMemoryStorage();
170
+ beforeEach(async () => {
171
+ const mockStorage = await StorageService.create({ type: 'in-memory' });
167
172
  init{{ServiceClass}}(mockConfig, mockStorage);
168
173
  });
169
174
 
@@ -206,8 +211,42 @@ it('respects cancellation', async () => {
206
211
  setTimeout(() => controller.abort(), 50);
207
212
  const result = await {{TOOL_EXPORT}}.handler(input, ctx);
208
213
 
209
- // Should have stopped early
210
- expect(result.finalCount).toBeGreaterThan(0);
214
+ // Should have returned a partial result rather than throwing on cancellation.
215
+ // Assert on a field from the tool's actual output schema.
216
+ expect(result).toBeDefined();
217
+ });
218
+ ```
219
+
220
+ ### Prompt test
221
+
222
+ ```typescript
223
+ /**
224
+ * @fileoverview Tests for {{PROMPT_NAME}} prompt.
225
+ * @module tests/prompts/{{PROMPT_NAME}}.prompt.test
226
+ */
227
+
228
+ import { describe, expect, it } from 'vitest';
229
+ import { {{PROMPT_EXPORT}} } from '@/mcp-server/prompts/definitions/{{prompt-name}}.prompt.js';
230
+
231
+ describe('{{PROMPT_EXPORT}}', () => {
232
+ it('generates valid messages for valid args', () => {
233
+ const args = {{PROMPT_EXPORT}}.args!.parse({
234
+ // valid args matching the Zod schema
235
+ });
236
+ const messages = {{PROMPT_EXPORT}}.generate(args);
237
+ expect(messages).toBeInstanceOf(Array);
238
+ expect(messages.length).toBeGreaterThan(0);
239
+ for (const msg of messages) {
240
+ expect(msg).toHaveProperty('role');
241
+ expect(msg).toHaveProperty('content');
242
+ }
243
+ });
244
+
245
+ // Include only when the prompt has no required args (args is optional or all fields optional).
246
+ it('generates messages with no args', () => {
247
+ const messages = {{PROMPT_EXPORT}}.generate({});
248
+ expect(messages.length).toBeGreaterThan(0);
249
+ });
211
250
  });
212
251
  ```
213
252
 
@@ -249,6 +288,8 @@ When scaffolding tests for an existing handler, use the Zod schemas to generate
249
288
  - [ ] `format` function tested if defined
250
289
  - [ ] `createMockContext` options match handler's ctx usage (`tenantId`, `progress`, `elicit`, `sample`)
251
290
  - [ ] Service re-initialized in `beforeEach` if handler depends on a service singleton
252
- - [ ] If wrapping external API: sparse-payload case tested (omitted upstream fields still validate; `format()` does not invent facts)
291
+ - [ ] If handler has optional fields: tested with empty-string inner values (form-client simulation)
292
+ - [ ] If wrapping external API: sparse-payload case tested — fixture omits at least one optional upstream field; output still validates and `format()` renders uncertainty honestly instead of inventing values
293
+ - [ ] If target is a prompt: `generate()` tested with valid args and (when applicable) no args
253
294
  - [ ] `bun run test` passes
254
295
  - [ ] `bun run devcheck` passes
@@ -11,18 +11,16 @@ metadata:
11
11
 
12
12
  ## Context
13
13
 
14
- Tools use the `tool()` builder from `@cyanheads/mcp-ts-core`. Each tool lives in `src/mcp-server/tools/definitions/` with a `.tool.ts` suffix and is registered into `createApp()` in `src/index.ts`. Some larger repos later add `definitions/index.ts` barrels; match the pattern already used by the project you're editing.
15
-
16
- For the full `tool()` API, `Context` interface, and error codes, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`.
14
+ Tools use the `tool()` builder from `@cyanheads/mcp-ts-core`. Each tool lives in `src/mcp-server/tools/definitions/` with a `.tool.ts` suffix. The standard registration pattern uses a `definitions/index.ts` barrel that collects all tools into an `allToolDefinitions` array for `createApp()`. Fresh scaffolds from `init` 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.
17
15
 
18
16
  ## Steps
19
17
 
20
- 1. **Ask the user** for the tool's name, purpose, and input/output shape
18
+ 1. **Gather** the tool's name, purpose, and input/output shape from the user's request — ask only if genuinely absent
21
19
  2. **Determine if long-running** — if the tool involves streaming, polling, or
22
20
  multi-step async work, it should use `task: true`
23
21
  3. **Create the file** at `src/mcp-server/tools/definitions/{{tool-name}}.tool.ts`
24
22
  4. **Register** the tool in the project's existing `createApp()` tool list (directly in `src/index.ts` for fresh scaffolds, or via a barrel if the repo already has one)
25
- 5. **Run `bun run devcheck`** to verify
23
+ 5. **Run `bun run devcheck`** to verify — if Biome reports formatting issues, run `bun run format` to auto-fix, then re-run devcheck
26
24
  6. **Smoke-test** with `bun run rebuild && bun run start:stdio` (or `start:http`)
27
25
 
28
26
  ## Naming
@@ -136,6 +134,7 @@ export const {{TOOL_EXPORT}} = tool('{{tool_name}}', {
136
134
  output: z.object({ /* ... */ }),
137
135
 
138
136
  async handler(input, ctx) {
137
+ // ctx.progress is guaranteed non-null when task: true — the ! assertion is safe here.
139
138
  await ctx.progress!.setTotal(totalSteps);
140
139
  for (const step of steps) {
141
140
  if (ctx.signal.aborted) break;
@@ -528,18 +527,26 @@ Large payloads burn the agent's context window. Default to curated summaries; of
528
527
  ## Checklist
529
528
 
530
529
  - [ ] File created at `src/mcp-server/tools/definitions/{{tool-name}}.tool.ts`
530
+ - [ ] Tool name passed to `tool()` uses snake_case
531
+ - [ ] `title` field set
532
+ - [ ] `annotations` set correctly — `readOnlyHint: false` for write tools, `destructiveHint: true` for delete/overwrite tools
531
533
  - [ ] All Zod schema fields have `.describe()` annotations
532
534
  - [ ] Numeric `output` fields carry units in the field name (`sizeInBytes`, `durationInMs`, `priceInCents`, `latencyInMs`) — `.describe()` may be summarized away or truncated, but the field name persists into the JSON the agent reads. Exempt: dimensionless counts (`totalCount`, `itemCount`), indices (`index`, `position`)
533
535
  - [ ] Schemas use only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`)
534
536
  - [ ] JSDoc `@fileoverview` and `@module` header present
535
537
  - [ ] Optional nested objects guarded for empty inner values from form-based clients (check `?.field` truthiness, not just object presence)
536
- - [ ] `handler(input, ctx)` is pure throws on failure, no try/catch
538
+ - [ ] No `console` callsuse `ctx.log` for handler logging
539
+ - [ ] `handler(input, ctx)` is pure — throws on failure, no try/catch (exception: batch tools with per-item isolation use try/catch inside the loop — that's intentional, don't remove it)
537
540
  - [ ] `format()` renders every field in the output schema — enforced at lint time via sentinel injection, startup fails with `format-parity` errors otherwise. Different clients forward different surfaces (Claude Code → `structuredContent`, Claude Desktop → `content[]`); both must carry the same data. Primary fix: render the missing field in `format()` (use `z.discriminatedUnion` for list/detail variants). Escape hatch: if the output schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) rather than maintaining aspirational typing
538
541
  - [ ] If wrapping external API: output schema and `format()` preserve uncertainty from sparse upstream payloads instead of inventing concrete values
539
542
  - [ ] `auth` scopes declared if the tool needs authorization
540
543
  - [ ] `errors: [...]` contract declared for the tool's domain-specific failure modes — or block deleted if no domain failures apply (baseline codes bubble freely)
541
544
  - [ ] Error contract declared inline on this tool — not imported from a shared module, even when other tools have near-identical entries
542
545
  - [ ] `task: true` added if the tool is long-running
546
+ - [ ] If `task: true`: handler checks `ctx.signal.aborted` in its loop for cancellation support
547
+ - [ ] If tool returns unbounded arrays: pagination with total count, or `spillover()` / DataCanvas for tabular working sets
548
+ - [ ] If tool is feature-gated: evaluated whether `disabledTool()` wrapper is appropriate (present in manifest but uncallable)
543
549
  - [ ] Registered in the project's existing `createApp()` tool list (directly or via barrel)
550
+ - [ ] Test file created via `add-test` skill, or handler tested directly with `createMockContext()`
544
551
  - [ ] `bun run devcheck` passes
545
552
  - [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)