@cyanheads/mcp-ts-core 0.13.11 → 0.13.13
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 +9 -8
- package/CLAUDE.md +9 -8
- package/README.md +1 -1
- package/changelog/0.13.x/0.13.12.md +50 -0
- package/changelog/0.13.x/0.13.13.md +93 -0
- package/dist/core/context.d.ts +12 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +59 -14
- package/dist/core/context.js.map +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +21 -8
- package/dist/core/worker.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +14 -8
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +16 -9
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +18 -9
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +29 -15
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +10 -7
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +29 -19
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +8 -2
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +18 -7
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +7 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +107 -21
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts +22 -0
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +29 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.js +2 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +4 -2
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/services/mirror/sqlite/handle.d.ts +13 -2
- package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/handle.js +17 -6
- package/dist/services/mirror/sqlite/handle.js.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js +3 -2
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +48 -21
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js +31 -16
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +29 -11
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +204 -95
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/helpers.d.ts +91 -9
- package/dist/utils/internal/error-handler/helpers.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/helpers.js +243 -39
- package/dist/utils/internal/error-handler/helpers.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +21 -4
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logValue.d.ts +33 -0
- package/dist/utils/internal/logValue.d.ts.map +1 -0
- package/dist/utils/internal/logValue.js +539 -0
- package/dist/utils/internal/logValue.js.map +1 -0
- package/dist/utils/internal/logger.d.ts +38 -9
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +228 -118
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/observabilityCap.d.ts +35 -0
- package/dist/utils/internal/observabilityCap.d.ts.map +1 -0
- package/dist/utils/internal/observabilityCap.js +43 -0
- package/dist/utils/internal/observabilityCap.js.map +1 -0
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +9 -11
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/requestContext.d.ts +3 -3
- package/dist/utils/internal/requestContext.js +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +20 -10
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +112 -30
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/httpError.d.ts +8 -6
- package/dist/utils/network/httpError.d.ts.map +1 -1
- package/dist/utils/network/httpError.js +23 -7
- package/dist/utils/network/httpError.js.map +1 -1
- package/dist/utils/network/retry.d.ts +25 -2
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +47 -5
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +24 -28
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +25 -84
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/security/sensitiveFields.d.ts +32 -4
- package/dist/utils/security/sensitiveFields.d.ts.map +1 -1
- package/dist/utils/security/sensitiveFields.js +85 -4
- package/dist/utils/security/sensitiveFields.js.map +1 -1
- package/dist/utils/telemetry/trace.d.ts +3 -1
- package/dist/utils/telemetry/trace.d.ts.map +1 -1
- package/dist/utils/telemetry/trace.js +6 -6
- package/dist/utils/telemetry/trace.js.map +1 -1
- package/framework-skills/api-auth/SKILL.md +3 -1
- package/framework-skills/api-canvas/SKILL.md +2 -2
- package/framework-skills/api-config/SKILL.md +2 -1
- package/framework-skills/api-context/SKILL.md +6 -6
- package/framework-skills/api-errors/SKILL.md +19 -17
- package/framework-skills/api-linter/SKILL.md +11 -10
- package/framework-skills/api-mirror/SKILL.md +3 -1
- package/framework-skills/api-telemetry/SKILL.md +14 -8
- package/framework-skills/api-utils/SKILL.md +6 -6
- package/framework-skills/api-utils/references/security.md +4 -2
- package/framework-skills/field-test/SKILL.md +2 -2
- package/framework-skills/git-wrapup/SKILL.md +3 -3
- package/framework-skills/tool-defs-analysis/SKILL.md +2 -2
- package/package.json +10 -9
- package/scripts/check-framework-antipatterns.ts +3 -2
- package/templates/.env.example +2 -0
- package/templates/tests/tools/echo.tool.test.ts +19 -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.13
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -320,7 +320,7 @@ interface Context {
|
|
|
320
320
|
|
|
321
321
|
### `ctx.log`
|
|
322
322
|
|
|
323
|
-
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
323
|
+
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]` and each `Error` in the data written as `{ type, message }` only (the process log keeps its stack, code, and cause). A data key the logger writes on the line itself (`level`, `time`, `msg`, `env`, `version`, `pid`, `hostname`, and `err` on `ctx.log.error` with an `Error`) goes to the process log as `data_level` and so on, so the record keeps its own level and the server's `version`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
324
324
|
|
|
325
325
|
### `ctx.state`
|
|
326
326
|
|
|
@@ -406,7 +406,7 @@ See `api-context` skill for full details.
|
|
|
406
406
|
|
|
407
407
|
## Error Handling
|
|
408
408
|
|
|
409
|
-
**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 wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. 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. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise. 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.
|
|
409
|
+
**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 wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. 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. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise, and so does a tool's missing-scope refusal (the inline `auth` check or `checkScopes`), which carries no reason to declare; a missing auth context, a handler's own `forbidden()`, and an upstream 403 keep `error`. All three refusal records log no stack, whatever level an entry declares, and an argument rejection's record keeps at most the first 1,024 characters of each string and the first 10 entries of each array, the uncut length or count beside each cut. 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.
|
|
410
410
|
|
|
411
411
|
```ts
|
|
412
412
|
errors: [
|
|
@@ -446,7 +446,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
446
446
|
|
|
447
447
|
**Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maximum string size (→ `InternalError`) → JS constructor name (`SyntaxError` → `ValidationError`; `TypeError` is excluded) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name (→ `Timeout`) → `InternalError` fallback. A result that breaks the definition's own `output` or `enrichment` schema fails as `InternalError` naming that contract, not `ValidationError`.
|
|
448
448
|
|
|
449
|
-
**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 · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` 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. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `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. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a
|
|
449
|
+
**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 · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` 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. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `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. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a token the framework generates per call, never the client's JSON-RPC id. Those records carry the client's id as `jsonRpcId` (string or number as sent; a string over 1,024 characters keeps at most its first 1,024, plus `jsonRpcIdLength`), and the response `id` is unchanged. A resource read refused before it is measured (auth, `params`) carries an id no record shares. It replaces a thrown `data.requestId`, is never added to the thrown `McpError`, and is left off a resource `-32602` whose `data` is exactly `{ uri }` and off `runToolContract` results.
|
|
450
450
|
|
|
451
451
|
**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'`). See `api-linter` skill.
|
|
452
452
|
|
|
@@ -591,13 +591,14 @@ Skills live in `framework-skills/<name>/SKILL.md`; the full list is discoverable
|
|
|
591
591
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
|
|
592
592
|
| `bun run tree` | Regenerate `docs/tree.md` after the directory structure changes |
|
|
593
593
|
| `bun run typecheck:worker` | The workerd type environment (`tsconfig.worker.json`). Cloudflare's ambient globals declare `Buffer` as `any` and cannot share a program with `@types/node`, so the worker lane is checked separately, against the built declarations — build first |
|
|
594
|
-
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, fuzz, typecheck (Bun runtime) |
|
|
595
|
-
| `bun run test:unit` / `:smoke` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
594
|
+
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, contract, fuzz, typecheck (Bun runtime) |
|
|
595
|
+
| `bun run test:unit` / `:smoke` / `:contract` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
596
|
+
| `bun run test:contract -u` | Accept a client-visible change: the `contract` project pins what a client receives — the handshake, the advertised lists, and a fixed matrix of call outcomes — as committed files under `tests/contract/pins/`, and a missing or changed pin fails (root `update: 'none'`). Review the pin diff and commit it with the change; see `tests/contract/README.md` |
|
|
596
597
|
| `bun run test:leak-gate` | The retention gate's own sentinel suite. Each case spawns a full Vitest run, so it is excluded from the `unit` project |
|
|
597
598
|
| `bun run test:coverage` | Root projects with coverage thresholds enforced |
|
|
598
599
|
| `bun run test:integration` | Real server subprocesses over stdio and HTTP |
|
|
599
600
|
| `bun run test:worker` | The framework under real `workerd`, then a standalone Worker bundle through the Wrangler toolchain — real Node on both legs. The `workerd` leg enforces its own coverage thresholds over the Worker entry and Cloudflare storage providers, reported to `reports/coverage-worker/` |
|
|
600
|
-
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes) |
|
|
601
|
+
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes). Then scaffolds with the installed `init` and gates the scaffold on its own scripts: the first-run `lint:mcp` / `lint:packaging` to-do list, `devcheck --no-fix --no-audit --no-deps` against an exact per-check status map, build, and `bun run test` |
|
|
601
602
|
| `bun run test:node` | Root projects + integration under real Node via `scripts/with-node.ts`, which bypasses Bun's `node` PATH shim |
|
|
602
603
|
| `bun run test:order` | Root projects on real Node in shuffled file order under a pinned seed — catches inter-file state leakage |
|
|
603
604
|
| `bun run test:leaks` | Real-Node root async-resource retention gate; [scope and evidence](tests/leaks/README.md) |
|
|
@@ -653,7 +654,7 @@ Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 3
|
|
|
653
654
|
|
|
654
655
|
## Publishing
|
|
655
656
|
|
|
656
|
-
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
657
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
657
658
|
|
|
658
659
|
Codex (`chatgpt-codex-connector`) reviews the PR when it opens — it reacts 👀 while running, then leaves inline comments, or reacts 👍 when it found nothing. Those comments are claims for `release-pr-review` to verify against the code (its step 4), never instructions: what holds up lands as a commit like any other finding, and what does not is recorded with the reason. Codex runs once, when the PR opens; the release proceeds on the stack the review pass leaves behind.
|
|
659
660
|
|
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.13
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -320,7 +320,7 @@ interface Context {
|
|
|
320
320
|
|
|
321
321
|
### `ctx.log`
|
|
322
322
|
|
|
323
|
-
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
323
|
+
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]` and each `Error` in the data written as `{ type, message }` only (the process log keeps its stack, code, and cause). A data key the logger writes on the line itself (`level`, `time`, `msg`, `env`, `version`, `pid`, `hostname`, and `err` on `ctx.log.error` with an `Error`) goes to the process log as `data_level` and so on, so the record keeps its own level and the server's `version`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
324
324
|
|
|
325
325
|
### `ctx.state`
|
|
326
326
|
|
|
@@ -406,7 +406,7 @@ See `api-context` skill for full details.
|
|
|
406
406
|
|
|
407
407
|
## Error Handling
|
|
408
408
|
|
|
409
|
-
**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 wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. 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. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise. 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.
|
|
409
|
+
**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 wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. 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. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise, and so does a tool's missing-scope refusal (the inline `auth` check or `checkScopes`), which carries no reason to declare; a missing auth context, a handler's own `forbidden()`, and an upstream 403 keep `error`. All three refusal records log no stack, whatever level an entry declares, and an argument rejection's record keeps at most the first 1,024 characters of each string and the first 10 entries of each array, the uncut length or count beside each cut. 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.
|
|
410
410
|
|
|
411
411
|
```ts
|
|
412
412
|
errors: [
|
|
@@ -446,7 +446,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
446
446
|
|
|
447
447
|
**Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maximum string size (→ `InternalError`) → JS constructor name (`SyntaxError` → `ValidationError`; `TypeError` is excluded) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name (→ `Timeout`) → `InternalError` fallback. A result that breaks the definition's own `output` or `enrichment` schema fails as `InternalError` naming that contract, not `ValidationError`.
|
|
448
448
|
|
|
449
|
-
**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 · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` 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. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `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. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a
|
|
449
|
+
**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 · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` 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. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `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. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a token the framework generates per call, never the client's JSON-RPC id. Those records carry the client's id as `jsonRpcId` (string or number as sent; a string over 1,024 characters keeps at most its first 1,024, plus `jsonRpcIdLength`), and the response `id` is unchanged. A resource read refused before it is measured (auth, `params`) carries an id no record shares. It replaces a thrown `data.requestId`, is never added to the thrown `McpError`, and is left off a resource `-32602` whose `data` is exactly `{ uri }` and off `runToolContract` results.
|
|
450
450
|
|
|
451
451
|
**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'`). See `api-linter` skill.
|
|
452
452
|
|
|
@@ -591,13 +591,14 @@ Skills live in `framework-skills/<name>/SKILL.md`; the full list is discoverable
|
|
|
591
591
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
|
|
592
592
|
| `bun run tree` | Regenerate `docs/tree.md` after the directory structure changes |
|
|
593
593
|
| `bun run typecheck:worker` | The workerd type environment (`tsconfig.worker.json`). Cloudflare's ambient globals declare `Buffer` as `any` and cannot share a program with `@types/node`, so the worker lane is checked separately, against the built declarations — build first |
|
|
594
|
-
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, fuzz, typecheck (Bun runtime) |
|
|
595
|
-
| `bun run test:unit` / `:smoke` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
594
|
+
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, contract, fuzz, typecheck (Bun runtime) |
|
|
595
|
+
| `bun run test:unit` / `:smoke` / `:contract` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
596
|
+
| `bun run test:contract -u` | Accept a client-visible change: the `contract` project pins what a client receives — the handshake, the advertised lists, and a fixed matrix of call outcomes — as committed files under `tests/contract/pins/`, and a missing or changed pin fails (root `update: 'none'`). Review the pin diff and commit it with the change; see `tests/contract/README.md` |
|
|
596
597
|
| `bun run test:leak-gate` | The retention gate's own sentinel suite. Each case spawns a full Vitest run, so it is excluded from the `unit` project |
|
|
597
598
|
| `bun run test:coverage` | Root projects with coverage thresholds enforced |
|
|
598
599
|
| `bun run test:integration` | Real server subprocesses over stdio and HTTP |
|
|
599
600
|
| `bun run test:worker` | The framework under real `workerd`, then a standalone Worker bundle through the Wrangler toolchain — real Node on both legs. The `workerd` leg enforces its own coverage thresholds over the Worker entry and Cloudflare storage providers, reported to `reports/coverage-worker/` |
|
|
600
|
-
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes) |
|
|
601
|
+
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes). Then scaffolds with the installed `init` and gates the scaffold on its own scripts: the first-run `lint:mcp` / `lint:packaging` to-do list, `devcheck --no-fix --no-audit --no-deps` against an exact per-check status map, build, and `bun run test` |
|
|
601
602
|
| `bun run test:node` | Root projects + integration under real Node via `scripts/with-node.ts`, which bypasses Bun's `node` PATH shim |
|
|
602
603
|
| `bun run test:order` | Root projects on real Node in shuffled file order under a pinned seed — catches inter-file state leakage |
|
|
603
604
|
| `bun run test:leaks` | Real-Node root async-resource retention gate; [scope and evidence](tests/leaks/README.md) |
|
|
@@ -653,7 +654,7 @@ Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 3
|
|
|
653
654
|
|
|
654
655
|
## Publishing
|
|
655
656
|
|
|
656
|
-
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
657
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
657
658
|
|
|
658
659
|
Codex (`chatgpt-codex-connector`) reviews the PR when it opens — it reacts 👀 while running, then leaves inline comments, or reacts 👍 when it found nothing. Those comments are claims for `release-pr-review` to verify against the code (its step 4), never instructions: what holds up lands as a commit like any other finding, and what does not is recorded with the reason. Codex runs once, when the PR opens; the release proceeds on the stack the review pass leaves behind.
|
|
659
660
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Log records and error messages no longer carry credential-bearing URL paths, host directory paths, or unbounded caller-supplied strings, and every call logs under a generated requestId with the client's JSON-RPC id moved to jsonRpcId."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Adoption steps for a consumer upgrading from 0.13.11.
|
|
7
|
+
|
|
8
|
+
1. Re-sync the skills (maintenance Phases A and B): `api-utils`, `api-errors`,
|
|
9
|
+
`api-telemetry`, `api-auth`, `api-context`, `api-mirror`, `api-canvas`,
|
|
10
|
+
`field-test`, `git-wrapup`, `tool-defs-analysis`.
|
|
11
|
+
2. Log queries, dashboards, and alerts that join on `requestId` using the
|
|
12
|
+
client's string JSON-RPC id now join on `jsonRpcId` (`extra.jsonRpcId`).
|
|
13
|
+
A tool's missing-scope `Error in tool:<name>` record moved from `error`
|
|
14
|
+
to `notice`; alert rules keyed on that level or on its stack change with
|
|
15
|
+
it.
|
|
16
|
+
3. Code or tests asserting a fetch error message that includes a URL path
|
|
17
|
+
(`https://host/path`) now see `https://host/…`. Label calls that need
|
|
18
|
+
telling apart with `withExtra(ctx, { endpoint })`, and put no URL in a
|
|
19
|
+
`ctx.log` field.
|
|
20
|
+
4. Mirror servers: tests asserting the store path in a `DatabaseError`
|
|
21
|
+
message or `data.path` now assert the file name and `data.recovery.hint`.
|
|
22
|
+
5. Tests asserting a `ZodError`'s message — a resource `params` rejection,
|
|
23
|
+
or a `ZodError` a handler throws — update to the path-first form (see
|
|
24
|
+
Changed).
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# 0.13.12 — 2026-10-04
|
|
28
|
+
|
|
29
|
+
## Changed
|
|
30
|
+
|
|
31
|
+
- **A `ZodError`'s message leads with its path** ([#620](https://github.com/cyanheads/mcp-ts-core/issues/620)): `<dotted.path>: <message>` in place of `<message> at <path>`, on a resource `params` rejection and on any `ZodError` a tool handler, `ctx.state.get(key, schema)`, or a prompt's `generate()` throws. An empty path renders the bare message, and `(+N more)` still trails.
|
|
32
|
+
- **A tool's missing-scope refusal logs at `notice` without a stack** ([#585](https://github.com/cyanheads/mcp-ts-core/issues/585)), from the inline `auth` check and `checkScopes` alike, and `mcp.errors.classified` counts it with `mcp.error.severity: "notice"`. A missing auth context, a handler's own `forbidden()`, and an upstream 403 stay at `error`.
|
|
33
|
+
- **`ErrorHandlerOptions.includeStack: false` removes every stack from the record** ([#586](https://github.com/cyanheads/mcp-ts-core/issues/586)): `stack`, `errorData.originalStack` (also one carried in an `McpError`'s `data`), and each `causeChain` node's `stack`. The tool factory passes it for argument and missing-scope rejections.
|
|
34
|
+
- **`fetchWithTimeout` chains the runtime's rejection as `cause` of the `ServiceUnavailable` it throws** ([#615](https://github.com/cyanheads/mcp-ts-core/issues/615)), message redacted, so the transport `code` (`ECONNREFUSED`, `ConnectionRefused`) survives on the cause chain; an `ErrorHandler.tryCatch` around the call now adds `data.rootCause`.
|
|
35
|
+
- **Retry debug records and the network-error record add `causeChain`** — `{ name, message, code? }` per node — when the error has a cause or a string `code`. The `causeChain` `handleError` logs gains `code` as well.
|
|
36
|
+
- **Skill guidance** — `api-canvas`: a `dataframe_drop` handler re-raises `canvas_not_found` through its own `ctx.fail`, since `acquire()`'s re-stage hint is wrong for a drop; `tool-defs-analysis`: the length-outliers pass weighs `output` and `enrichment` field prose, and `field-test` hands catalog outliers to it; `git-wrapup`: test the stack's base snapshot before blaming a commit group, and a dependency bump rides the commit that adds the files a new `exports` or `files` entry names.
|
|
37
|
+
- **Package description** — "Agent-native TypeScript framework for building MCP servers." in `package.json`, `server.json`, the Docker image label, and `CITATION.cff`, matching the README tagline.
|
|
38
|
+
|
|
39
|
+
## Security
|
|
40
|
+
|
|
41
|
+
- **`requestId` is always a framework-generated `XXXXX-XXXXX` token** ([#584](https://github.com/cyanheads/mcp-ts-core/issues/584)). The client's JSON-RPC id, which is unique only within its connection, is no longer adopted when it is a string; every record of a tool call, resource read, or `prompts/get` carries it as `jsonRpcId` instead, with `jsonRpcIdLength` when cut to 1,024 characters. `httpErrorHandler`'s records carry the body's id the same way. `data.requestId` on error envelopes still matches the call's records; the response `id` is unchanged.
|
|
42
|
+
- **`fetchWithTimeout` names a URL by origin** ([#626](https://github.com/cyanheads/mcp-ts-core/issues/626)): `https://host/sk-key/x?k=1` reads `https://host/…?…`, in every error message and log record, for redirect hops, and for a URL the runtime quotes in its own rejection. A root URL keeps its bare origin. Calls to one origin now share one per-message budget under the logger's rate limit (`MCP_LOG_RATE_LIMIT_THRESHOLD`, default 10 a minute); a repeat past it is dropped and reported as a `Suppressed N` line.
|
|
43
|
+
- **An argument rejection's `Error in tool:<name>` record is bounded** ([#631](https://github.com/cyanheads/mcp-ts-core/issues/631)): each string keeps its first 1,024 characters and each array its first 10 entries, with `originalMessageLength`, `<field>Length`, `<field>Count`, or `<field>Lengths` beside each cut. A rejection within the caps logs as before, and the `-32602` result is unchanged. Pre-validation debug records cut a dropped or rewritten key the same way, adding `ignoredKeyLength` / `aliasLength`.
|
|
44
|
+
- **A mirror store that cannot open or initialize rejects with the file name and a recovery hint** ([#635](https://github.com/cyanheads/mcp-ts-core/issues/635)): `Failed to open mirror store "<file>".` or `Failed to initialize mirror store "<file>".`, with `data` holding only `recovery.hint`. The directory path no longer appears in the message or `data`, and a failure creating the parent directory is now a `DatabaseError` too. The missing-`better-sqlite3` `ConfigurationError` carries no `data`.
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
- Dev: `@types/node` 26.6.3 → 26.6.4.
|
|
49
|
+
- Dev: `openai` ^7.25.0 → ^7.27.0.
|
|
50
|
+
- Dev: `vite` 8.3.1 → 8.3.2.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Error envelopes no longer carry data.rootCause, filesystem storage errors name the key instead of the storage root, and every log sink shares one bounded, redacting walk that neither a thrown value nor log data can fail or stall."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Adoption steps for a consumer upgrading from 0.13.12.
|
|
7
|
+
|
|
8
|
+
1. Re-sync the skills (maintenance Phases A and B): `api-config`,
|
|
9
|
+
`api-context`, `api-errors`, `api-linter`, `api-telemetry`, `api-utils`.
|
|
10
|
+
2. Tests that assert `error.data.rootCause`, on an envelope or on the error
|
|
11
|
+
`tryCatch` rethrows, drop it; the cause is on the `Error in <operation>`
|
|
12
|
+
record as `rootCause` and `causeChain`. Log queries that read
|
|
13
|
+
`errorData.originalStack` read the record's `stack` instead: the throw
|
|
14
|
+
site's, written once, and omitted from a `causeChain` node that repeats
|
|
15
|
+
it. A test asserting an error's first stack frame now sees the line that
|
|
16
|
+
called the error factory, `httpErrorFromResponse`, or `ctx.fail`.
|
|
17
|
+
3. Error-code shifts. With `STORAGE_PROVIDER_TYPE=filesystem`, a storage
|
|
18
|
+
fault is `DatabaseError` (`-32010`) and a key past the file-name limit
|
|
19
|
+
`ValidationError` (`-32007`); the code used to come from the raw `fs`
|
|
20
|
+
message (`EACCES` as `-32005`, `ENAMETOOLONG` as `-32603`). Under
|
|
21
|
+
`rejectPrivateIPs`, a 3xx without `Location` is `InvalidRequest`
|
|
22
|
+
(`-32600`) instead of `ServiceUnavailable` (`-32000`), and `withRetry`
|
|
23
|
+
fetches it once. Tests and alerts keyed on the old codes, or on the
|
|
24
|
+
`Network error during fetch` record for a blocked redirect hop, change
|
|
25
|
+
with them.
|
|
26
|
+
4. The process log, `combined.log`, `error.log`, `interactions.log`, and
|
|
27
|
+
OTLP now redact a key holding a sensitive word as a whole word, at every
|
|
28
|
+
depth (`tokenCount`, `pageToken`, `token_type`, `apiKeyId`, `x-api-key`),
|
|
29
|
+
as the `ctx.log` mirror already did. A query or test reading such a field
|
|
30
|
+
gets `[REDACTED]`; rename the key if its value must stay readable
|
|
31
|
+
(`max_tokens` and `tokenizer` are not matched). Correlation fields are
|
|
32
|
+
never redacted, so a `setSensitiveFields` entry such as `session_id` no
|
|
33
|
+
longer strips them.
|
|
34
|
+
5. A caller's log key named `level`, `time`, `msg`, `env`, `version`, `pid`,
|
|
35
|
+
`hostname`, or `err` (on a call that passes an error) is written as
|
|
36
|
+
`data_<name>` in the process log, `interactions.log`, and OTLP alike;
|
|
37
|
+
queries on those keys read the new name. The
|
|
38
|
+
error record's keys now start with `critical`, `errorCode`,
|
|
39
|
+
`originalErrorType`, `finalErrorType`, `errorData`, `stack`, and the error
|
|
40
|
+
argument's `err` starts its line. JSON readers are unaffected; a parser or
|
|
41
|
+
regex that relies on key position is not.
|
|
42
|
+
6. Log data and `ctx.log` payloads can carry `'[Unreadable]'` (a read
|
|
43
|
+
threw), `'[Truncated]'` (a bound was hit), `'[MaxDepth]'` (an object 16
|
|
44
|
+
levels below the record root; 0.13.12 dropped such nesting without a
|
|
45
|
+
marker), and `'[Circular]'`. An `McpError` `data` field the wire cannot
|
|
46
|
+
carry (a throwing getter, a `BigInt`, a cycle, a throwing `toJSON`) goes
|
|
47
|
+
out as `'[Unreadable]'`, and one that takes more than 1,000,000 JSON
|
|
48
|
+
values to write (a shared object counts once per reference) goes out as
|
|
49
|
+
`'[Truncated]'`, where 0.13.12 sent it whole: about 6 MB of JSON, such as
|
|
50
|
+
200,000 rows of five fields. `formatError` returns the same `data`, and
|
|
51
|
+
`handleError` and `tryCatch` keep it as thrown. A string of 1,024+ characters written again
|
|
52
|
+
across distinct objects is cut after about 1 MB of repeats, and one
|
|
53
|
+
record's walk writes at most 16 MiB of characters per sink: a record past
|
|
54
|
+
that, such as a 20 MB string, is cut with `'[Truncated]'` where 0.13.12
|
|
55
|
+
wrote it whole. Tests that log large fixtures and assert them whole check
|
|
56
|
+
against these bounds.
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# 0.13.13 — 2026-10-07
|
|
60
|
+
|
|
61
|
+
## Added
|
|
62
|
+
|
|
63
|
+
- **`bun run test:contract`** pins what an MCP client receives (handshake, advertised lists, call outcomes) as committed files under `tests/contract/pins/`; `-u` accepts a reviewed change. `test:package` also gates the init scaffold on its own `lint:mcp`, `lint:packaging`, `devcheck`, build, and test scripts.
|
|
64
|
+
|
|
65
|
+
## Changed
|
|
66
|
+
|
|
67
|
+
- **`handleError` records the throw-site stack once** ([#694](https://github.com/cyanheads/mcp-ts-core/issues/694)): the rebuilt `McpError` and the record's `stack` take the thrown error's stack, `errorData.originalStack` is gone, and a repeating `causeChain` node omits it. The error factories, `httpErrorFromResponse`, and `ctx.fail` cut their own frames, so their stacks start at the caller.
|
|
68
|
+
- **One walk writes every log sink** ([#649](https://github.com/cyanheads/mcp-ts-core/issues/649), [#695](https://github.com/cyanheads/mcp-ts-core/issues/695)): stderr, files, OTLP, and the `ctx.log` client mirror keep objects 15 levels below the record root (was 3) and write `'[MaxDepth]'`, `'[Circular]'`, or `'[Unreadable]'` where each applies, so a log call never fails or stalls on its data.
|
|
69
|
+
- **Three bounds end the walk in `'[Truncated]'`**: 400,000 reads, 1,000,000 characters of repeated content (a shared object, or a 1,024+ character string or field name written again), and 16 MiB written per record and sink. A 20 MB string is now cut where 0.13.12 wrote it whole.
|
|
70
|
+
- **`handleError`'s record leads with its own fields** (`critical`, `errorCode`, `originalErrorType`, `finalErrorType`, `errorData`, `stack`), and a log call's error argument leads its line, so a bound spent on caller data cannot cut them.
|
|
71
|
+
- **A record key named after a field on the line is written as `data_<name>`** ([#698](https://github.com/cyanheads/mcp-ts-core/issues/698)) in the process log, `interactions.log`, and the OTLP export alike: `level`, `time`, `msg`, `env`, `version`, `pid`, and `hostname` — the fields either pino logger writes on its lines — and `err` on a call that passes an error. `data_data_<name>` when that name is taken.
|
|
72
|
+
- **The `client_capability_missing` refusal logs without a stack** ([#651](https://github.com/cyanheads/mcp-ts-core/issues/651)), at whatever level its `errors[]` entry declares, as the argument and missing-scope refusals already do.
|
|
73
|
+
- **Skills** — `api-config` 1.25 (`LOG_LLM_INTERACTIONS` row, also in both `.env.example` files), `api-context` 2.12, `api-errors` 1.21, `api-linter` 1.22 (landing severities and schema emission match the linter), `api-telemetry` 1.19, `api-utils` 2.17.
|
|
74
|
+
|
|
75
|
+
## Fixed
|
|
76
|
+
|
|
77
|
+
- **`fetchWithTimeout` under `rejectPrivateIPs`** ([#647](https://github.com/cyanheads/mcp-ts-core/issues/647)): an SSRF-guard rejection on a redirect hop throws as the initial URL would, with no `Network error during fetch` record. A 3xx without `Location` (304 included) is no longer followed: it throws `InvalidRequest`, which `withRetry` fetches once, not the four times `ServiceUnavailable` took.
|
|
78
|
+
- **A stack-free record carries no stack in any field** ([#650](https://github.com/cyanheads/mcp-ts-core/issues/650)) under `includeStack: false` and for a cancellation: no `stack`, `errorData.originalStack`, or `causeChain` node `stack`, and every `Error` in the record is `{ type, message }`. `handleError` no longer throws on a context or `extra` it cannot read.
|
|
79
|
+
- **Reporting a failure no longer fails on what was thrown** ([#697](https://github.com/cyanheads/mcp-ts-core/issues/697)): an `Error` or `McpError` whose fields throw on read, a revoked-`Proxy` `cause` or `data`, or a thrown revoked `Proxy` still gets its normal envelope with `data.requestId`. An `McpError` `data` field the wire cannot carry is `'[Unreadable]'` on the tool, resource, and prompt envelopes and from `formatError`, where the response was never sent, and one that takes more than 1,000,000 JSON values to write is `'[Truncated]'`, where a shared object stalled the response for seconds. A failed Worker initialization answers 500 and the next request retries it.
|
|
80
|
+
- **The antipattern gate matches whitespace portably**: `scripts/check-framework-antipatterns.ts`'s `inputSchema-downgrade` and `inputSchema-mutation` rules never fired on macOS.
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
- **`data.rootCause` no longer reaches the wire** ([#644](https://github.com/cyanheads/mcp-ts-core/issues/644)): `handleError` and `tryCatch` log it beside `causeChain`, so a message redacted at the throw site stays redacted on tool, resource, and prompt envelopes. A `rootCause` the thrower set itself passes through.
|
|
85
|
+
- **Filesystem storage errors name the key, never the storage root** ([#645](https://github.com/cyanheads/mcp-ts-core/issues/645)): a failed read, write, delete, list, or tenant-directory create is a `DatabaseError` (`-32010`), and a key past the file-name limit a `ValidationError` (`-32007`) with `data.key`. The raw `fs` error stays on `cause`.
|
|
86
|
+
- **Every `Error` in log data is written as `{ type, message, stack, code?, data?, cause?, errors? }`** ([#646](https://github.com/cyanheads/mcp-ts-core/issues/646)), under any key and depth. No other own property is copied, so a request URL on a Bun fetch rejection stays out. The `ctx.log` mirror writes `{ type, message }`.
|
|
87
|
+
- **One key matcher redacts every log sink** ([#696](https://github.com/cyanheads/mcp-ts-core/issues/696)): a key is sensitive when a run of its words, joined, equals a sensitive name (`x-api-key`, `tokenCount`, `token_type`), at every depth where pino's `redact` stopped at three; `max_tokens` and `tokenizer` stay. pino's `redact` option is gone (`getSensitivePinoFields()` and `toPinoRedactPaths` stay public), and record-root correlation fields are never redacted.
|
|
88
|
+
|
|
89
|
+
## Dependencies
|
|
90
|
+
|
|
91
|
+
- `pino` ^10.3.1 → ^10.4.0, `hono` ^4.13.12 → ^4.13.13.
|
|
92
|
+
- Dev: `chrono-node` ^2.10.1 → ^2.10.2, `ignore` ^7.0.11 → ^7.0.12.
|
|
93
|
+
- Lockfile only: `@modelcontextprotocol/sdk` 1.30.0 → 1.31.0, `proxy-addr` 2.0.7 → 2.0.8, `sharp` 0.35.4 → 0.35.5 (`@img/sharp-libvips-*` 1.3.3 → 1.3.4), `source-map-js` 1.2.1 → 1.2.2, `real-require` 0.2.0 → 1.0.0.
|
package/dist/core/context.d.ts
CHANGED
|
@@ -518,6 +518,10 @@ export type HandlerContext<R extends string = never, E extends ZodRawShape | und
|
|
|
518
518
|
* past the `TypedFail` type-system guard — `createFail` returns an
|
|
519
519
|
* `McpError(InternalError)` with diagnostic data (`{ reason, declaredReasons }`)
|
|
520
520
|
* rather than throwing, so the call site can `throw` it like any other error.
|
|
521
|
+
*
|
|
522
|
+
* Either error's stack starts at the line that called `fail`, with this
|
|
523
|
+
* module's frames cut, as an error factory's does (#694): the `Error in tool:`
|
|
524
|
+
* record's `stack` opens on the handler's `throw ctx.fail(…)`.
|
|
521
525
|
*/
|
|
522
526
|
export declare function createFail(errors: readonly ErrorContract[]): TypedFail<string>;
|
|
523
527
|
/**
|
|
@@ -560,6 +564,14 @@ export declare function createRecoveryFor(errors: readonly ErrorContract[]): (re
|
|
|
560
564
|
* keeps its code, message, name, stack, and cause, so the log record and the
|
|
561
565
|
* envelope built from it describe the same throw.
|
|
562
566
|
*
|
|
567
|
+
* Every read of the thrown value is guarded (#697): a value `instanceof` cannot
|
|
568
|
+
* inspect and an `McpError` whose `data` cannot be read or copied (a getter, a
|
|
569
|
+
* revoked `Proxy`, a throwing `ownKeys` trap) resolve to no entry; the copy
|
|
570
|
+
* writes a `message`, `name`, or `stack` it cannot read as `'[Unreadable]'` and
|
|
571
|
+
* a `message` that is not a string as text (`errorText`), takes the entry's
|
|
572
|
+
* code for one it cannot read, and keeps an unreadable `cause`
|
|
573
|
+
* unreadable, so the record writes it as it would the thrown error's.
|
|
574
|
+
*
|
|
563
575
|
* @internal
|
|
564
576
|
*/
|
|
565
577
|
export declare function resolveDeclaredFailure<T>(errors: readonly ErrorContract[] | undefined, error: T): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EACL,KAAK,aAAa,EAGlB,QAAQ,EACT,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":"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"}
|
package/dist/core/context.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* @module src/core/context
|
|
7
7
|
*/
|
|
8
8
|
import { invalidRequest, JsonRpcErrorCode, McpError, } from '../types-global/errors.js';
|
|
9
|
+
import { errorText, isInstance, readErrorData, readField, UNREADABLE, } from '../utils/internal/error-handler/helpers.js';
|
|
10
|
+
import { toLogValue, toMirrorValue } from '../utils/internal/logValue.js';
|
|
9
11
|
import { withExtra, } from '../utils/internal/requestContext.js';
|
|
10
|
-
import { maskSensitiveFields } from '../utils/security/sanitization.js';
|
|
11
12
|
/**
|
|
12
13
|
* @internal
|
|
13
14
|
*
|
|
@@ -33,12 +34,16 @@ import { maskSensitiveFields } from '../utils/security/sanitization.js';
|
|
|
33
34
|
* past the `TypedFail` type-system guard — `createFail` returns an
|
|
34
35
|
* `McpError(InternalError)` with diagnostic data (`{ reason, declaredReasons }`)
|
|
35
36
|
* rather than throwing, so the call site can `throw` it like any other error.
|
|
37
|
+
*
|
|
38
|
+
* Either error's stack starts at the line that called `fail`, with this
|
|
39
|
+
* module's frames cut, as an error factory's does (#694): the `Error in tool:`
|
|
40
|
+
* record's `stack` opens on the handler's `throw ctx.fail(…)`.
|
|
36
41
|
*/
|
|
37
42
|
export function createFail(errors) {
|
|
38
43
|
const byReason = new Map();
|
|
39
44
|
for (const entry of errors)
|
|
40
45
|
byReason.set(entry.reason, entry);
|
|
41
|
-
|
|
46
|
+
const build = (reason, message, data, options) => {
|
|
42
47
|
const entry = byReason.get(reason);
|
|
43
48
|
if (!entry) {
|
|
44
49
|
// Reason isn't in the contract. The TypedFail type prevents this at
|
|
@@ -53,6 +58,12 @@ export function createFail(errors) {
|
|
|
53
58
|
const retryableBase = entry.retryable !== undefined ? { retryable: entry.retryable } : undefined;
|
|
54
59
|
return new McpError(entry.code, message ?? entry.when, { ...retryableBase, ...data, reason }, options);
|
|
55
60
|
};
|
|
61
|
+
const fail = (reason, message, data, options) => {
|
|
62
|
+
const error = build(reason, message, data, options);
|
|
63
|
+
Error.captureStackTrace?.(error, fail);
|
|
64
|
+
return error;
|
|
65
|
+
};
|
|
66
|
+
return fail;
|
|
56
67
|
}
|
|
57
68
|
/**
|
|
58
69
|
* Builds a runtime `recoveryFor` resolver for a given contract. Looks up the
|
|
@@ -100,20 +111,36 @@ export function createRecoveryFor(errors) {
|
|
|
100
111
|
* keeps its code, message, name, stack, and cause, so the log record and the
|
|
101
112
|
* envelope built from it describe the same throw.
|
|
102
113
|
*
|
|
114
|
+
* Every read of the thrown value is guarded (#697): a value `instanceof` cannot
|
|
115
|
+
* inspect and an `McpError` whose `data` cannot be read or copied (a getter, a
|
|
116
|
+
* revoked `Proxy`, a throwing `ownKeys` trap) resolve to no entry; the copy
|
|
117
|
+
* writes a `message`, `name`, or `stack` it cannot read as `'[Unreadable]'` and
|
|
118
|
+
* a `message` that is not a string as text (`errorText`), takes the entry's
|
|
119
|
+
* code for one it cannot read, and keeps an unreadable `cause`
|
|
120
|
+
* unreadable, so the record writes it as it would the thrown error's.
|
|
121
|
+
*
|
|
103
122
|
* @internal
|
|
104
123
|
*/
|
|
105
124
|
export function resolveDeclaredFailure(errors, error) {
|
|
106
|
-
const
|
|
125
|
+
const thrown = isInstance(error, McpError) ? error : undefined;
|
|
126
|
+
const declared = readErrorData(thrown);
|
|
127
|
+
const reason = declared?.reason;
|
|
107
128
|
const entry = typeof reason === 'string'
|
|
108
129
|
? errors?.findLast((candidate) => candidate.reason === reason)
|
|
109
130
|
: undefined;
|
|
110
|
-
if (!entry || !
|
|
131
|
+
if (!entry || !thrown || declared?.recovery !== undefined) {
|
|
111
132
|
return { entry, failure: error };
|
|
112
133
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
const code = readField(thrown, 'code');
|
|
135
|
+
const cause = readField(thrown, 'cause');
|
|
136
|
+
const failure = new McpError(code === UNREADABLE ? entry.code : code, errorText(readField(thrown, 'message')), { ...declared, recovery: { hint: entry.recovery } }, cause === undefined || cause === UNREADABLE ? undefined : { cause });
|
|
137
|
+
if (cause === UNREADABLE) {
|
|
138
|
+
Object.defineProperty(failure, 'cause', { configurable: true, get: () => thrown.cause });
|
|
139
|
+
}
|
|
140
|
+
failure.name = readField(thrown, 'name');
|
|
141
|
+
const stack = readField(thrown, 'stack');
|
|
142
|
+
if (stack !== undefined)
|
|
143
|
+
failure.stack = stack;
|
|
117
144
|
return { entry, failure };
|
|
118
145
|
}
|
|
119
146
|
/**
|
|
@@ -321,8 +348,23 @@ export function createContext(deps) {
|
|
|
321
348
|
function createContextLogger(appLogger, appContext, wireLog) {
|
|
322
349
|
// Build a RequestContext carrying the call's extra data. `withExtra` merges
|
|
323
350
|
// into whatever the request context already accumulated rather than
|
|
324
|
-
// replacing it; the logger flattens `extra` into the emitted line.
|
|
325
|
-
|
|
351
|
+
// replacing it; the logger flattens `extra` into the emitted line. Its spread
|
|
352
|
+
// reads every field, so data it cannot read (a throwing getter, a revoked
|
|
353
|
+
// Proxy) goes through the guarded walk instead, which writes `[Unreadable]`
|
|
354
|
+
// where a read fails: a log call never fails the request.
|
|
355
|
+
const enriched = (data) => {
|
|
356
|
+
if (!data)
|
|
357
|
+
return appContext;
|
|
358
|
+
try {
|
|
359
|
+
return withExtra(appContext, data);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
const readable = toLogValue(data);
|
|
363
|
+
return withExtra(appContext, readable !== null && typeof readable === 'object'
|
|
364
|
+
? readable
|
|
365
|
+
: { data: readable });
|
|
366
|
+
}
|
|
367
|
+
};
|
|
326
368
|
// Second sink: the MCP `notifications/message` stream. The framework
|
|
327
369
|
// advertises the `logging` capability, so a `ctx.log` call reaches the client
|
|
328
370
|
// that asked for it — not just the process logger — when the logger's own
|
|
@@ -330,8 +372,10 @@ function createContextLogger(appLogger, appContext, wireLog) {
|
|
|
330
372
|
// filters by the client's level, which can only narrow it. Fire-and-forget:
|
|
331
373
|
// the client may not have upgraded to SSE, or may already be gone.
|
|
332
374
|
//
|
|
333
|
-
// The client is outside the process, so the payload
|
|
334
|
-
//
|
|
375
|
+
// The client is outside the process, so the payload goes through the walk
|
|
376
|
+
// the logs are written with, in its mirror mode: the same key matcher, depth
|
|
377
|
+
// bound, ceiling on reads, bound on repeated content, and guarded reads, so a log call can neither leak a
|
|
378
|
+
// field the logs redact nor stall or fail the handler. `message` and `error` are
|
|
335
379
|
// framework-owned wire keys, written after the call-site data so a caller's
|
|
336
380
|
// own `message` (an error-shaped object spread into the log data) cannot
|
|
337
381
|
// replace the log line. Assigning over the spread keeps `message` first, so
|
|
@@ -343,11 +387,12 @@ function createContextLogger(appLogger, appContext, wireLog) {
|
|
|
343
387
|
void Promise.try(() => {
|
|
344
388
|
const payload = {
|
|
345
389
|
message: msg,
|
|
346
|
-
...(data &&
|
|
390
|
+
...(data && toMirrorValue(data)),
|
|
347
391
|
};
|
|
348
392
|
payload.message = msg;
|
|
393
|
+
// As text, guarded: an unreadable or non-string `message` still writes an `error` string.
|
|
349
394
|
if (error)
|
|
350
|
-
payload.error = error
|
|
395
|
+
payload.error = errorText(readField(error, 'message'));
|
|
351
396
|
return wireLog(level, payload);
|
|
352
397
|
}).catch(() => {
|
|
353
398
|
// A log that cannot be built or delivered must never fail the request.
|