@cyanheads/mcp-ts-core 0.13.5 → 0.13.7
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/AGENTS.md +6 -6
- package/CLAUDE.md +6 -6
- package/README.md +55 -52
- package/biome.json +2 -2
- package/changelog/0.13.x/0.13.6.md +49 -0
- package/changelog/0.13.x/0.13.7.md +77 -0
- package/config/tsconfig.base.json +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +42 -11
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +21 -4
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +9 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +4 -13
- package/dist/core/context.js.map +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +7 -1
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts +5 -4
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +99 -22
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +46 -10
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +180 -27
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/format-parity-rules.js +1 -1
- package/dist/linter/rules/format-parity-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +2 -1
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +2 -1
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +6 -0
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +3 -0
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +6 -3
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +5 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +15 -5
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/storage/core/IStorageProvider.d.ts +5 -2
- package/dist/storage/core/IStorageProvider.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.d.ts +29 -8
- package/dist/storage/core/providerHelpers.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.js +49 -11
- package/dist/storage/core/providerHelpers.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +4 -4
- package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts +2 -0
- package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +11 -9
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +8 -5
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -0
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +10 -8
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +5 -0
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +9 -5
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.js +5 -1
- package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
- package/dist/testing/index.d.ts +6 -4
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -0
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/utils/formatting/partialResult.d.ts +28 -2
- package/dist/utils/formatting/partialResult.d.ts.map +1 -1
- package/dist/utils/formatting/partialResult.js +46 -2
- package/dist/utils/formatting/partialResult.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +8 -2
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +27 -16
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/mappings.d.ts +1 -0
- package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/mappings.js +1 -0
- package/dist/utils/internal/error-handler/mappings.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +5 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +13 -8
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +15 -15
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +108 -88
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts +6 -2
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +23 -8
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +12 -18
- package/framework-skills/add-prompt/SKILL.md +3 -1
- package/framework-skills/add-provider/SKILL.md +14 -4
- package/framework-skills/add-resource/SKILL.md +3 -3
- package/framework-skills/add-service/SKILL.md +5 -2
- package/framework-skills/add-tool/SKILL.md +25 -8
- package/framework-skills/api-canvas/SKILL.md +2 -2
- package/framework-skills/api-config/SKILL.md +4 -3
- package/framework-skills/api-context/SKILL.md +8 -5
- package/framework-skills/api-errors/SKILL.md +25 -5
- package/framework-skills/api-linter/SKILL.md +95 -22
- package/framework-skills/api-telemetry/SKILL.md +9 -4
- package/framework-skills/api-testing/SKILL.md +21 -13
- package/framework-skills/api-utils/SKILL.md +3 -3
- package/framework-skills/api-utils/references/security.md +7 -6
- package/framework-skills/code-simplifier/SKILL.md +31 -18
- package/framework-skills/design-mcp-server/SKILL.md +62 -35
- package/framework-skills/git-wrapup/SKILL.md +16 -10
- package/framework-skills/maintenance/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +15 -8
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/references/package-meta.md +1 -1
- package/framework-skills/polish-docs-meta/references/readme.md +3 -3
- package/framework-skills/release-and-publish/SKILL.md +6 -4
- package/framework-skills/release-pr-review/SKILL.md +18 -1
- package/framework-skills/report-issue-framework/SKILL.md +2 -2
- package/framework-skills/report-issue-local/SKILL.md +3 -3
- package/framework-skills/security-pass/SKILL.md +11 -3
- package/framework-skills/tool-defs-analysis/SKILL.md +3 -3
- package/package.json +16 -37
- package/scripts/lint-mcp.ts +43 -4
- package/templates/.env.example +3 -1
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -1
- package/templates/AGENTS.md +3 -3
- package/templates/CLAUDE.md +3 -3
- package/templates/Dockerfile +4 -4
- package/templates/devcheck.config.json +1 -0
- package/templates/package.json +4 -4
- package/templates/src/mcp-server/prompts/definitions/echo.prompt.ts +2 -4
- package/templates/src/mcp-server/resources/definitions/echo-app-ui.app-resource.ts +51 -14
- package/templates/src/mcp-server/resources/definitions/echo.resource.ts +1 -1
- package/templates/src/mcp-server/tools/definitions/echo-app.app-tool.ts +2 -3
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +8 -2
- package/dist/utils/telemetry/index.d.ts +0 -12
- package/dist/utils/telemetry/index.d.ts.map +0 -1
- package/dist/utils/telemetry/index.js +0 -12
- package/dist/utils/telemetry/index.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.7
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -247,7 +247,7 @@ export const myTool = tool('my_tool', {
|
|
|
247
247
|
**`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
|
|
248
248
|
|
|
249
249
|
- **Parity is lint-enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or the `format-parity` rule fails `bun run lint:mcp` / `devcheck`.
|
|
250
|
-
- **Primary fix:** render the missing field in `format()`.
|
|
250
|
+
- **Primary fix:** render the missing field in `format()`. For list/detail variants, declare one flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks — `tool()` rejects a `z.discriminatedUnion` output root.
|
|
251
251
|
- **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
|
|
252
252
|
- **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
|
|
253
253
|
|
|
@@ -323,7 +323,7 @@ Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `
|
|
|
323
323
|
|
|
324
324
|
### `ctx.state`
|
|
325
325
|
|
|
326
|
-
Tenant-scoped KV. Accepts any serializable value — no manual `JSON.stringify`/`JSON.parse` needed.
|
|
326
|
+
Tenant-scoped KV. Accepts any JSON-serializable value — no manual `JSON.stringify`/`JSON.parse` needed — and reads return its JSON form (a `Date` comes back as its ISO string, a `Map` as `{}`), never the object that was written. A `bigint`, a cyclic reference, or a top-level `undefined`, function, or symbol rejects with `McpError(SerializationError)` before anything is written.
|
|
327
327
|
|
|
328
328
|
```ts
|
|
329
329
|
await ctx.state.set('item/123', { name: 'Widget', count: 42 });
|
|
@@ -378,7 +378,7 @@ See `api-context` skill for full details.
|
|
|
378
378
|
|
|
379
379
|
## Error Handling
|
|
380
380
|
|
|
381
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched.
|
|
381
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
382
382
|
|
|
383
383
|
```ts
|
|
384
384
|
errors: [
|
|
@@ -420,7 +420,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
420
420
|
|
|
421
421
|
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
422
422
|
|
|
423
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names). See `api-linter` skill.
|
|
423
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`), `error-contract-recovery-unforwarded` (a `ctx.fail` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface). See `api-linter` skill.
|
|
424
424
|
|
|
425
425
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
426
426
|
|
|
@@ -483,7 +483,7 @@ describe('myTool', () => {
|
|
|
483
483
|
|
|
484
484
|
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
485
485
|
|
|
486
|
-
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected) and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
486
|
+
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected), the JSON round-trip of every value, and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
487
487
|
|
|
488
488
|
**HTTP/session fixtures:** `createFetchMock(routes)` provides a strict fetch-compatible fake with ordered routes, captured `Request` objects, one-shot responses, and optional global install/restore. `createMockSession(options)` returns `{ sessionId, tenantId, ctx }` for handlers that branch on durable HTTP session identity.
|
|
489
489
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.7
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -247,7 +247,7 @@ export const myTool = tool('my_tool', {
|
|
|
247
247
|
**`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
|
|
248
248
|
|
|
249
249
|
- **Parity is lint-enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or the `format-parity` rule fails `bun run lint:mcp` / `devcheck`.
|
|
250
|
-
- **Primary fix:** render the missing field in `format()`.
|
|
250
|
+
- **Primary fix:** render the missing field in `format()`. For list/detail variants, declare one flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks — `tool()` rejects a `z.discriminatedUnion` output root.
|
|
251
251
|
- **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
|
|
252
252
|
- **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
|
|
253
253
|
|
|
@@ -323,7 +323,7 @@ Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `
|
|
|
323
323
|
|
|
324
324
|
### `ctx.state`
|
|
325
325
|
|
|
326
|
-
Tenant-scoped KV. Accepts any serializable value — no manual `JSON.stringify`/`JSON.parse` needed.
|
|
326
|
+
Tenant-scoped KV. Accepts any JSON-serializable value — no manual `JSON.stringify`/`JSON.parse` needed — and reads return its JSON form (a `Date` comes back as its ISO string, a `Map` as `{}`), never the object that was written. A `bigint`, a cyclic reference, or a top-level `undefined`, function, or symbol rejects with `McpError(SerializationError)` before anything is written.
|
|
327
327
|
|
|
328
328
|
```ts
|
|
329
329
|
await ctx.state.set('item/123', { name: 'Widget', count: 42 });
|
|
@@ -378,7 +378,7 @@ See `api-context` skill for full details.
|
|
|
378
378
|
|
|
379
379
|
## Error Handling
|
|
380
380
|
|
|
381
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched.
|
|
381
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
382
382
|
|
|
383
383
|
```ts
|
|
384
384
|
errors: [
|
|
@@ -420,7 +420,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
420
420
|
|
|
421
421
|
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
422
422
|
|
|
423
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names). See `api-linter` skill.
|
|
423
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`), `error-contract-recovery-unforwarded` (a `ctx.fail` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface). See `api-linter` skill.
|
|
424
424
|
|
|
425
425
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
426
426
|
|
|
@@ -483,7 +483,7 @@ describe('myTool', () => {
|
|
|
483
483
|
|
|
484
484
|
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
485
485
|
|
|
486
|
-
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected) and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
486
|
+
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected), the JSON round-trip of every value, and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
487
487
|
|
|
488
488
|
**HTTP/session fixtures:** `createFetchMock(routes)` provides a strict fetch-compatible fake with ordered routes, captured `Request` objects, one-shot responses, and optional global install/restore. `createMockSession(options)` returns `{ sessionId, tenantId, ctx }` for handlers that branch on durable HTTP session identity.
|
|
489
489
|
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/mcp-ts-core</h1>
|
|
3
|
-
<p><b>Agent-native TypeScript framework for building MCP servers
|
|
4
|
-
<p>
|
|
3
|
+
<p><b>Agent-native TypeScript framework for building MCP servers.</b></p>
|
|
4
|
+
<p>Runtime infrastructure for your server, and the agent skills to build, test, and ship it.</p>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
|
|
9
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
10
10
|
|
|
11
11
|
[](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
12
12
|
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## Build AI tools for anything you can describe
|
|
19
|
+
## Build AI tools for anything you can describe
|
|
20
20
|
|
|
21
|
-
Connect an API, a dataset, or a workflow to an AI agent through the Model Context Protocol (MCP). Your project holds the domain code; `@cyanheads/mcp-ts-core`
|
|
21
|
+
Connect an API, a dataset, or a workflow to an AI agent through the Model Context Protocol (MCP). Your project holds the domain code; `@cyanheads/mcp-ts-core` handles the auth, storage, logging, and transports underneath it.
|
|
22
22
|
|
|
23
|
-
**Agent-native
|
|
23
|
+
**Agent-native.** Every scaffold ships the framework reference and a set of Agent Skills: workflows for designing tools, writing tests, reviewing security, and cutting releases. You decide what the server does; your agent follows the skills to build it.
|
|
24
24
|
|
|
25
|
-
**The framework stays a dependency.** Infrastructure fixes arrive
|
|
25
|
+
**The framework stays a dependency.** Infrastructure fixes arrive as package upgrades. Run the `maintenance` skill and your agent bumps core, syncs the latest skills, and adopts what changed.
|
|
26
26
|
|
|
27
27
|
## Quick start
|
|
28
28
|
|
|
29
|
-
Servers
|
|
29
|
+
Servers run on Bun, Node.js 24+, or Cloudflare Workers.
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
bunx @cyanheads/mcp-ts-core init my-mcp-server
|
|
@@ -34,17 +34,15 @@ cd my-mcp-server
|
|
|
34
34
|
bun install
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Open
|
|
37
|
+
The scaffold includes a source tree, build and test configuration, `CLAUDE.md`/`AGENTS.md`, Agent Skills, and plugin metadata for Claude Code and Codex. Open it in Claude Code, Codex, or another agent and describe what you want:
|
|
38
38
|
|
|
39
39
|
> Build an MCP server for my team's inventory API. We need to find products, check stock across warehouses, investigate stock movements, and record adjustments and transfers. Let's get started.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Already have a TypeScript project? Install the framework directly with `bun add @cyanheads/mcp-ts-core` and register your definitions with `createApp()`.
|
|
41
|
+
Already have a TypeScript project? Run `bun add @cyanheads/mcp-ts-core` and register your definitions with `createApp()`.
|
|
44
42
|
|
|
45
43
|
## A tool is a schema and a function
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
This is a complete server that searches a three-item catalog. To try it, replace the scaffold's `src/index.ts` with it:
|
|
48
46
|
|
|
49
47
|
```ts
|
|
50
48
|
import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -79,7 +77,7 @@ bun run rebuild
|
|
|
79
77
|
bun run start:http
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
Point your MCP client at `http://127.0.0.1:3010/mcp` (Streamable HTTP), or have the client launch it over stdio with `bun /absolute/path/to/dist/index.js`.
|
|
83
81
|
|
|
84
82
|
## What comes with it
|
|
85
83
|
|
|
@@ -91,13 +89,13 @@ Connect your MCP client to `http://127.0.0.1:3010/mcp` (Streamable HTTP), or con
|
|
|
91
89
|
| Run locally or host a service | stdio and HTTP on Bun/Node.js; a separate entry point for Cloudflare Workers |
|
|
92
90
|
| Understand failures and catch mistakes | Structured logs, optional OpenTelemetry, definition linting, contract tests, and fuzz testing |
|
|
93
91
|
|
|
94
|
-
Optional integrations
|
|
92
|
+
Optional integrations (DuckDB, Supabase, the OpenTelemetry SDK) are peer dependencies; install them when you need them.
|
|
95
93
|
|
|
96
94
|
## Give agents useful results
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
Two declared contracts shape what an agent gets back. `enrichment` carries success-path context (totals, the parsed query, empty-result notices), populated with `ctx.enrich()`. `errors` lists each expected failure with its recovery guidance, and the handler throws one with the typed `ctx.fail()`.
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
`runSearch(query, limit)` stands in for your search backend. It returns `{ items, total, parsed }`, or `null` when the index is down:
|
|
101
99
|
|
|
102
100
|
```ts
|
|
103
101
|
import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -143,13 +141,13 @@ const search = tool('search', {
|
|
|
143
141
|
await createApp({ tools: [search] });
|
|
144
142
|
```
|
|
145
143
|
|
|
146
|
-
|
|
144
|
+
Both contracts are advertised in `tools/list`, so clients see them before calling, and the definition linter checks the handler against them. `ctx.recoveryFor()` adds the declared recovery hint to the error response.
|
|
147
145
|
|
|
148
146
|
### Same data across client surfaces
|
|
149
147
|
|
|
150
|
-
MCP hosts differ in
|
|
148
|
+
MCP hosts differ in which part of a tool result they hand the agent: `structuredContent` (JSON), `content[]` (text), or both. The framework fills both with the same data, so the agent sees the same result on any host.
|
|
151
149
|
|
|
152
|
-
`format()`
|
|
150
|
+
`format()` renders the text side; without one, `content[]` gets JSON. The format-parity lint rule fails `lint:mcp` if any output field is missing from the rendered text. Enrichment needs no `format()` entry, because the framework adds it to both surfaces. This formatter renders the items as a markdown list:
|
|
153
151
|
|
|
154
152
|
```ts
|
|
155
153
|
format: (result) => [{
|
|
@@ -162,7 +160,7 @@ format: (result) => [{
|
|
|
162
160
|
|
|
163
161
|
### Resources
|
|
164
162
|
|
|
165
|
-
Resources expose data at a URI. This
|
|
163
|
+
Resources expose data at a URI. This one reads from your own `getItem()` service:
|
|
166
164
|
|
|
167
165
|
```ts
|
|
168
166
|
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -183,7 +181,7 @@ Everything registers through `createApp()` in your entry point:
|
|
|
183
181
|
```ts
|
|
184
182
|
await createApp({
|
|
185
183
|
name: 'my-mcp-server',
|
|
186
|
-
|
|
184
|
+
title: 'my-mcp-server', // display name in client UIs
|
|
187
185
|
tools: allToolDefinitions,
|
|
188
186
|
resources: allResourceDefinitions,
|
|
189
187
|
prompts: allPromptDefinitions,
|
|
@@ -191,16 +189,17 @@ await createApp({
|
|
|
191
189
|
});
|
|
192
190
|
```
|
|
193
191
|
|
|
194
|
-
|
|
192
|
+
On Cloudflare Workers, `createWorkerHandler()` takes the same definitions from a separate entry point.
|
|
195
193
|
|
|
196
194
|
## Runtime and integration details
|
|
197
195
|
|
|
198
|
-
- **Auth and storage:** Declare `auth: ['scope']` on a definition
|
|
199
|
-
- **Client interaction:** Return `ctx.requestInput(...)` to
|
|
200
|
-
- **Protocol compatibility:** HTTP
|
|
201
|
-
- **Server presentation:** `instructions`
|
|
202
|
-
- **Definition checks:** `lint:mcp` checks names, schemas, scopes, annotations, format parity, and JSON Schema portability at build time
|
|
203
|
-
- **DataCanvas:** An optional DuckDB workspace
|
|
196
|
+
- **Auth and storage:** Declare `auth: ['scope']` on a definition and the scope is checked, under JWT or OAuth, before the handler runs. `ctx.state` is tenant-scoped storage over in-memory, filesystem, Supabase, or Cloudflare D1/KV/R2, chosen by config.
|
|
197
|
+
- **Client interaction:** Return `ctx.requestInput(...)` to ask the user for input, the client's model for a sample, or the client for its roots. The handler runs again with the answers on `ctx.inputs`.
|
|
198
|
+
- **Protocol compatibility:** HTTP serves 2026-07-28 clients (per-request `_meta` envelope) and session-based 2025-era clients. The SDK's compatibility layer handles input requests for the older ones.
|
|
199
|
+
- **Server presentation:** `instructions` gives the model server-wide guidance once, at `initialize`, instead of in every tool description. Identity fields (`title`, `websiteUrl`, `description`, `icons`) populate the client's server info, the `/.well-known/mcp.json` server card, and the HTTP landing page.
|
|
200
|
+
- **Definition checks:** `lint:mcp` checks names, schemas, scopes, annotations, format parity, and JSON Schema portability. It runs at build time, never at startup, so a new rule can't break a deployed server.
|
|
201
|
+
- **DataCanvas:** An optional DuckDB workspace where agents run SQL across staged API results and export CSV, Parquet, or JSON. Agents share a workspace by passing its canvas token. Enable it with `CANVAS_PROVIDER_TYPE=duckdb` and `@duckdb/node-api` (Bun or Node.js only). [brapi-mcp-server](https://github.com/cyanheads/brapi-mcp-server#working-with-dataframes) walks through loading API results into a dataframe and querying it.
|
|
202
|
+
- **Mirror:** The `/mirror` module keeps a persistent local copy of a bulk upstream dataset in embedded SQLite with an optional FTS5 index, so tools query it locally instead of paging the live API on every call. You write the `sync` ingester and the schema; the framework handles storage, resumable initial loads, and incremental refreshes. Bun or Node.js only (`better-sqlite3` is an optional peer on Node). [faa-aircraft-registry-mcp-server](https://github.com/cyanheads/faa-aircraft-registry-mcp-server) serves the full FAA registry this way.
|
|
204
203
|
|
|
205
204
|
See the [framework reference](CLAUDE.md) for configuration and handler patterns, and the [observability guide](docs/telemetry/observability.md) for Pino logging and OpenTelemetry traces and metrics.
|
|
206
205
|
|
|
@@ -221,14 +220,14 @@ my-mcp-server/
|
|
|
221
220
|
prompts/definitions/ # Prompt definitions (.prompt.ts)
|
|
222
221
|
package.json
|
|
223
222
|
tsconfig.json # extends @cyanheads/mcp-ts-core/tsconfig.base.json
|
|
224
|
-
CLAUDE.md / AGENTS.md #
|
|
223
|
+
CLAUDE.md / AGENTS.md # Server conventions; points to core's framework reference
|
|
225
224
|
```
|
|
226
225
|
|
|
227
|
-
Framework infrastructure lives in `node_modules`; your source tree
|
|
226
|
+
Framework infrastructure lives in `node_modules`; your source tree holds the server's definitions, configuration, and domain services.
|
|
228
227
|
|
|
229
228
|
## Configuration
|
|
230
229
|
|
|
231
|
-
|
|
230
|
+
Core config comes from environment variables, validated with Zod. Server-specific variables get their own schema, parsed lazily so Workers can inject env at request time.
|
|
232
231
|
|
|
233
232
|
| Variable | Description | Default |
|
|
234
233
|
|:---------|:------------|:--------|
|
|
@@ -240,7 +239,7 @@ All core config is Zod-validated from environment variables. Server-specific con
|
|
|
240
239
|
| `STORAGE_PROVIDER_TYPE` | `in-memory`, `filesystem`, `supabase`, `cloudflare-d1`/`kv`/`r2` | `in-memory` |
|
|
241
240
|
| `CANVAS_PROVIDER_TYPE` | `none` or `duckdb` (optional peer dependency `@duckdb/node-api`) | `none` |
|
|
242
241
|
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
|
|
243
|
-
| `OPENROUTER_API_KEY` | OpenRouter LLM
|
|
242
|
+
| `OPENROUTER_API_KEY` | API key for the optional OpenRouter LLM provider (`/services`) | — |
|
|
244
243
|
|
|
245
244
|
See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the full configuration reference.
|
|
246
245
|
|
|
@@ -250,7 +249,7 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the full configuration reference.
|
|
|
250
249
|
|
|
251
250
|
| Function | Purpose |
|
|
252
251
|
|:---------|:--------|
|
|
253
|
-
| `createApp(options)` | Bun or Node.js server
|
|
252
|
+
| `createApp(options)` | Bun or Node.js server; manages startup and shutdown |
|
|
254
253
|
| `createWorkerHandler(options)` | Cloudflare Workers — returns an `ExportedHandler` |
|
|
255
254
|
|
|
256
255
|
### Builders
|
|
@@ -265,7 +264,7 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the full configuration reference.
|
|
|
265
264
|
|
|
266
265
|
### Context
|
|
267
266
|
|
|
268
|
-
|
|
267
|
+
Tool and resource handlers receive a `Context`. `ctx.enrich` and `ctx.fail` are typed against the definition's declared contracts:
|
|
269
268
|
|
|
270
269
|
| Property | Type | Description |
|
|
271
270
|
|:---------|:-----|:------------|
|
|
@@ -276,7 +275,7 @@ Handlers receive a shared `Context`, with typed helpers for declared enrichment
|
|
|
276
275
|
| `ctx.enrich` | `Enrich` / `TypedEnrich<E>` | Add declared result context to structured output and text content |
|
|
277
276
|
| `ctx.content` | `ContentCollect` | Attach image/audio blocks to `content[]` — `content.image(data, mimeType)`, `content.audio(...)`, or a raw block |
|
|
278
277
|
| `ctx.fail` | `(reason, msg?, data?) => McpError` | Creates an error for `throw ctx.fail(...)`; available with a declared `errors` contract |
|
|
279
|
-
| `ctx.recoveryFor` | `(reason) => object` | Resolves a declared recovery hint to `{ recovery: { hint } }
|
|
278
|
+
| `ctx.recoveryFor` | `(reason) => object` | Resolves a declared recovery hint to `{ recovery: { hint } }`, for `ctx.fail`'s data argument |
|
|
280
279
|
| `ctx.signal` | `AbortSignal` | Cancellation signal |
|
|
281
280
|
| `ctx.notifyResourceUpdated` | `Function?` | Notify subscribed clients a resource changed |
|
|
282
281
|
| `ctx.notifyResourceListChanged` | `Function?` | Notify clients the resource list changed |
|
|
@@ -298,6 +297,7 @@ import { checkScopes } from '@cyanheads/mcp-ts-core/auth';
|
|
|
298
297
|
import { markdown, fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
|
|
299
298
|
import { OpenRouterProvider, GraphService } from '@cyanheads/mcp-ts-core/services';
|
|
300
299
|
import type { DataCanvas, CanvasInstance } from '@cyanheads/mcp-ts-core/canvas';
|
|
300
|
+
import { defineMirror, sqliteMirrorStore } from '@cyanheads/mcp-ts-core/mirror';
|
|
301
301
|
import { validateDefinitions } from '@cyanheads/mcp-ts-core/linter';
|
|
302
302
|
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
|
|
303
303
|
import { mcpTest, toolContractSuite } from '@cyanheads/mcp-ts-core/testing/vitest';
|
|
@@ -308,21 +308,23 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the complete exports reference.
|
|
|
308
308
|
|
|
309
309
|
## Examples
|
|
310
310
|
|
|
311
|
-
|
|
311
|
+
`examples/` holds a reference server built only on the public exports. `examples/index.ts` (Node/Bun) and `examples/worker.ts` (Cloudflare Workers) register the same `definitions/index.ts` barrels.
|
|
312
312
|
|
|
313
|
-
|
|
|
314
|
-
|
|
315
|
-
| `template_echo_message` |
|
|
316
|
-
| `template_cat_fact` |
|
|
317
|
-
|
|
|
318
|
-
| `
|
|
319
|
-
| `template_data_explorer` |
|
|
313
|
+
| Kind | Name | Pattern |
|
|
314
|
+
|:-----|:-----|:--------|
|
|
315
|
+
| Tool | `template_echo_message` | `errors[]` contract with `ctx.fail`, `inputAliases`, full-fidelity `format()` |
|
|
316
|
+
| Tool | `template_cat_fact` | `fetchWithTimeout`, a typed not-found contract, enrichment echo |
|
|
317
|
+
| Tool | `template_image_test` | `ctx.content.image` |
|
|
318
|
+
| Tool | `template_madlibs_elicitation` | `return ctx.requestInput`, a declined-input contract with `severity` |
|
|
319
|
+
| Tool | `template_data_explorer` | `appTool`/`appResource`, host theming, `cacheHint` |
|
|
320
|
+
| Resource | `echo://{message}` | Templated resource |
|
|
321
|
+
| Resource | `ui://template-data-explorer/app.html` | UI resource paired with `template_data_explorer` |
|
|
322
|
+
| Prompt | `code_review` | `completable()` argument, `code` argument |
|
|
320
323
|
|
|
321
324
|
## Testing
|
|
322
325
|
|
|
323
326
|
```ts
|
|
324
327
|
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
|
|
325
|
-
import { mcpTest, toolContractSuite } from '@cyanheads/mcp-ts-core/testing/vitest';
|
|
326
328
|
import { myTool } from '@/mcp-server/tools/definitions/my-tool.tool.js';
|
|
327
329
|
|
|
328
330
|
const ctx = createMockContext();
|
|
@@ -330,11 +332,11 @@ const input = myTool.input.parse({ query: 'test' });
|
|
|
330
332
|
const result = await myTool.handler(input, ctx);
|
|
331
333
|
```
|
|
332
334
|
|
|
333
|
-
`createMockContext()`
|
|
335
|
+
`createMockContext()` gives you a recording `log`, a `signal`, and a `state` backed by a real `StorageService` over an in-memory provider, so key validation, TTL expiry, and the JSON round-trip of stored values behave as they do in production: a `Date` reads back as its ISO string, and a value JSON cannot encode rejects. It uses tenant `'default'` unless you pass `{ tenantId }`. Pass `{ errors: myTool.errors }` for a typed `ctx.fail`, or `{ inputResponses, requestState }` to start a multi-round-trip handler at its second round.
|
|
334
336
|
|
|
335
|
-
`/testing` also exports `createMockSession()` for session-bound contexts, `createFetchMock()` for upstream HTTP
|
|
337
|
+
`/testing` also exports `createMockSession()` for session-bound contexts, `createFetchMock()` as a strict fake for upstream HTTP, and `runToolContract()`, which runs a definition through schema, handler, formatting, and error-envelope checks. `/testing/vitest` adds the `mcpTest` fixtures (`ctx`, `session`, `fetchMock`, `storage`) and `toolContractSuite()`.
|
|
336
338
|
|
|
337
|
-
|
|
339
|
+
`/testing/fuzz` uses `fast-check` to generate valid inputs from your Zod schemas plus adversarial payloads, then checks for crashes, stack-trace leaks, and prototype pollution:
|
|
338
340
|
|
|
339
341
|
```ts
|
|
340
342
|
import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';
|
|
@@ -345,13 +347,13 @@ expect(report.leaks).toHaveLength(0);
|
|
|
345
347
|
expect(report.prototypePollution).toBe(false);
|
|
346
348
|
```
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
It also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL_STRINGS` for custom property-based tests.
|
|
349
351
|
|
|
350
352
|
## Documentation
|
|
351
353
|
|
|
352
|
-
- **[CLAUDE.md/AGENTS.md](CLAUDE.md)
|
|
353
|
-
- **[docs/telemetry/](docs/telemetry/)
|
|
354
|
-
- **[CHANGELOG.md](CHANGELOG.md)
|
|
354
|
+
- **[CLAUDE.md/AGENTS.md](CLAUDE.md)**: the framework reference, covering exports, patterns, `Context`, error codes, auth, config, and testing. It ships in the npm package, so your agent reads it from `node_modules` after `init`.
|
|
355
|
+
- **[docs/telemetry/](docs/telemetry/)**: every span, metric, and attribute the framework emits ([observability.md](docs/telemetry/observability.md)), plus an example Grafana dashboard and query recipes for Datadog, New Relic, and Honeycomb ([dashboards.md](docs/telemetry/dashboards.md)).
|
|
356
|
+
- **[CHANGELOG.md](CHANGELOG.md)**: version history, indexing one file per release under `changelog/`. Each has a summary, migration notes, and links to commits and issues; releases that need downstream changes carry `agent-notes` for the `maintenance` skill to act on.
|
|
355
357
|
|
|
356
358
|
## Development
|
|
357
359
|
|
|
@@ -360,6 +362,7 @@ bun run rebuild # clean + build (scripts/clean.ts + scripts/build.ts)
|
|
|
360
362
|
bun run devcheck # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync, audit, outdated, secrets/TODO scan
|
|
361
363
|
bun run lint:mcp # validate MCP definitions against spec
|
|
362
364
|
bun run test:all # rebuild + coverage + Node.js + Workers + integration
|
|
365
|
+
bun run test:package # pack the tarball and consume it as an external project would
|
|
363
366
|
```
|
|
364
367
|
|
|
365
368
|
## License
|
package/biome.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.5.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
|
|
3
3
|
"vcs": {
|
|
4
4
|
"enabled": true,
|
|
5
5
|
"clientKind": "git",
|
|
6
6
|
"useIgnoreFile": true
|
|
7
7
|
},
|
|
8
8
|
"files": {
|
|
9
|
-
"includes": ["src/**", "scripts/**", "tests/**", "*.json", "*.js", "*.ts"]
|
|
9
|
+
"includes": ["src/**", "scripts/**", "tests/**", "examples/**", "*.json", "*.js", "*.ts"]
|
|
10
10
|
},
|
|
11
11
|
"formatter": {
|
|
12
12
|
"enabled": true,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "A ctx.fail site that never forwards its declared recovery now warns, an errors[] entry can mark itself thrownBy: 'service' so the unthrown rule skips it, capped-list-no-truncation stops firing on value bounds like max_depth_km, and devcheck.config.json can carry the truncation allowlist."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Four adoption steps for a consumer upgrading from 0.13.5.
|
|
7
|
+
|
|
8
|
+
1. `error-contract-recovery-unforwarded` is new and fires on the next
|
|
9
|
+
`devcheck` / `lint:mcp`: a literal `ctx.fail('<reason>', …)` site carrying
|
|
10
|
+
neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key. Clear it
|
|
11
|
+
by spreading `...ctx.recoveryFor('<reason>')` into the data argument, by
|
|
12
|
+
passing the resolver as that argument, or with an explicit
|
|
13
|
+
`recovery: { hint }` when the hint needs runtime context.
|
|
14
|
+
2. An `errors[]` reason the service layer produces takes `thrownBy: 'service'`,
|
|
15
|
+
which is what `error-contract-unthrown` reads to skip it. Prefer it over
|
|
16
|
+
moving the handler's literal `ctx.fail` out of scan range — that silences
|
|
17
|
+
the rule for the whole definition, handler-local reasons included.
|
|
18
|
+
3. Declare `lint.truncationAllowlist` in `devcheck.config.json` in place of
|
|
19
|
+
`MCP_LINT_TRUNCATION_ALLOWLIST` on each script entry, and drop the env var
|
|
20
|
+
from the `devcheck` and `lint:mcp` scripts once the key is in place. A
|
|
21
|
+
scaffolded config predating this release has no `lint` key; add it.
|
|
22
|
+
4. Drop any `truncationAllowlist` entry added only to silence
|
|
23
|
+
`capped-list-no-truncation` on a value-bound `max_*` input — those no
|
|
24
|
+
longer warn.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# 0.13.6 — 2026-09-19
|
|
28
|
+
|
|
29
|
+
## Added
|
|
30
|
+
|
|
31
|
+
- **Lint rule `error-contract-recovery-unforwarded`** (warning, [#255](https://github.com/cyanheads/mcp-ts-core/issues/255)) — one diagnostic per literal `ctx.fail('<reason>', …)` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface. A site whose only resolver names a different reason warns too, naming both.
|
|
32
|
+
- **`thrownBy: 'service'` on an `errors[]` entry** ([#462](https://github.com/cyanheads/mcp-ts-core/issues/462)) — lint-only metadata marking a reason produced below the handler, so `error-contract-unthrown` skips it while the handler's own reasons keep being checked. The entry stays typed, advertised, and thrown exactly as an unmarked one.
|
|
33
|
+
- **`lint.truncationAllowlist` in `devcheck.config.json`** ([#388](https://github.com/cyanheads/mcp-ts-core/issues/388)) — read by `scripts/lint-mcp.ts` and forwarded as `LintInput.truncationAllowlist`, so one project-owned declaration covers every entrypoint that shells out to the linter. Takes an array of tool names or `false`; precedence is explicit `LintInput`, then the config file, then `MCP_LINT_TRUNCATION_ALLOWLIST`.
|
|
34
|
+
|
|
35
|
+
## Changed
|
|
36
|
+
|
|
37
|
+
- **The scaffold's `echo` tool forwards its declared recovery** ([#255](https://github.com/cyanheads/mcp-ts-core/issues/255)) — `templates/src/mcp-server/tools/definitions/echo.tool.ts` spreads `ctx.recoveryFor('empty_message')`, so a new server starts from the shape the rule asks for.
|
|
38
|
+
- Skill versions: `add-service` 1.10 → 1.11, `add-tool` 2.28 → 2.29, `api-errors` 1.14 → 1.15, `api-linter` 1.16 → 1.17.
|
|
39
|
+
|
|
40
|
+
## Fixed
|
|
41
|
+
|
|
42
|
+
- **`capped-list-no-truncation`'s `max_` arm requires the counted noun to name a returned list** ([#345](https://github.com/cyanheads/mcp-ts-core/issues/345)) — a depth-0 array in `output` (plural-insensitive, a trailing `_count` stripped first) or a generic result container (`results`, `records`, `items`, `rows`, `hits`, `entries`, `matches`, `count`, `page`, `docs`). Value bounds (`max_depth_km`, `maxLat`, `max_date`) and work budgets (`max_court_lookups`, `maxCharacters`) stop warning; `limit`, `<noun>_limit`, and the page-size idioms are unchanged.
|
|
43
|
+
- **`error-contract-unthrown` skips a definition whose `ctx.recoveryFor(` takes a non-literal first argument** ([#462](https://github.com/cyanheads/mcp-ts-core/issues/462)), as it already did for `ctx.fail(` — unknown reasons are in play either way. Its message now names all three fixes.
|
|
44
|
+
- **The `format-parity` diagnostic no longer points authors at a `z.discriminatedUnion` output** ([#268](https://github.com/cyanheads/mcp-ts-core/issues/268)), which `tool()` rejects before any lint rule runs. The "Primary fix" line names a flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks.
|
|
45
|
+
- **The `sanitizeHtml` event-handler property matches only inside a tag** ([#463](https://github.com/cyanheads/mcp-ts-core/issues/463)) — the whole-string `/\bon\w+\s*=/` also matched inert text such as `on0=`, failing roughly once in 300 runs. `on0=` and `<p>onclick=1</p>` are pinned as `examples`; the sanitizer itself is unchanged.
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
- `@biomejs/biome` 2.5.13 → 2.5.14, `fast-check` ^4.10.0 → ^4.10.1.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Server stack traces no longer reach clients through McpError.data, in-memory storage round-trips values as JSON like every other provider, OTEL_EXPORTER_OTLP_ENDPOINT is honored, and the unused MCP client, ext-apps, and dotenv dependencies are gone."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: true
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Adoption steps for a consumer upgrading from 0.13.6.
|
|
7
|
+
|
|
8
|
+
1. The framework no longer installs `@modelcontextprotocol/client` (now a
|
|
9
|
+
devDependency), `@modelcontextprotocol/ext-apps`, or `dotenv`. A server
|
|
10
|
+
whose own code or tests import any of them without declaring it adds it —
|
|
11
|
+
tests that drive a `Client` need `bun add -d @modelcontextprotocol/client@^2.0.0`.
|
|
12
|
+
2. Raise the server's `zod` to `^4.6.5`, the new peer floor, so a single copy
|
|
13
|
+
installs alongside the framework.
|
|
14
|
+
3. Stored values now round-trip as JSON on every provider, `in-memory` and
|
|
15
|
+
`createMockContext().state` included. Fix code and tests that expect the
|
|
16
|
+
written object back by identity, or a `Date` / `Map` to survive: a `Date`
|
|
17
|
+
reads back as its ISO string, so validate reads with `z.string()`, not
|
|
18
|
+
`z.date()`. A value JSON cannot encode now throws
|
|
19
|
+
`McpError(SerializationError)` on write, and fails a whole `setMany`.
|
|
20
|
+
4. `McpError.data` from `ErrorHandler.handleError` / `tryCatch` no longer
|
|
21
|
+
carries `originalStack` or `causeChain`; anything reading them there reads
|
|
22
|
+
the log record instead (`rootCause` and `originalMessage` stay). A prompt
|
|
23
|
+
whose `generate()` throws a non-`McpError` now answers with code and
|
|
24
|
+
message only.
|
|
25
|
+
5. With `OTEL_ENABLED=true`, `OTEL_EXPORTER_OTLP_ENDPOINT` alone now exports
|
|
26
|
+
traces and metrics to `<base>/v1/traces` and `<base>/v1/metrics`. OTel log
|
|
27
|
+
records are never exported, `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER`
|
|
28
|
+
are not consulted, and a deployment with no endpoint set exports nothing
|
|
29
|
+
(it used to send metrics and logs to `localhost:4318`). Confirm a
|
|
30
|
+
deployment that sets only the base endpoint wants traces sent there.
|
|
31
|
+
6. The `filesystem` provider writes its envelope as compact single-line JSON.
|
|
32
|
+
Existing indented files still read; update anything that inspects the files
|
|
33
|
+
on disk and expects the old formatting.
|
|
34
|
+
7. `.env` loads through `process.loadEnvFile()` with the same no-override
|
|
35
|
+
rule, but a `.env` that exists and cannot be read now fails startup with a
|
|
36
|
+
`ConfigurationError`.
|
|
37
|
+
8. Drop `validator` / `@types/validator` if they were installed only for
|
|
38
|
+
`sanitizeUrl` / `sanitizeNumber`. `sanitizeUrl` now passes single-label
|
|
39
|
+
hosts such as `localhost` (loopback IP literals always passed); a server
|
|
40
|
+
that must refuse internal hosts checks the host itself.
|
|
41
|
+
9. Optional, to match the scaffold: `packageManager` `bun@1.4.2` and
|
|
42
|
+
`oven/bun:1.4.2` Dockerfile tags. The `maintenance` template review picks up
|
|
43
|
+
the `templates/CLAUDE.md` edits (the `ctx.state` line, a bounded `limit`).
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# 0.13.7 — 2026-09-25
|
|
47
|
+
|
|
48
|
+
## Changed
|
|
49
|
+
|
|
50
|
+
- **Stored values round-trip as JSON on every provider** ([#522](https://github.com/cyanheads/mcp-ts-core/issues/522)) — `in-memory`, and so `createMockContext().state`, holds JSON text and parses on each read: a `Date` reads back as its ISO string, a `Map` as `{}`, and a returned object never shares identity with the one written.
|
|
51
|
+
- **The `filesystem` provider writes compact JSON** — the stored envelope is one line; files written by earlier versions still read.
|
|
52
|
+
- **`sanitizeUrl` and `sanitizeNumber` validate without a peer dependency** — built-in checks replace `validator`; `sanitizeNumber` accepts the same plain decimals as before. `sanitizeUrl` now accepts single-label hosts (`localhost`), `_` in host labels, and a trailing-dot host, and refuses a host that parses differently from how it is written (`http://evil.com\@good.com`, which `validator` passed). It stays a format check, not an SSRF guard: loopback and private IP literals pass, as they did before.
|
|
53
|
+
- **Two-segment tool names are scoped to complete-action verbs** ([#248](https://github.com/cyanheads/mcp-ts-core/issues/248)) — `design-mcp-server` and `add-tool` keep `git_pull`-style names, while an object-taking verb (`search`, `get`, `connect`) always carries its noun.
|
|
54
|
+
- **The scaffold's echo app UI follows the host theme** — it reads the MCP Apps style variables over a light/dark baseline. New scaffolds only.
|
|
55
|
+
- **The example server registers through definition barrels** — `examples/index.ts` and `examples/worker.ts` share one `definitions/index.ts` per primitive plus server `instructions`, and `examples/` is now typechecked, linted, and smoke-tested.
|
|
56
|
+
- Skill versions: `add-app-tool` 1.5 → 1.6, `add-prompt` 1.3 → 1.4, `add-provider` 1.1 → 1.2, `add-resource` 1.6 → 1.7, `add-tool` 2.29 → 2.30, `api-canvas` 2.3 → 2.4, `api-config` 1.19 → 1.20, `api-context` 2.5 → 2.6, `api-errors` 1.15 → 1.16, `api-linter` 1.17 → 1.18, `api-telemetry` 1.12 → 1.13, `api-testing` 1.10 → 1.11, `api-utils` 2.11 → 2.12, `code-simplifier` 1.5 → 1.6, `design-mcp-server` 2.28 → 2.29, `git-wrapup` 1.19 → 1.25, `maintenance` 2.8 → 2.9, `orchestrations` 1.10 → 1.11, `polish-docs-meta` 2.17 → 2.18, `release-and-publish` 2.19 → 2.20, `release-pr-review` 1.4 → 1.5, `report-issue-framework` 1.12 → 1.13, `report-issue-local` 1.10 → 1.11, `security-pass` 1.8 → 1.10, `tool-defs-analysis` 1.6 → 1.7.
|
|
57
|
+
|
|
58
|
+
## Fixed
|
|
59
|
+
|
|
60
|
+
- **`OTEL_EXPORTER_OTLP_ENDPOINT` is honored** ([#523](https://github.com/cyanheads/mcp-ts-core/issues/523)) — it resolves `<base>/v1/traces` and `<base>/v1/metrics` when the signal-specific variable is unset. `NodeSDK` gets explicit metric readers and an empty log-processor list, so its env defaults no longer export outside the framework config.
|
|
61
|
+
- **Unencodable values are rejected on every provider** ([#539](https://github.com/cyanheads/mcp-ts-core/issues/539)) — a `bigint`, a cyclic reference, or a top-level `undefined`, function, or symbol throws `McpError(SerializationError)` before anything is written; one such value fails a whole `setMany`.
|
|
62
|
+
- **A client that disconnects mid-body is `RequestCancelled`** ([#507](https://github.com/cyanheads/mcp-ts-core/issues/507)) — `httpErrorHandler` resolves the error against the inbound request's signal, so the hang-up logs at info with no stack and answers 499 instead of a `-32004` 504.
|
|
63
|
+
- **The default tenant comes from parsed config** ([#520](https://github.com/cyanheads/mcp-ts-core/issues/520)) — a blank or `${…}` `MCP_AUTH_MODE` on HTTP resolves tenant `'default'`, matching the `none` that config reads it as, instead of leaving `ctx.state` without a tenant.
|
|
64
|
+
- **The shipped `tsconfig.base.json` anchors `outDir` and `@/*` to `${configDir}`** ([#521](https://github.com/cyanheads/mcp-ts-core/issues/521)) — a project extending it without restating them emits into its own `dist/`, never into the installed package; `test:package` checks this on TypeScript 7 and 6.
|
|
65
|
+
- **Partial-success telemetry reads `partialResultSchema()`'s own keys** ([#524](https://github.com/cyanheads/mcp-ts-core/issues/524)) — `mcp.tool.partial_success` and `mcp.tool.batch.*` follow a custom `failedKey` / `succeededKey`, including through `.extend()`, `.pick()`, `.omit()`, and a `.shape` spread.
|
|
66
|
+
- **`logger.close()` runs after a failed telemetry flush** ([#540](https://github.com/cyanheads/mcp-ts-core/issues/540)) — the failure is logged as a warning first, so the final log lines survive an unreachable collector.
|
|
67
|
+
|
|
68
|
+
## Security
|
|
69
|
+
|
|
70
|
+
- **Server stack traces no longer reach clients through `McpError.data`** ([#519](https://github.com/cyanheads/mcp-ts-core/issues/519)) — `ErrorHandler.handleError` and `tryCatch` keep `originalStack` and `causeChain` in the log record, and `prompts/get` forwards only the thrown `McpError`'s own `data`.
|
|
71
|
+
|
|
72
|
+
## Dependencies
|
|
73
|
+
|
|
74
|
+
- `@modelcontextprotocol/client` ^2.0.0 moves from `dependencies` to `devDependencies`; `@modelcontextprotocol/ext-apps` ^2.0.0 and `dotenv` ^17.4.2 are removed, `dotenv` in favor of `process.loadEnvFile()` ([#525](https://github.com/cyanheads/mcp-ts-core/issues/525)).
|
|
75
|
+
- Peer `zod` ^4.4.3 → ^4.6.5, matching the dependency range; the optional `validator` peer is removed.
|
|
76
|
+
- Dev: `@cloudflare/workers-types` 5.20260910.1 → 5.20260922.1, `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3, `@supabase/supabase-js` ^2.116.0 → ^2.117.0, `@types/node` 26.5.1 → 26.6.2, `defuddle` ^0.19.3 → ^0.19.4, `fast-check` ^4.10.1 → ^4.10.2, `ignore` ^7.0.9 → ^7.0.10, `openai` ^7.15.0 → ^7.21.0, `repomix` ^1.18.0 → ^1.18.1; `validator`, `@types/validator`, and `bun-types` removed. The `depcheck` script and `package.json` block give way to the list in `devcheck.config.json`.
|
|
77
|
+
- Bun 1.4.0 → 1.4.2 (`packageManager`, Docker base images, and the scaffold, whose `@types/node` and `@socketsecurity/bun-security-scanner` pins follow the framework's).
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
"esModuleInterop": true,
|
|
16
16
|
|
|
17
|
-
"outDir": "
|
|
17
|
+
"outDir": "${configDir}/dist",
|
|
18
18
|
"declaration": true,
|
|
19
19
|
"declarationMap": true,
|
|
20
20
|
"sourceMap": true,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"allowJs": false,
|
|
35
35
|
|
|
36
36
|
"paths": {
|
|
37
|
-
"@/*": ["
|
|
37
|
+
"@/*": ["${configDir}/src/*"]
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,wEAAwE;AACxE,eAAO,MAAM,cAAc,2BAA2B,CAAC;AACvD,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AA6DjE,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsWd,CAAC;AAGL,QAAA,MAAM,WAAW,kBAAmB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2MrE,CAAC;AAIF;;;;;;;;GAQG;AACH,QAAA,MAAM,WAAW,kBAAmB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAG,IAExE,CAAC;AAEF;;;;;;GAMG;AACH,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBV,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAErD;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC"}
|