@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/skills/api-auth/SKILL.md
CHANGED
|
@@ -141,7 +141,8 @@ A `WARNING`-level log is emitted at startup whenever the flag is active so opera
|
|
|
141
141
|
| `GET /healthz` | No |
|
|
142
142
|
| `GET /mcp` | No |
|
|
143
143
|
| `POST /mcp` | Yes (when auth enabled) |
|
|
144
|
-
| `
|
|
144
|
+
| `DELETE /mcp` | Yes (when auth enabled) — session termination |
|
|
145
|
+
| `OPTIONS /mcp` | No (handled by CORS middleware before auth) |
|
|
145
146
|
|
|
146
147
|
**CORS:** Set `MCP_ALLOWED_ORIGINS` to a comma-separated list of allowed origins, or `*` for open access.
|
|
147
148
|
|
|
@@ -196,7 +197,7 @@ interface AuthContext {
|
|
|
196
197
|
clientId: string; // Required — 'cid' or 'client_id' JWT claim
|
|
197
198
|
scopes: string[]; // Required — union of 'scp', 'scope', and 'mcp_tool_scopes' claims
|
|
198
199
|
sub: string; // Required — 'sub' claim; falls back to clientId when absent
|
|
199
|
-
token
|
|
200
|
+
token?: string; // Optional — raw JWT or OAuth bearer token string (present when transport provides it)
|
|
200
201
|
tenantId?: string; // Optional — 'tid' claim; present only for multi-tenant tokens
|
|
201
202
|
}
|
|
202
203
|
```
|
|
@@ -13,7 +13,7 @@ metadata:
|
|
|
13
13
|
|
|
14
14
|
`DataCanvas` is a primitive for **storage stashes, canvas computes**. The existing `IStorageProvider` is a key/value abstraction — it can stash blobs but exposes no analytical surface. `DataCanvas` is the analytical surface: register tabular data from upstream APIs, run SQL across multiple registered tables, and export results as CSV/Parquet/JSON.
|
|
15
15
|
|
|
16
|
-
**Tier 3** — `@duckdb/node-api` is an optional peer dependency. Servers that don't enable canvas pay zero install cost. Lazy-loaded on first use.
|
|
16
|
+
**Tier 3** — `@duckdb/node-api` is an optional peer dependency (`bun add @duckdb/node-api`). Servers that don't enable canvas pay zero install cost. Lazy-loaded on first use.
|
|
17
17
|
|
|
18
18
|
**Disabled by default.** Set `CANVAS_PROVIDER_TYPE=duckdb` to enable. Otherwise `core.canvas` is `undefined`.
|
|
19
19
|
|
|
@@ -27,7 +27,27 @@ metadata:
|
|
|
27
27
|
import type { DataCanvas, CanvasInstance, ColumnSchema } from '@cyanheads/mcp-ts-core/canvas';
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The framework wires the optional service onto `CoreServices
|
|
30
|
+
The framework wires the optional service onto `CoreServices`, accessible in the `setup()` callback — **not on `Context`**. Handlers access canvas via a module-level accessor:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// src/services/canvas-accessor.ts
|
|
34
|
+
import type { DataCanvas } from '@cyanheads/mcp-ts-core/canvas';
|
|
35
|
+
|
|
36
|
+
let _canvas: DataCanvas | undefined;
|
|
37
|
+
export const setCanvas = (c: DataCanvas | undefined) => { _canvas = c; };
|
|
38
|
+
export const getCanvas = () => _canvas;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/index.ts — wire in setup()
|
|
43
|
+
import { setCanvas } from './services/canvas-accessor.js';
|
|
44
|
+
|
|
45
|
+
await createApp({
|
|
46
|
+
setup(core) {
|
|
47
|
+
setCanvas(core.canvas);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
31
51
|
|
|
32
52
|
```ts
|
|
33
53
|
interface CoreServices {
|
|
@@ -74,7 +94,11 @@ The sweeper runs as an `unref`'d `setInterval` — does not keep the event loop
|
|
|
74
94
|
Resolves an existing canvas or creates a new one. Returns a {@link CanvasInstance} bound to `(canvasId, tenantId)`. Subsequent operations don't repeat them.
|
|
75
95
|
|
|
76
96
|
```ts
|
|
77
|
-
|
|
97
|
+
import { getCanvas } from '@/services/canvas-accessor.js';
|
|
98
|
+
|
|
99
|
+
const canvas = getCanvas();
|
|
100
|
+
if (!canvas) throw new Error('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
101
|
+
const instance = await canvas.acquire(input.canvas_id, ctx);
|
|
78
102
|
// instance.canvasId — surface to the agent
|
|
79
103
|
// instance.isNew — true on first call
|
|
80
104
|
// instance.expiresAt — ISO 8601 after sliding extension
|
|
@@ -116,7 +140,7 @@ const joined = await instance.query(`
|
|
|
116
140
|
// joined.tableName === 'g_with_obs'; joined.rows.length === 10; joined.rowCount === <full count>
|
|
117
141
|
```
|
|
118
142
|
|
|
119
|
-
`registerAs` rejects with `
|
|
143
|
+
`registerAs` rejects with `ValidationError` (`data.reason: 'register_as_clash'`) if the target name already exists — drop it first.
|
|
120
144
|
|
|
121
145
|
**Read-only enforcement** (four layers):
|
|
122
146
|
1. Text-level deny-list — pre-parse scan for file/HTTP-reading table functions (`read_csv*`, `read_json*`, `read_parquet*`, `read_text`, `read_blob`, `glob`, `iceberg_scan`, `delta_scan`, `postgres_scan`, `mysql_scan`, `sqlite_scan`, plus pre-staged spatial ones).
|
|
@@ -225,6 +249,7 @@ If your tool surfaces row data via `structuredContent`, the JSON-safe shape flow
|
|
|
225
249
|
|
|
226
250
|
```ts
|
|
227
251
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
252
|
+
import { getCanvas } from '@/services/canvas-accessor.js';
|
|
228
253
|
|
|
229
254
|
export const fetchAndStage = tool('fetch_and_stage_germplasm', {
|
|
230
255
|
description: 'Fetch germplasm matching a query and stage it on a DataCanvas for follow-up SQL.',
|
|
@@ -245,7 +270,7 @@ export const fetchAndStage = tool('fetch_and_stage_germplasm', {
|
|
|
245
270
|
expires_at: z.string().describe('ISO 8601 expiry after sliding 24h window'),
|
|
246
271
|
}),
|
|
247
272
|
async handler(input, ctx) {
|
|
248
|
-
const canvas =
|
|
273
|
+
const canvas = getCanvas();
|
|
249
274
|
if (!canvas) {
|
|
250
275
|
throw new Error('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
251
276
|
}
|
|
@@ -334,7 +359,7 @@ When the preview budget is small (single-digit rows) and the sniff window matter
|
|
|
334
359
|
|
|
335
360
|
### Out of scope
|
|
336
361
|
|
|
337
|
-
- **Provenance metadata** (source URI, original query). Caller stores externally —
|
|
362
|
+
- **Provenance metadata** (source URI, original query). Caller stores externally via `ctx.state` or tool output — canvas tables carry data only, not lineage.
|
|
338
363
|
- **Pagination-flavored builder.** A `paginate(fetchPage) → AsyncIterable<Row>` adapter is deferred until a second non-paginated consumer surfaces.
|
|
339
364
|
- **Token-accurate budget.** `previewTokens` (tokenizer-driven) is a future option; characters cover the common case.
|
|
340
365
|
- **`caps.maxBytes`.** Row caps cover the common case without re-doing serialization the canvas appender skips.
|
|
@@ -363,6 +388,18 @@ When the preview budget is small (single-digit rows) and the sniff window matter
|
|
|
363
388
|
|
|
364
389
|
---
|
|
365
390
|
|
|
391
|
+
## Checklist
|
|
392
|
+
|
|
393
|
+
- [ ] `@duckdb/node-api` installed as a peer dependency (`bun add @duckdb/node-api`)
|
|
394
|
+
- [ ] `CANVAS_PROVIDER_TYPE=duckdb` set in `.env`
|
|
395
|
+
- [ ] Canvas accessor module created (`src/services/canvas-accessor.ts` or equivalent)
|
|
396
|
+
- [ ] Accessor wired in `setup()` callback via `setCanvas(core.canvas)`
|
|
397
|
+
- [ ] Handler guards for canvas availability (`if (!canvas) throw ...`)
|
|
398
|
+
- [ ] `canvas_id` accepted as optional input, returned in output
|
|
399
|
+
- [ ] SQL queries are read-only (enforced by the four-layer gate, but don't attempt writes)
|
|
400
|
+
- [ ] Testing: mock the module-level `getCanvas()` accessor with `vi.spyOn` or a test setup that calls `setCanvas(mockCanvas)`
|
|
401
|
+
- [ ] `bun run devcheck` passes
|
|
402
|
+
|
|
366
403
|
## Related skills
|
|
367
404
|
|
|
368
405
|
- `add-tool` — scaffold a new MCP tool definition (use the canvas template above)
|
|
@@ -59,6 +59,10 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod from environment variable
|
|
|
59
59
|
| `MCP_RESPONSE_VERBOSITY` | `mcpResponseVerbosity` | `standard` | `minimal` \| `standard` \| `full` |
|
|
60
60
|
| `MCP_ALLOWED_ORIGINS` | `mcpAllowedOrigins` | — | Comma-separated list; omit to allow all |
|
|
61
61
|
| `MCP_SERVER_RESOURCE_IDENTIFIER` | `mcpServerResourceIdentifier` | — | RFC 8707 resource indicator URL |
|
|
62
|
+
| `MCP_PUBLIC_URL` | `mcpPublicUrl` | — | Public-facing origin for reverse proxies (Cloudflare Tunnel, nginx, ALB) so emitted URLs carry the correct scheme |
|
|
63
|
+
| `MCP_HEARTBEAT_INTERVAL_MS` | `mcpHeartbeatIntervalMs` | `0` (disabled) | Heartbeat ping interval; 0 disables |
|
|
64
|
+
| `MCP_HEARTBEAT_MISS_THRESHOLD` | `mcpHeartbeatMissThreshold` | `3` | Missed heartbeats before session is considered stale |
|
|
65
|
+
| `MCP_GC_PRESSURE_INTERVAL_MS` | `mcpGcPressureIntervalMs` | `0` (disabled) | Bun-only opt-in forced GC loop for HTTP deployments with heap growth |
|
|
62
66
|
|
|
63
67
|
---
|
|
64
68
|
|
|
@@ -75,6 +79,8 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod from environment variable
|
|
|
75
79
|
| `OAUTH_JWKS_COOLDOWN_MS` | `oauthJwksCooldownMs` | `300000` | 5 min; min time between JWKS refetches |
|
|
76
80
|
| `OAUTH_JWKS_TIMEOUT_MS` | `oauthJwksTimeoutMs` | `5000` | JWKS fetch timeout (ms) |
|
|
77
81
|
| `DEV_MCP_AUTH_BYPASS` | `devMcpAuthBypass` | `false` | Skip auth in development; blocked in `production` |
|
|
82
|
+
| `MCP_JWT_EXPECTED_ISSUER` | `mcpJwtExpectedIssuer` | — | Optional issuer validation for JWT mode |
|
|
83
|
+
| `MCP_JWT_EXPECTED_AUDIENCE` | `mcpJwtExpectedAudience` | — | Optional audience validation for JWT mode |
|
|
78
84
|
| `DEV_MCP_CLIENT_ID` | `devMcpClientId` | — | Dev-only: override client ID |
|
|
79
85
|
| `DEV_MCP_SCOPES` | `devMcpScopes` | — | Dev-only: comma-separated scope overrides |
|
|
80
86
|
|
|
@@ -42,9 +42,11 @@ interface Context {
|
|
|
42
42
|
readonly elicit?: (message: string, schema: z.ZodObject<z.ZodRawShape>) => Promise<ElicitResult>;
|
|
43
43
|
readonly sample?: (messages: SamplingMessage[], opts?: SamplingOpts) => Promise<CreateMessageResult>;
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Notifications — present when transport supports them
|
|
46
46
|
readonly notifyResourceListChanged?: () => void;
|
|
47
47
|
readonly notifyResourceUpdated?: (uri: string) => void;
|
|
48
|
+
readonly notifyPromptListChanged?: () => void;
|
|
49
|
+
readonly notifyToolListChanged?: () => void;
|
|
48
50
|
|
|
49
51
|
// Cancellation
|
|
50
52
|
readonly signal: AbortSignal;
|
|
@@ -415,8 +417,10 @@ Present only when the definition declares an `errors[]` contract. Builds an `Mcp
|
|
|
415
417
|
export const fetchItems = tool('fetch_items', {
|
|
416
418
|
description: 'Fetch items by ID.',
|
|
417
419
|
errors: [
|
|
418
|
-
{ reason: 'no_match', code: JsonRpcErrorCode.NotFound, when: 'No items matched'
|
|
419
|
-
|
|
420
|
+
{ reason: 'no_match', code: JsonRpcErrorCode.NotFound, when: 'No items matched',
|
|
421
|
+
recovery: 'Broaden the query or check the spelling and try again.' },
|
|
422
|
+
{ reason: 'queue_full', code: JsonRpcErrorCode.RateLimited, when: 'Local queue at capacity', retryable: true,
|
|
423
|
+
recovery: 'Wait a few seconds before retrying or reduce batch size.' },
|
|
420
424
|
],
|
|
421
425
|
input: z.object({ ids: z.array(z.string()).describe('Item IDs') }),
|
|
422
426
|
output: z.object({ items: z.array(ItemSchema).describe('Resolved items') }),
|
|
@@ -534,6 +538,8 @@ The `≥5 words` lint rule on contract `recovery` (validated at lint time) makes
|
|
|
534
538
|
| `ctx.sample` | `function \| undefined` | Client supports sampling |
|
|
535
539
|
| `ctx.notifyResourceListChanged` | `function \| undefined` | Transport supports resource notifications |
|
|
536
540
|
| `ctx.notifyResourceUpdated` | `function \| undefined` | Transport supports resource notifications |
|
|
541
|
+
| `ctx.notifyPromptListChanged` | `function \| undefined` | Transport supports prompt notifications |
|
|
542
|
+
| `ctx.notifyToolListChanged` | `function \| undefined` | Transport supports tool notifications |
|
|
537
543
|
| `ctx.progress` | `ContextProgress \| undefined` | Tool defined with `task: true` |
|
|
538
544
|
| `ctx.uri` | `URL \| undefined` | Resource handlers only |
|
|
539
545
|
| `ctx.fail` | `(reason, msg?, data?, opts?) => McpError` | Definition declares `errors[]` contract |
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
Error handling in `@cyanheads/mcp-ts-core` follows a strict layered pattern: tool and resource handlers throw `McpError` freely (no try/catch), the handler factory catches and normalizes all errors, and services use `ErrorHandler.tryCatch` for
|
|
14
|
+
Error handling in `@cyanheads/mcp-ts-core` follows a strict layered pattern: tool and resource handlers throw `McpError` freely (no try/catch), the handler factory catches and normalizes all errors, and services use `ErrorHandler.tryCatch` for structured logging and wrapping.
|
|
15
15
|
|
|
16
16
|
**Imports:**
|
|
17
17
|
|
|
@@ -64,7 +64,7 @@ export const fetchTool = tool('fetch_articles', {
|
|
|
64
64
|
|:--------|:---------|
|
|
65
65
|
| Compile time | `ctx.fail('typo')` is a TS error. Auto-completes declared reasons. |
|
|
66
66
|
| Runtime | `ctx.fail(reason, msg?, data?, options?)` builds an `McpError(contract.code, msg, { ...data, reason }, options)` — `data.reason` is auto-populated from the contract and cannot be overridden by caller-supplied data (spread first, then `reason` written last), so observers see a stable identifier. `options` accepts `{ cause }` for ES2022 error chaining. |
|
|
67
|
-
| Lint (
|
|
67
|
+
| Lint (devcheck) | Each `code` validated against `JsonRpcErrorCode`. Reasons validated as snake_case + unique within contract. `recovery` validated as non-empty and ≥ 5 words. Build-time only — not invoked at server startup. |
|
|
68
68
|
| Lint (conformance) | If the handler `throw new McpError(JsonRpcErrorCode.X)` outside `ctx.fail`, conformance check warns when X isn't declared. |
|
|
69
69
|
|
|
70
70
|
> **`recovery` is opt-in resolution, not auto-population.** The contract `recovery` is required metadata documenting the agent's next move when this failure mode fires (a forcing function for thoughtful guidance — placeholders like "Try again." get flagged by the linter). It does **not** automatically appear in runtime `data.recovery.hint` — the framework never injects it without an explicit signal at the throw site. Authors opt in by spreading `ctx.recoveryFor('reason')` into the `data` argument, the same way `ctx.fail('reason')` opts into resolving the contract `code`. What the author types at the throw site is what flows to the wire, with no hidden transformation; the resolver is just a typed lookup keyed by the same `reason` the author already typed.
|
|
@@ -126,7 +126,7 @@ throw ctx.fail('no_match', `No item ${id}`, {
|
|
|
126
126
|
|
|
127
127
|
### Carrying contract `reason` from services
|
|
128
128
|
|
|
129
|
-
Services don't
|
|
129
|
+
Services don't receive `ctx` automatically (unlike handlers), so they can't call `ctx.fail` directly — though `ctx` can be passed as a parameter when needed. To make a service-thrown failure carry the contract's `reason` on the wire, **pass `data: { reason: 'X' }` to the factory**. The framework's auto-classifier preserves `data` unchanged, so clients see the same `error.data.reason` they'd see from `ctx.fail`:
|
|
130
130
|
|
|
131
131
|
```ts
|
|
132
132
|
// my-service.ts
|
|
@@ -271,7 +271,7 @@ Use factories or `McpError` directly when the code must be exact — auto-classi
|
|
|
271
271
|
The framework applies these steps in order — first match wins:
|
|
272
272
|
|
|
273
273
|
1. **`McpError` instance** — `error.code` is preserved as-is; no classification needed.
|
|
274
|
-
2. **JS constructor name** — matched against a fixed table (e.g. `
|
|
274
|
+
2. **JS constructor name** — matched against a fixed table (e.g. `ZodError` → `ValidationError`, `SyntaxError` → `ValidationError`). Note: `TypeError` is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.
|
|
275
275
|
3. **Provider-specific patterns** — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are more specific (e.g. `status code 429` beats the generic `rate limit` pattern).
|
|
276
276
|
4. **Common message/name patterns** — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
|
|
277
277
|
5. **`AbortError` name** — `error.name === 'AbortError'` → `Timeout`.
|
|
@@ -344,7 +344,7 @@ Checked before common patterns. Cover: AWS exception names, HTTP status codes, D
|
|
|
344
344
|
| Tool/resource handlers | Throw `McpError` — no try/catch |
|
|
345
345
|
| Handler factory (tools) | Catches all errors, normalizes to `McpError`, sets `isError: true`, mirrors error across both client surfaces (see [Error-path parity](#error-path-parity)) |
|
|
346
346
|
| Handler factory (resources) | Catches and re-throws to the SDK, which routes through the JSON-RPC error envelope |
|
|
347
|
-
| Services/setup code | `ErrorHandler.tryCatch` for
|
|
347
|
+
| Services/setup code | `ErrorHandler.tryCatch` for structured logging and wrapping (always rethrows — never swallows) |
|
|
348
348
|
|
|
349
349
|
### Error-path parity
|
|
350
350
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-linter
|
|
3
3
|
description: >
|
|
4
|
-
MCP definition linter rules reference. Use when `bun run lint:mcp
|
|
4
|
+
MCP definition linter rules reference. Use when `bun run lint:mcp` or `bun run devcheck` reports a lint error or warning (`format-parity`, `schema-is-object`, `name-format`, `server-json-*`, etc.) and you need to understand the rule, its severity, and how to fix it. Every rule ID the linter emits has an entry in this doc.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
7
|
version: "1.3"
|
|
@@ -11,19 +11,18 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
The linter validates tool, resource, and prompt definitions against the MCP spec and framework conventions. It runs in
|
|
14
|
+
The linter validates tool, resource, and prompt definitions against the MCP spec and framework conventions. **It is build-time only — not invoked at server startup.** It runs in two places:
|
|
15
15
|
|
|
16
16
|
| Entry point | When | On failure |
|
|
17
17
|
|:------------|:-----|:-----------|
|
|
18
|
-
| `createApp()` / `createWorkerHandler()` | Every startup | Throws `ConfigurationError`; process exits with a formatted banner. Warnings are logged and startup continues. |
|
|
19
18
|
| `bun run lint:mcp` | Manual or CI | Prints errors + warnings, exits non-zero on errors. |
|
|
20
19
|
| `bun run devcheck` | Pre-commit workflow | Wraps `lint:mcp` alongside typecheck, format, `bun audit`, `bun outdated`. |
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Both surface the same `LintReport` from `validateDefinitions()` (exported from `@cyanheads/mcp-ts-core/linter`). Each diagnostic has a stable `rule` ID — that's the anchor you land on via the `See: skills/api-linter/SKILL.md#<rule>` breadcrumb appended to every message.
|
|
23
22
|
|
|
24
23
|
**Severity:**
|
|
25
|
-
- **error** — MUST-level spec violation; blocks
|
|
26
|
-
- **warning** — SHOULD-level or quality issue; logged but
|
|
24
|
+
- **error** — MUST-level spec violation; blocks `devcheck`.
|
|
25
|
+
- **warning** — SHOULD-level or quality issue; logged but `devcheck` continues.
|
|
27
26
|
|
|
28
27
|
**Imports (if you need to run the linter programmatically):**
|
|
29
28
|
|
|
@@ -52,7 +51,7 @@ Grouped by family. Jump to any rule ID via its anchor.
|
|
|
52
51
|
| Landing | `landing-*` (23 rules — shape, tagline, logo, links, repo, envExample, connectSnippets, theme) | [Landing config rules](#landing-config-rules) |
|
|
53
52
|
| Prompts | `generate-required` | [Prompt rules](#prompt-rules) |
|
|
54
53
|
| Handler body | `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error` | [Handler body rules](#handler-body-rules) |
|
|
55
|
-
| Error contract (structural) | `error-contract-type`, `error-contract-empty`, `error-contract-entry-type`, `error-contract-code-type`, `error-contract-code-unknown`, `error-contract-code-unknown-error`, `error-contract-reason-required`, `error-contract-reason-format`, `error-contract-reason-unique`, `error-contract-when-required`, `error-contract-retryable-type` | [Error contract rules](#error-contract-rules) |
|
|
54
|
+
| Error contract (structural) | `error-contract-type`, `error-contract-empty`, `error-contract-entry-type`, `error-contract-code-type`, `error-contract-code-unknown`, `error-contract-code-unknown-error`, `error-contract-reason-required`, `error-contract-reason-format`, `error-contract-reason-unique`, `error-contract-when-required`, `error-contract-retryable-type`, `error-contract-recovery-required`, `error-contract-recovery-empty`, `error-contract-recovery-min-words` | [Error contract rules](#error-contract-rules) |
|
|
56
55
|
| Error contract (conformance) | `error-contract-conformance`, `error-contract-prefer-fail` | [Error contract rules](#error-contract-rules) |
|
|
57
56
|
| server.json | ~40 rules prefixed `server-json-*` | [server.json rules](#server-json-rules) |
|
|
58
57
|
|
|
@@ -515,7 +514,9 @@ Heuristic source-text checks that scan `handler.toString()` for common error-han
|
|
|
515
514
|
|
|
516
515
|
Fires when a handler contains `throw new Error(...)`. Plain `Error` doesn't carry a JSON-RPC code — the framework's auto-classifier degrades to `InternalError`, hiding the actual failure mode.
|
|
517
516
|
|
|
518
|
-
|
|
517
|
+
Plain `Error` is acceptable for "don't care" cases where the specific code doesn't matter (per CLAUDE.md: "plain `Error` for don't-care cases"). This rule targets domain-specific failures that deserve a concrete code — upgrade those to factories or `ctx.fail`, and accept the warning for the rest.
|
|
518
|
+
|
|
519
|
+
**Fix:** use `McpError` or a factory for domain-specific failures:
|
|
519
520
|
|
|
520
521
|
```ts
|
|
521
522
|
// instead of:
|
|
@@ -596,7 +597,7 @@ Fires when `errors: []` is declared. An empty contract is a no-op — nothing to
|
|
|
596
597
|
|
|
597
598
|
**Severity:** error
|
|
598
599
|
|
|
599
|
-
Fires when an entry in `errors[]` isn't an object. Each entry must be `{ code, reason, when }` (and optionally `retryable`).
|
|
600
|
+
Fires when an entry in `errors[]` isn't an object. Each entry must be `{ code, reason, when, recovery }` (and optionally `retryable`).
|
|
600
601
|
|
|
601
602
|
### error-contract-code-type
|
|
602
603
|
|
|
@@ -654,6 +655,28 @@ Fires when an entry's `when` field is missing or empty. `when` is the human-read
|
|
|
654
655
|
|
|
655
656
|
Fires when an entry's optional `retryable` field is present but isn't a boolean. Only `true` or `false` is meaningful — drop the field if you can't commit to either.
|
|
656
657
|
|
|
658
|
+
### error-contract-recovery-required
|
|
659
|
+
|
|
660
|
+
**Severity:** error
|
|
661
|
+
|
|
662
|
+
Fires when an entry's `recovery` field is missing or not a string. `recovery` is the agent's next-move guidance when this failure fires — it flows to the wire via `ctx.recoveryFor`.
|
|
663
|
+
|
|
664
|
+
### error-contract-recovery-empty
|
|
665
|
+
|
|
666
|
+
**Severity:** error
|
|
667
|
+
|
|
668
|
+
Fires when `recovery` is an empty string. A blank recovery is worse than none — it suggests the field was considered and deliberately left empty.
|
|
669
|
+
|
|
670
|
+
**Fix:** write a concrete recovery hint (≥5 words).
|
|
671
|
+
|
|
672
|
+
### error-contract-recovery-min-words
|
|
673
|
+
|
|
674
|
+
**Severity:** warning
|
|
675
|
+
|
|
676
|
+
Fires when `recovery` has fewer than 5 words. Short recoveries like "Try again." are too vague to guide an agent's next action.
|
|
677
|
+
|
|
678
|
+
**Fix:** expand with specifics — what to try, what parameter to change, which tool to call instead.
|
|
679
|
+
|
|
657
680
|
### error-contract-conformance
|
|
658
681
|
|
|
659
682
|
**Severity:** warning
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
Service
|
|
14
|
+
Service providers are exported from `@cyanheads/mcp-ts-core/services`. These are documented here for servers that use the built-in providers.
|
|
15
15
|
|
|
16
16
|
All services follow the **init/accessor pattern**: initialized in `setup()`, accessed at request time via lazy accessor. See the `add-service` skill for the full pattern.
|
|
17
17
|
|
|
@@ -111,7 +111,7 @@ const path = await graphService.shortestPath('user:alice', 'user:charlie', conte
|
|
|
111
111
|
algorithm: 'bfs',
|
|
112
112
|
maxLength: 4,
|
|
113
113
|
});
|
|
114
|
-
|
|
114
|
+
// path.vertices.length gives the hop count
|
|
115
115
|
|
|
116
116
|
// Check reachability
|
|
117
117
|
const connected = await graphService.pathExists('user:alice', 'user:charlie', context, 3);
|
|
@@ -24,7 +24,7 @@ The provider interface — implemented by ElevenLabs (TTS) and Whisper (STT):
|
|
|
24
24
|
| `.getSTTProvider()` | `ISpeechProvider` | Throws `McpError(InvalidRequest)` if no STT provider configured |
|
|
25
25
|
| `.hasTTS()` | `boolean` | Check if TTS is available |
|
|
26
26
|
| `.hasSTT()` | `boolean` | Check if STT is available |
|
|
27
|
-
| `.healthCheck()` | `Promise<{ tts: boolean; stt: boolean }>` | Checks both providers
|
|
27
|
+
| `.healthCheck()` | `Promise<{ tts: boolean; stt: boolean }>` | Checks both providers sequentially |
|
|
28
28
|
|
|
29
29
|
## Providers
|
|
30
30
|
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
The framework auto-instruments every tool, resource, prompt, storage, LLM, speech, and graph call — each gets its own span and the standard counters/histograms. HTTP server requests pick up spans from `HttpInstrumentation` (
|
|
14
|
+
The framework auto-instruments every tool, resource, prompt, storage, LLM, speech, and graph call — each gets its own span and the standard counters/histograms. HTTP server requests pick up spans from `HttpInstrumentation` (all Node.js HTTP traffic, skips `/healthz`) plus `httpInstrumentationMiddleware` from `@hono/otel` on the MCP HTTP endpoint when installed (optional Tier 3 peer — `bun add @hono/otel`). On Bun, `HttpInstrumentation` silently no-ops and `@hono/otel` is the only HTTP coverage. Auth checks, session lifecycle, and task lifecycle are tracked as **metrics only** — auth decorates the active HTTP span with attributes, sessions and tasks emit counters.
|
|
15
15
|
|
|
16
16
|
`requestId`, `traceId`, and `tenantId` correlate automatically across spans, metrics, and logs. Pino logs get `trace_id`/`span_id` injected when a span is active.
|
|
17
17
|
|
|
@@ -76,7 +76,7 @@ Trace context propagates across boundaries via W3C `traceparent` headers. See `a
|
|
|
76
76
|
|
|
77
77
|
## Metrics
|
|
78
78
|
|
|
79
|
-
All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` where standard semconv applies). Lazy-initialized on first emission;
|
|
79
|
+
All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` where standard semconv applies). Lazy-initialized on first emission; tool, resource, prompt, `http.client.request.duration`, heartbeat, session, auth, rate-limit, and error metrics are eagerly created at startup so series exist from the first export cycle. LLM, speech, graph, and storage instruments are lazy-initialized on first use.
|
|
80
80
|
|
|
81
81
|
### Tools, resources, prompts
|
|
82
82
|
|
|
@@ -86,17 +86,17 @@ All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` wher
|
|
|
86
86
|
| `mcp.tool.duration` | histogram | `ms` | `mcp.tool.name`, `mcp.tool.success` |
|
|
87
87
|
| `mcp.tool.errors` | counter | `{errors}` | `mcp.tool.name`, `mcp.tool.error_category` (`upstream`/`server`/`client`) |
|
|
88
88
|
| `mcp.tool.input_bytes` | histogram | `bytes` | `mcp.tool.name` |
|
|
89
|
-
| `mcp.tool.output_bytes` | histogram | `bytes` | `mcp.tool.name` |
|
|
89
|
+
| `mcp.tool.output_bytes` | histogram | `bytes` | `mcp.tool.name` (success only) |
|
|
90
90
|
| `mcp.tool.param.usage` | counter | `{uses}` | `mcp.tool.name`, `mcp.tool.param` (top-level keys supplied by caller) |
|
|
91
91
|
| `mcp.resource.reads` | counter | `{reads}` | `mcp.resource.name`, `mcp.resource.success` |
|
|
92
92
|
| `mcp.resource.duration` | histogram | `ms` | `mcp.resource.name`, `mcp.resource.success` |
|
|
93
93
|
| `mcp.resource.errors` | counter | `{errors}` | `mcp.resource.name` |
|
|
94
|
-
| `mcp.resource.output_bytes` | histogram | `bytes` | `mcp.resource.name` |
|
|
94
|
+
| `mcp.resource.output_bytes` | histogram | `bytes` | `mcp.resource.name` (success only) |
|
|
95
95
|
| `mcp.prompt.generations` | counter | `{generations}` | `mcp.prompt.name`, `mcp.prompt.success` |
|
|
96
96
|
| `mcp.prompt.duration` | histogram | `ms` | `mcp.prompt.name`, `mcp.prompt.success` |
|
|
97
97
|
| `mcp.prompt.errors` | counter | `{errors}` | `mcp.prompt.name`, `mcp.prompt.error_category` |
|
|
98
98
|
| `mcp.prompt.input_bytes` | histogram | `bytes` | `mcp.prompt.name` |
|
|
99
|
-
| `mcp.prompt.output_bytes` | histogram | `bytes` | `mcp.prompt.name` |
|
|
99
|
+
| `mcp.prompt.output_bytes` | histogram | `bytes` | `mcp.prompt.name` (success only) |
|
|
100
100
|
| `mcp.prompt.message_count` | histogram | `{messages}` | `mcp.prompt.name` |
|
|
101
101
|
| `mcp.requests.active` | up/down counter | `{requests}` | — (in-flight handler executions, all three types) |
|
|
102
102
|
|
|
@@ -13,6 +13,8 @@ metadata:
|
|
|
13
13
|
|
|
14
14
|
Tests target handler behavior directly — call `handler(input, ctx)`, assert on the return value or thrown error. The framework's handler factory (try/catch, formatting, telemetry) is not involved. Use `createMockContext` from `@cyanheads/mcp-ts-core/testing` to construct the `ctx` argument.
|
|
15
15
|
|
|
16
|
+
**Additional exports from `/testing`:** `createMockLogger()` returns a standalone `MockContextLogger` for unit-testing code that accepts a `ContextLogger` directly (services, utilities). `createInMemoryStorage(options?)` provides a real `StorageService` backed by `InMemoryProvider` for testing services that take a `StorageService` dependency.
|
|
17
|
+
|
|
16
18
|
**Philosophy:** Test behavior, not implementation. Refactors should not break tests. Match the repo's existing test layout: fresh scaffolds use `tests/`, while colocated `src/**/*.test.ts` files are also supported. Integration tests at I/O boundaries over unit tests of internals.
|
|
17
19
|
|
|
18
20
|
---
|
|
@@ -43,9 +45,12 @@ interface MockContextOptions {
|
|
|
43
45
|
auth?: AuthContext;
|
|
44
46
|
elicit?: (message: string, schema: z.ZodObject<z.ZodRawShape>) => Promise<ElicitResult>;
|
|
45
47
|
errors?: readonly ErrorContract[];
|
|
48
|
+
notifyPromptListChanged?: () => void;
|
|
46
49
|
notifyResourceListChanged?: () => void;
|
|
47
50
|
notifyResourceUpdated?: (uri: string) => void;
|
|
51
|
+
notifyToolListChanged?: () => void;
|
|
48
52
|
progress?: boolean;
|
|
53
|
+
sessionId?: string;
|
|
49
54
|
requestId?: string;
|
|
50
55
|
sample?: (messages: SamplingMessage[], opts?: SamplingOpts) => Promise<CreateMessageResult>;
|
|
51
56
|
signal?: AbortSignal;
|
|
@@ -60,8 +65,11 @@ interface MockContextOptions {
|
|
|
60
65
|
| `auth` | Sets `ctx.auth` for scope-checking tests |
|
|
61
66
|
| `elicit` | Assigns a function to `ctx.elicit` for testing elicitation calls |
|
|
62
67
|
| `errors` | Attaches a typed `ctx.fail` against the contract — same wiring the production handler factory uses. Pass `myTool.errors` directly. |
|
|
68
|
+
| `notifyPromptListChanged` | Assigns `ctx.notifyPromptListChanged` for prompt-list change notification tests |
|
|
63
69
|
| `notifyResourceListChanged` | Assigns `ctx.notifyResourceListChanged` for resource notification tests |
|
|
64
70
|
| `notifyResourceUpdated` | Assigns `ctx.notifyResourceUpdated` for resource update notification tests |
|
|
71
|
+
| `notifyToolListChanged` | Assigns `ctx.notifyToolListChanged` for tool-list change notification tests |
|
|
72
|
+
| `sessionId` | Sets `ctx.sessionId` for handlers that branch on session ID |
|
|
65
73
|
| `progress` | Populates `ctx.progress` with real state-tracking implementation (see below) |
|
|
66
74
|
| `requestId` | Overrides `ctx.requestId` (default: `'test-request-id'`) |
|
|
67
75
|
| `sample` | Assigns a function to `ctx.sample` for testing sampling calls |
|
|
@@ -93,7 +101,7 @@ expect(progress._messages).toContain('step message');
|
|
|
93
101
|
|
|
94
102
|
### Mock logger
|
|
95
103
|
|
|
96
|
-
`ctx.log` captures all log calls for inspection.
|
|
104
|
+
`ctx.log` captures all log calls for inspection. Import `MockContextLogger` from `@cyanheads/mcp-ts-core/testing` and cast `ctx.log` to access the `.calls` array (the cast is necessary because `createMockContext` returns `Context`, which types `log` as `ContextLogger`):
|
|
97
105
|
|
|
98
106
|
```ts
|
|
99
107
|
import { createMockContext, type MockContextLogger } from '@cyanheads/mcp-ts-core/testing';
|
|
@@ -29,7 +29,7 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
|
|
|
29
29
|
|
|
30
30
|
| Export | API | Notes |
|
|
31
31
|
|:-------|:----|:------|
|
|
32
|
-
| `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean` and `signal?: AbortSignal` (external cancellation). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node
|
|
32
|
+
| `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean` and `signal?: AbortSignal` (external cancellation). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. |
|
|
33
33
|
| `withRetry` | `<T>(fn: () => Promise<T>, options?: RetryOptions) -> Promise<T>` | Executes `fn` with exponential backoff. Retries on transient errors (`ServiceUnavailable`, `Timeout`, `RateLimited`); non-transient errors fail immediately. On exhaustion, enriches the final error with attempt count in message and `data.retryAttempts`. **Place the retry boundary around the full pipeline** (fetch + parse), not just the network call. `RetryOptions`: `maxRetries` (default `3`), `baseDelayMs` (default `1000`), `maxDelayMs` (default `30000`), `jitter` (default `0.25`), `operation` (log label), `context` (RequestContext), `signal` (AbortSignal), `isTransient` (custom predicate). |
|
|
34
34
|
| `httpErrorFromResponse` | `(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>` | Maps an HTTP `Response` to a properly classified `McpError` — full status table including 401/403/408/422/429/5xx, body capture (truncated), `retry-after` header, optional `cause`. Use this instead of hand-rolling `if (status === 429) ...` ladders. Reads the response body — `clone()` first if you need it elsewhere. `HttpErrorFromResponseOptions`: `service?` (logical name in message, e.g. `'NCBI'`), `captureBody?` (default `true`), `bodyLimit?` (default `500`), `data?` (extra fields merged into `error.data`), `cause?`, `codeOverride?` (per-status mapping override). Pairs naturally with `withRetry` — both classify codes the same way. |
|
|
35
35
|
| `httpStatusToErrorCode` | `(status: number) -> JsonRpcErrorCode \| undefined` | Sync status → code lookup. Returns `undefined` for 1xx/2xx/3xx. Use when you need just the code without a `Response` object handy. |
|
|
@@ -59,7 +59,7 @@ Fresh scaffolds register definitions directly in the entry point as shown above.
|
|
|
59
59
|
|
|
60
60
|
- **Per-request `McpServer` factory**: a new server instance is created for each request. Required by SDK security advisory GHSA-345p-7cg4-v4c7.
|
|
61
61
|
- **Env bindings refreshed per-request**: Cloudflare may rotate binding object references between requests; the handler re-injects them on every call.
|
|
62
|
-
-
|
|
62
|
+
- **OTel NodeSDK is disabled in Workers** — `canUseNodeSDK()` returns `false` for V8 isolates, so no OTLP spans or metrics are emitted. Structured logs via `ctx.log` still work. `OTEL_ENABLED=true` has no effect in Workers. `ctx.waitUntil()` is received and passed through to `app.fetch` and `onScheduled` but not called by the framework (nothing to flush asynchronously).
|
|
63
63
|
- **Singleton app promise with retry-on-failure**: the framework init runs once; if it fails, the next request retries rather than leaving the Worker in a permanently broken state.
|
|
64
64
|
|
|
65
65
|
---
|
|
@@ -130,7 +130,7 @@ In Workers, only these storage providers are allowed:
|
|
|
130
130
|
`filesystem`, `supabase`, and unknown provider types are not on the whitelist:
|
|
131
131
|
|
|
132
132
|
- **`filesystem`** and unknown types throw `ConfigurationError` in serverless environments.
|
|
133
|
-
- **`supabase`** does **not** silently fall back. The
|
|
133
|
+
- **`supabase`** does **not** silently fall back. The serverless provider whitelist check fires immediately at the top of `createStorageProvider()` — Supabase credentials are never validated. Worker startup fails with `ConfigurationError` because Supabase is not on the serverless whitelist. Do not set `STORAGE_PROVIDER_TYPE=supabase` in a Worker.
|
|
134
134
|
|
|
135
135
|
Set `STORAGE_PROVIDER_TYPE` to one of the four whitelisted values to avoid unexpected behavior.
|
|
136
136
|
|
|
@@ -142,17 +142,24 @@ Set `STORAGE_PROVIDER_TYPE` to one of the four whitelisted values to avoid unexp
|
|
|
142
142
|
compatibility_flags = ["nodejs_compat"]
|
|
143
143
|
compatibility_date = "2025-09-01" # must be >= 2025-09-01
|
|
144
144
|
|
|
145
|
+
# Built-in storage providers require these exact binding names:
|
|
145
146
|
[[kv_namespaces]]
|
|
146
|
-
binding = "
|
|
147
|
+
binding = "KV_NAMESPACE" # required for cloudflare-kv storage
|
|
147
148
|
id = "..."
|
|
148
149
|
|
|
149
150
|
[[r2_buckets]]
|
|
150
|
-
binding = "
|
|
151
|
+
binding = "R2_BUCKET" # required for cloudflare-r2 storage
|
|
151
152
|
bucket_name = "..."
|
|
153
|
+
|
|
154
|
+
[[d1_databases]]
|
|
155
|
+
binding = "DB" # required for cloudflare-d1 storage
|
|
156
|
+
database_id = "..."
|
|
152
157
|
```
|
|
153
158
|
|
|
154
159
|
`nodejs_compat` is required for Node.js API shims (e.g., `process.env`, `Buffer`, `crypto`). The minimum `compatibility_date` activates the required shim set.
|
|
155
160
|
|
|
161
|
+
**Binding names for core storage are hardcoded** — the storage factory looks for `KV_NAMESPACE`, `R2_BUCKET`, and `DB` on `globalThis`. Using different binding names will cause a `ConfigurationError`. For custom (non-storage) bindings, use `extraObjectBindings` to map arbitrary binding names to `globalThis` keys.
|
|
162
|
+
|
|
156
163
|
---
|
|
157
164
|
|
|
158
165
|
## Workers-specific warnings
|
|
@@ -190,4 +197,4 @@ export function getServerConfig() {
|
|
|
190
197
|
|
|
191
198
|
> `DuckDB canvas requires Node.js or Bun. Set CANVAS_PROVIDER_TYPE=none or omit it for Cloudflare Workers deployment.`
|
|
192
199
|
|
|
193
|
-
Leave the env unset (or set to `none`) for Worker deployments. Tools that conditionally use canvas should check `if (!
|
|
200
|
+
Leave the env unset (or set to `none`) for Worker deployments. Tools that conditionally use canvas should check the module-level accessor (`if (!getCanvas()) { ... }`) and surface a clear "feature unavailable on this deployment" message. See `api-canvas` for the full DataCanvas reference and setup wiring pattern.
|
|
@@ -29,6 +29,14 @@ Gather before designing. Ask the user if not obvious from context:
|
|
|
29
29
|
|
|
30
30
|
If the domain has a public API, read its docs before designing. For internal-only servers, skip API research and go straight to user goals. Don't design from vibes either way.
|
|
31
31
|
|
|
32
|
+
## Server Naming
|
|
33
|
+
|
|
34
|
+
The server name (repo name, npm package, public identity) must communicate what it does at a glance. The test: can a human or agent scanning a server list tell what this server does from the name alone?
|
|
35
|
+
|
|
36
|
+
- **Use the canonical platform/brand name, not abbreviations.** `libofcongress-mcp-server` not `loc-mcp-server` ("loc" reads as lines-of-code or location). `federal-reserve-mcp-server` not `fred-mcp-server` ("fred" reads as a person's name).
|
|
37
|
+
- **Add a descriptive suffix when the base name is a non-obvious acronym.** Pattern: `{acronym}-{domain}-mcp-server` — e.g., `eia-energy-mcp-server`, `bls-labor-mcp-server`, `nhtsa-vehicle-safety-mcp-server`. Skip when the name is already self-descriptive (`earthquake-mcp-server`, `wikidata-mcp-server`).
|
|
38
|
+
- **The name becomes the tool prefix.** Every tool is `{prefix}_{verb}_{noun}`, so the server name shows up in every tool call an agent sees. A descriptive name gives agents domain context without reading the server's instructions.
|
|
39
|
+
|
|
32
40
|
## Steps
|
|
33
41
|
|
|
34
42
|
### 1. Research External Dependencies
|
|
@@ -53,6 +61,8 @@ When research is genuinely parallelizable (multiple independent APIs, several SD
|
|
|
53
61
|
- **Pagination behavior** — verify token format, page size limits, and what happens when results exceed one page.
|
|
54
62
|
- **Error shapes** — trigger real 400/404/429 responses to see the actual error format, not just what docs claim.
|
|
55
63
|
|
|
64
|
+
**Stopping condition:** at minimum, probe one list/search endpoint, one single-item GET, and one error case (force a 404 or 400). For large APIs with many resource types, add one probe per major noun. Stop when the response shapes and error envelope are confirmed.
|
|
65
|
+
|
|
56
66
|
This step prevents building a service layer against assumed response shapes that don't match reality.
|
|
57
67
|
|
|
58
68
|
### 2. Map User Goals, Then Domain Operations
|
|
@@ -74,7 +84,7 @@ Then enumerate the underlying **domain operations** the system supports, grouped
|
|
|
74
84
|
| Task | list (by project), get, create, update status, assign, comment |
|
|
75
85
|
| User | list, get current |
|
|
76
86
|
|
|
77
|
-
The user-goal list shapes the tool surface; the operation list fills in the gaps. Not every operation becomes a tool.
|
|
87
|
+
The user-goal list shapes the tool surface; the operation list fills in the gaps. Not every operation becomes a tool — an operation stays as raw material (not its own tool) when it's already fully covered by an existing tool's output, or when the only agents who'd use it are in scenarios outside this server's stated purpose.
|
|
78
88
|
|
|
79
89
|
### 3. Classify into MCP Primitives
|
|
80
90
|
|
|
@@ -482,12 +492,12 @@ What this server does, what system it wraps, who it's for.
|
|
|
482
492
|
|
|
483
493
|
Each step is independently testable.
|
|
484
494
|
|
|
485
|
-
<!-- Optional sections
|
|
486
|
-
## Domain Mapping <!-- nouns × operations → API endpoints -->
|
|
487
|
-
## Workflow Analysis <!-- how tools chain for real tasks -->
|
|
488
|
-
## Design Decisions <!-- rationale for consolidation, naming, tradeoffs -->
|
|
489
|
-
## Known Limitations <!-- inherent API/data constraints the server can't solve -->
|
|
490
|
-
## API Reference <!-- query language, pagination, rate limits -->
|
|
495
|
+
<!-- Optional sections — include when the trigger fires: -->
|
|
496
|
+
## Domain Mapping <!-- nouns × operations → API endpoints; include when ≥3 nouns each with ≥3 operations -->
|
|
497
|
+
## Workflow Analysis <!-- how tools chain for real tasks; include when any tool makes ≥3 upstream calls -->
|
|
498
|
+
## Design Decisions <!-- rationale for consolidation, naming, tradeoffs; include when a choice would otherwise be opaque -->
|
|
499
|
+
## Known Limitations <!-- inherent API/data constraints the server can't solve; include when a constraint visibly caps utility -->
|
|
500
|
+
## API Reference <!-- query language, pagination, rate limits; include when worth documenting -->
|
|
491
501
|
```
|
|
492
502
|
|
|
493
503
|
Keep it concise. The design doc is a working reference, not a spec document — enough to orient a developer (or agent) implementing the server, not more.
|
|
@@ -512,7 +522,7 @@ The table surfaces design questions early: should the elicit happen before or af
|
|
|
512
522
|
|
|
513
523
|
### 9. Confirm and Proceed
|
|
514
524
|
|
|
515
|
-
If the user has already authorized implementation (e.g., "build me a ___ server"
|
|
525
|
+
If the user has already authorized implementation — any message that contains both a design request and a build/implement verb in the same clause (e.g., "build me a ___ server", "design and implement a ___") — proceed directly to scaffolding using the design doc as the plan. Otherwise, present the design doc to the user for review before implementing.
|
|
516
526
|
|
|
517
527
|
## After Design
|
|
518
528
|
|
|
@@ -530,6 +540,7 @@ Execute the plan using the scaffolding skills:
|
|
|
530
540
|
Items without an `If …:` prefix apply to every design. Conditional items only apply when the trigger fires — otherwise skip them.
|
|
531
541
|
|
|
532
542
|
- [ ] External APIs/dependencies researched and verified (docs fetched, SDKs identified)
|
|
543
|
+
- [ ] **If wrapping an external API:** live API probed (at minimum: one list/search, one single-item GET, one error case)
|
|
533
544
|
- [ ] User goals enumerated first (3–10 outcomes agents will accomplish, scaled to domain size), then domain operations mapped as raw material
|
|
534
545
|
- [ ] Each operation classified as tool, resource, prompt, or excluded
|
|
535
546
|
- [ ] Catastrophically irreversible operations excluded from the tool surface (stay in vendor UI) — not just `destructiveHint`
|
|
@@ -553,6 +564,7 @@ Items without an `If …:` prefix apply to every design. Conditional items only
|
|
|
553
564
|
- [ ] **If a parameter determines blast radius:** safe default set (e.g., `mode: 'preview'`, `dryRun: true`, `confirmCount` required)
|
|
554
565
|
- [ ] **App tools default to no.** If one was proposed, verified there's a real human-in-the-loop in an MCP Apps-capable client justifying the iframe/CSP/`format()`-twin maintenance cost — otherwise dropped in favor of a standard tool
|
|
555
566
|
- [ ] **If the server exposes resources:** URIs use `{param}` templates, pagination planned for large lists
|
|
567
|
+
- [ ] **If the server is itself the source of truth (no external API):** state lifecycle planned — tenant-scoped vs. global, TTLs, what survives restart, storage backend chosen
|
|
556
568
|
- [ ] **If the server has external deps or shared state:** service layer planned (or explicitly skipped with reasoning)
|
|
557
569
|
- [ ] **If services wrap external APIs:** resilience planned (retry boundary, backoff, parse classification)
|
|
558
570
|
- [ ] **If exposing a SQL/analytical workspace over tabular data is in scope:** DataCanvas considered (`api-canvas` skill) as one option before designing custom analytical state — register / query / export tools accepting an optional `canvas_id`, with `ctx.core.canvas?` reads
|