@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.
- package/CLAUDE.md +3 -2
- package/README.md +12 -8
- package/changelog/0.9.x/0.9.8.md +24 -0
- package/changelog/0.9.x/0.9.9.md +20 -0
- package/dist/testing/fuzz.d.ts +6 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +93 -49
- package/dist/testing/fuzz.js.map +1 -1
- package/package.json +7 -6
- package/scripts/check-framework-antipatterns.ts +8 -4
- package/skills/add-app-tool/SKILL.md +6 -4
- package/skills/add-export/SKILL.md +10 -8
- package/skills/add-prompt/SKILL.md +15 -8
- package/skills/add-provider/SKILL.md +29 -12
- package/skills/add-resource/SKILL.md +20 -11
- package/skills/add-service/SKILL.md +15 -17
- package/skills/add-test/SKILL.md +50 -9
- package/skills/add-tool/SKILL.md +13 -6
- package/skills/api-auth/SKILL.md +3 -2
- package/skills/api-canvas/SKILL.md +43 -6
- package/skills/api-config/SKILL.md +6 -0
- package/skills/api-context/SKILL.md +9 -3
- package/skills/api-errors/SKILL.md +5 -5
- package/skills/api-linter/SKILL.md +32 -9
- package/skills/api-services/SKILL.md +1 -1
- package/skills/api-services/references/graph.md +1 -1
- package/skills/api-services/references/speech.md +1 -1
- package/skills/api-telemetry/SKILL.md +5 -5
- package/skills/api-testing/SKILL.md +9 -1
- package/skills/api-utils/SKILL.md +1 -1
- package/skills/api-workers/SKILL.md +12 -5
- package/skills/design-mcp-server/SKILL.md +20 -8
- package/skills/field-test/SKILL.md +9 -7
- package/skills/git-wrapup/SKILL.md +218 -0
- package/skills/maintenance/SKILL.md +8 -6
- package/skills/migrate-mcp-ts-template/SKILL.md +11 -7
- package/skills/multi-server-orchestration/SKILL.md +17 -5
- package/skills/multi-server-orchestration/references/greenfield-buildout.md +6 -3
- package/skills/multi-server-orchestration/references/maintenance-pass.md +11 -3
- package/skills/multi-server-orchestration/references/release-and-publish-pass.md +14 -25
- package/skills/multi-server-orchestration/references/wrapup-pass.md +13 -41
- package/skills/polish-docs-meta/SKILL.md +3 -1
- package/skills/polish-docs-meta/references/package-meta.md +1 -1
- package/skills/release-and-publish/SKILL.md +10 -9
- package/skills/report-issue-framework/SKILL.md +5 -3
- package/skills/report-issue-local/SKILL.md +10 -5
- package/skills/setup/SKILL.md +13 -8
- package/skills/tool-defs-analysis/SKILL.md +6 -3
- package/templates/CLAUDE.md +1 -0
- package/dist/logs/combined.log +0 -7
- package/dist/logs/error.log +0 -5
- package/dist/logs/interactions.log +0 -0
- 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.
|
|
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.
|
|
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.
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 `
|
|
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. **
|
|
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()`
|
|
237
|
-
- [ ] App resource `_meta.ui.csp`
|
|
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/`
|
|
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**
|
|
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
|
|
40
|
-
bun -e "import('
|
|
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/
|
|
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`
|
|
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
|
|
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
|
|
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. **
|
|
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`,
|
|
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
|
-
- [ ]
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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** —
|
|
67
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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**
|
|
94
|
-
|
|
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
|
-
- [ ]
|
|
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
|
-
- [ ]
|
|
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
|
-
- [ ]
|
|
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
|
|
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. **
|
|
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`,
|
|
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
|
|
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
|
-
- [ ]
|
|
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 `
|
|
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. **
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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`
|
|
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
|
|
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
|
package/skills/add-test/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
|
|
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
|
|
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
|
package/skills/add-tool/SKILL.md
CHANGED
|
@@ -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
|
|
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. **
|
|
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
|
-
- [ ]
|
|
538
|
+
- [ ] No `console` calls — use `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`)
|