@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.
Files changed (125) hide show
  1. package/AGENTS.md +9 -8
  2. package/CLAUDE.md +9 -8
  3. package/README.md +1 -1
  4. package/changelog/0.13.x/0.13.12.md +50 -0
  5. package/changelog/0.13.x/0.13.13.md +93 -0
  6. package/dist/core/context.d.ts +12 -0
  7. package/dist/core/context.d.ts.map +1 -1
  8. package/dist/core/context.js +59 -14
  9. package/dist/core/context.js.map +1 -1
  10. package/dist/core/worker.d.ts.map +1 -1
  11. package/dist/core/worker.js +21 -8
  12. package/dist/core/worker.js.map +1 -1
  13. package/dist/mcp-server/handlerContext.d.ts +14 -8
  14. package/dist/mcp-server/handlerContext.d.ts.map +1 -1
  15. package/dist/mcp-server/handlerContext.js +16 -9
  16. package/dist/mcp-server/handlerContext.js.map +1 -1
  17. package/dist/mcp-server/inputRequired.d.ts +18 -9
  18. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  19. package/dist/mcp-server/inputRequired.js +29 -15
  20. package/dist/mcp-server/inputRequired.js.map +1 -1
  21. package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
  22. package/dist/mcp-server/prompts/prompt-registration.js +10 -7
  23. package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
  24. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  25. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +29 -19
  26. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  27. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +8 -2
  28. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/utils/inputPrevalidation.js +18 -7
  30. package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
  31. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +7 -1
  32. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +107 -21
  34. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  35. package/dist/mcp-server/transports/auth/lib/authUtils.d.ts +22 -0
  36. package/dist/mcp-server/transports/auth/lib/authUtils.d.ts.map +1 -1
  37. package/dist/mcp-server/transports/auth/lib/authUtils.js +29 -1
  38. package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
  39. package/dist/mcp-server/transports/auth/lib/checkScopes.d.ts.map +1 -1
  40. package/dist/mcp-server/transports/auth/lib/checkScopes.js +2 -1
  41. package/dist/mcp-server/transports/auth/lib/checkScopes.js.map +1 -1
  42. package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
  43. package/dist/mcp-server/transports/http/httpErrorHandler.js +4 -2
  44. package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
  45. package/dist/services/mirror/sqlite/handle.d.ts +13 -2
  46. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
  47. package/dist/services/mirror/sqlite/handle.js +17 -6
  48. package/dist/services/mirror/sqlite/handle.js.map +1 -1
  49. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
  50. package/dist/services/mirror/sqlite/sqliteMirrorStore.js +3 -2
  51. package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
  52. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
  53. package/dist/storage/providers/fileSystem/fileSystemProvider.js +48 -21
  54. package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
  55. package/dist/types-global/errors.d.ts.map +1 -1
  56. package/dist/types-global/errors.js +31 -16
  57. package/dist/types-global/errors.js.map +1 -1
  58. package/dist/utils/internal/error-handler/errorHandler.d.ts +29 -11
  59. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  60. package/dist/utils/internal/error-handler/errorHandler.js +204 -95
  61. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  62. package/dist/utils/internal/error-handler/helpers.d.ts +91 -9
  63. package/dist/utils/internal/error-handler/helpers.d.ts.map +1 -1
  64. package/dist/utils/internal/error-handler/helpers.js +243 -39
  65. package/dist/utils/internal/error-handler/helpers.js.map +1 -1
  66. package/dist/utils/internal/error-handler/types.d.ts +21 -4
  67. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  68. package/dist/utils/internal/logValue.d.ts +33 -0
  69. package/dist/utils/internal/logValue.d.ts.map +1 -0
  70. package/dist/utils/internal/logValue.js +539 -0
  71. package/dist/utils/internal/logValue.js.map +1 -0
  72. package/dist/utils/internal/logger.d.ts +38 -9
  73. package/dist/utils/internal/logger.d.ts.map +1 -1
  74. package/dist/utils/internal/logger.js +228 -118
  75. package/dist/utils/internal/logger.js.map +1 -1
  76. package/dist/utils/internal/observabilityCap.d.ts +35 -0
  77. package/dist/utils/internal/observabilityCap.d.ts.map +1 -0
  78. package/dist/utils/internal/observabilityCap.js +43 -0
  79. package/dist/utils/internal/observabilityCap.js.map +1 -0
  80. package/dist/utils/internal/performance.d.ts.map +1 -1
  81. package/dist/utils/internal/performance.js +9 -11
  82. package/dist/utils/internal/performance.js.map +1 -1
  83. package/dist/utils/internal/requestContext.d.ts +3 -3
  84. package/dist/utils/internal/requestContext.js +1 -1
  85. package/dist/utils/network/fetchWithTimeout.d.ts +20 -10
  86. package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
  87. package/dist/utils/network/fetchWithTimeout.js +112 -30
  88. package/dist/utils/network/fetchWithTimeout.js.map +1 -1
  89. package/dist/utils/network/httpError.d.ts +8 -6
  90. package/dist/utils/network/httpError.d.ts.map +1 -1
  91. package/dist/utils/network/httpError.js +23 -7
  92. package/dist/utils/network/httpError.js.map +1 -1
  93. package/dist/utils/network/retry.d.ts +25 -2
  94. package/dist/utils/network/retry.d.ts.map +1 -1
  95. package/dist/utils/network/retry.js +47 -5
  96. package/dist/utils/network/retry.js.map +1 -1
  97. package/dist/utils/security/sanitization.d.ts +24 -28
  98. package/dist/utils/security/sanitization.d.ts.map +1 -1
  99. package/dist/utils/security/sanitization.js +25 -84
  100. package/dist/utils/security/sanitization.js.map +1 -1
  101. package/dist/utils/security/sensitiveFields.d.ts +32 -4
  102. package/dist/utils/security/sensitiveFields.d.ts.map +1 -1
  103. package/dist/utils/security/sensitiveFields.js +85 -4
  104. package/dist/utils/security/sensitiveFields.js.map +1 -1
  105. package/dist/utils/telemetry/trace.d.ts +3 -1
  106. package/dist/utils/telemetry/trace.d.ts.map +1 -1
  107. package/dist/utils/telemetry/trace.js +6 -6
  108. package/dist/utils/telemetry/trace.js.map +1 -1
  109. package/framework-skills/api-auth/SKILL.md +3 -1
  110. package/framework-skills/api-canvas/SKILL.md +2 -2
  111. package/framework-skills/api-config/SKILL.md +2 -1
  112. package/framework-skills/api-context/SKILL.md +6 -6
  113. package/framework-skills/api-errors/SKILL.md +19 -17
  114. package/framework-skills/api-linter/SKILL.md +11 -10
  115. package/framework-skills/api-mirror/SKILL.md +3 -1
  116. package/framework-skills/api-telemetry/SKILL.md +14 -8
  117. package/framework-skills/api-utils/SKILL.md +6 -6
  118. package/framework-skills/api-utils/references/security.md +4 -2
  119. package/framework-skills/field-test/SKILL.md +2 -2
  120. package/framework-skills/git-wrapup/SKILL.md +3 -3
  121. package/framework-skills/tool-defs-analysis/SKILL.md +2 -2
  122. package/package.json +10 -9
  123. package/scripts/check-framework-antipatterns.ts +3 -2
  124. package/templates/.env.example +2 -0
  125. package/templates/tests/tools/echo.tool.test.ts +19 -1
@@ -1 +1 @@
1
- {"version":3,"file":"trace.js","sourceRoot":"","sources":["../../../src/utils/telemetry/trace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace.js","sourceRoot":"","sources":["../../../src/utils/telemetry/trace.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ description: >
4
4
  Authentication, authorization, and multi-tenancy patterns for `@cyanheads/mcp-ts-core`. Use when implementing auth scopes on tools/resources, configuring auth modes (none/jwt/oauth), working with JWT/OAuth env vars, or understanding how tenantId flows through ctx.state.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.5"
7
+ version: "1.6"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -36,6 +36,8 @@ When `MCP_AUTH_MODE=none`, auth checks are skipped and defaults are allowed.
36
36
 
37
37
  A failed check returns `Forbidden` (-32005, `Insufficient permissions.`) or, when auth is enabled but the request carries no auth context, `Unauthorized` (-32006). Neither carries `data`: the required, granted, and missing scope names stay in the server log, so a caller cannot enumerate scopes from the error.
38
38
 
39
+ The scope check logs a missing scope at `warning`, naming the missing scopes. On a tool, the call's `Error in tool:<name>` record follows at `notice` with no stack — the refusal is the caller's standing, not a fault in this server — and `mcp.errors.classified` counts it with `mcp.error.severity: "notice"`. That holds for the inline check and for `checkScopes` in a handler alike, and the level is fixed: the refusal carries no `data.reason`, so no `errors[]` entry can set it. A missing auth context keeps its `Error in tool:` record at `error`, and so does a `forbidden()` the handler throws itself. A resource read the inline check refuses writes only the scope check's own records.
40
+
39
41
  ---
40
42
 
41
43
  ## Dynamic auth
@@ -4,7 +4,7 @@ description: >
4
4
  DataCanvas primitive reference — a Tier 3 SQL/analytical workspace for tabular MCP servers, backed by DuckDB. Use when registering tables from upstream APIs, running ad-hoc SQL across them, and exporting results. Covers the acquire → register → query → export flow, per-table TTL, the token-sharing pattern for multi-agent collaboration, env config, and Cloudflare Workers fail-closed behavior.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.6"
7
+ version: "2.7"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -329,7 +329,7 @@ Most canvas use cases are public-data analytics: fetch from an upstream API, sta
329
329
  | Table naming | `spillover()` auto-names the table `spilled_<id>`; pass `tableName` for a stable handle. A dataframe-query surface commonly adds its own `df_<id>` convention. |
330
330
  | Access control | Possession of the `canvas_id` is access — unguessable in practice (see [token-sharing model](#the-token-sharing-model)). TTL + the framework rate limiter backstop brute force. |
331
331
  | Enable flag | None of your own — canvas presence is the gate (`CANVAS_PROVIDER_TYPE=duckdb`; `getCanvas()` returns `undefined` otherwise). |
332
- | Tools | A fetcher that spills **plus the dataframe trio — all three ship whenever canvas is integrated**. `dataframe_query` is mandatory once anything emits a `canvas_id`: a token with no query tool in the same server is dead output (the agent can't reach the staged data). `dataframe_describe` is required alongside it — the agent discovers staged table and column names before writing SQL. `dataframe_drop` is implemented but **opt-in via a server env var**: when the flag is off, register it with `disabledTool()` (see `add-tool`) so it stays visible in the manifest with the enable hint while uncallable. None are framework-provided; you register them. |
332
+ | Tools | A fetcher that spills **plus the dataframe trio — all three ship whenever canvas is integrated**. `dataframe_query` is mandatory once anything emits a `canvas_id`: a token with no query tool in the same server is dead output (the agent can't reach the staged data). `dataframe_describe` is required alongside it — the agent discovers staged table and column names before writing SQL. `dataframe_drop` is implemented but **opt-in via a server env var**: when the flag is off, register it with `disabledTool()` (see `add-tool`) so it stays visible in the manifest with the enable hint while uncallable. `acquire()`'s `canvas_not_found` already carries a re-stage hint, which a declared contract recovery never overrides — right for describe/query, wrong for drop (a missing canvas has nothing left to remove). The drop handler catches that reason and re-raises it through its own `ctx.fail('canvas_not_found', …, { cause })`, leaving the contract entry unmarked by `thrownBy: 'service'`. None are framework-provided; you register them. |
333
333
  | Fetcher output | Two things in one response: the inline preview (answer to the immediate question) and the table handle (escape hatch for follow-up SQL via `dataframe_query`). Neither replaces the other. |
334
334
 
335
335
  > The `MCP_HTTP_MAX_BODY_BYTES` request-body cap is **inbound-only** — it bounds the JSON-RPC request, not the upstream data a handler stages into the canvas or the rows it returns. Canvas servers send small requests (queries, SQL, canvas IDs) regardless of dataset size, so the cap never constrains canvas ingestion.
@@ -4,7 +4,7 @@ description: >
4
4
  Reference for core and server configuration in `@cyanheads/mcp-ts-core`. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.24"
7
+ version: "1.25"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -96,6 +96,7 @@ await createApp({ sessionMode: { default: 'stateful', require: 'stateful' } });
96
96
  | `LOGS_DIR` | `logsPath` | `<app-root>/logs` | Node.js only; absolute paths are used verbatim, relative ones resolve against the application root (see Core config) — never the framework's install directory. A file under it that cannot be opened (read-only mount, another user's directory) is dropped at startup with one `warning` naming it and the error code; stderr and the other files keep logging |
97
97
  | `LOG_TOOL_FAILURE_PAYLOADS` | `logToolFailurePayloads` | `false` | Opt-in. Each failed tool call also writes a `Tool failure payload: <tool>` record carrying `toolInput` (the arguments as sent) and `toolResult` (the `CallToolResult` returned) as redacted JSON strings, at the call's error-record level. Reaches every log destination — stderr, `combined.log`, and OTLP when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` is set. Redaction is by key name only, so a secret inside a free-form value (a query, a message) is logged. Record shape: `api-telemetry` Logs |
98
98
  | `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` | `logToolFailurePayloadMaxBytes` | `16384` | Cap per payload, in UTF-8 bytes. A longer one is cut on a character boundary and flagged with `toolInputTruncated` / `toolResultTruncated` |
99
+ | `LOG_LLM_INTERACTIONS` | `logLlmInteractions` | `false` | Opt-in. The OpenRouter provider (`/services`) writes each chat completion's full request and response bodies to `interactions.log` instead of metadata (model, message count, generation params, finish reasons, token usage). Transcripts can carry user PII, secrets, and confidential prompts, and redaction is by key name only. `interactions.log` sits under `LOGS_DIR` (Node.js only) and is never exported over OTLP |
99
100
 
100
101
  ### Transport
101
102
 
@@ -4,7 +4,7 @@ description: >
4
4
  Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, its `RequestContext` base, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.requestInput`, `ctx.inputs`, `ctx.clientCapabilities`, `ctx.enrich`, `ctx.content`), and when to use each.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.10"
7
+ version: "2.12"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -87,7 +87,7 @@ interface Context extends RequestContext {
87
87
 
88
88
  | Field | Always present | Source |
89
89
  |:------|:--------------|:-------|
90
- | `requestId` | Yes | The client's JSON-RPC id when it is a string, otherwise a generated `XXXXX-XXXXX` token. Every log record of the call carries it, and the framework returns it on the call's error envelope as `data.requestId` |
90
+ | `requestId` | Yes | A generated `XXXXX-XXXXX` token, one per call — never the client's JSON-RPC id, which the call's log records carry as `jsonRpcId` instead. Every log record of the call carries it, and the framework returns it on the call's error envelope as `data.requestId`. A 2025-era `ctx.requestInput` round trip re-enters the handler under a new token; the shared `jsonRpcId` ties the entries together |
91
91
  | `timestamp` | Yes | ISO 8601, request start |
92
92
  | `tenantId` | Stdio and HTTP+`MCP_AUTH_MODE=none` (as `'default'`); JWT `tid` claim in HTTP+`jwt`/`oauth` | JWT / single-tenant default |
93
93
  | `sessionId` | HTTP `stateful` / `auto` mode; undefined for stdio and stateless HTTP unless opted in | `Mcp-Session-Id` header (or server-minted) — see [§ `ctx.sessionId`](#ctxsessionid) |
@@ -122,11 +122,11 @@ Three supported ways, most common first:
122
122
 
123
123
  ```ts
124
124
  // 1. Per-log-call metadata — the common case. Nothing lands on the context.
125
- ctx.log.info('Retrying upstream call', { attempt, url });
125
+ ctx.log.info('Retrying upstream call', { attempt, endpoint: 'search' });
126
126
 
127
127
  // 2. A copy of this context carrying extra fields. `withExtra` MERGES into any
128
128
  // bag the parent already had; a hand-written `{ ...ctx, extra: {…} }` replaces it.
129
- logger.warning('Retrying upstream call', withExtra(ctx, { attempt, url }));
129
+ logger.warning('Retrying upstream call', withExtra(ctx, { attempt, endpoint: 'search' }));
130
130
 
131
131
  // 3. A derived context for a sub-operation. `additionalContext` lands on `extra`,
132
132
  // merged over whatever the parent already carried.
@@ -154,7 +154,7 @@ Never re-open the shape to get past a type error: no index signature, no widenin
154
154
 
155
155
  Request-scoped structured logger. Every log line is automatically annotated with `requestId`, `traceId`, and `tenantId` — no manual spreading needed.
156
156
 
157
- **Dual-sink.** Each call writes to Pino *and* mirrors onto the MCP wire as a `notifications/message` (the framework advertises the `logging` capability). One level check gates both sinks: `MCP_LOG_LEVEL` (or a runtime `logger.setLevel()`) is a floor for the client stream as well as the process log, compared on the RFC 5424 order, so a `notice` floor drops `info` from both. The SDK then filters by the client's own level — `logging/setLevel`, or the `io.modelcontextprotocol/logLevel` a 2026-07-28 request carries — which can only narrow the floor, never widen it. The wire payload is `{ message, ...data }` with every sensitive field masked as `[REDACTED]` at any depth: the same field list the logs are redacted with, extensible through `sanitization.setSensitiveFields`, matched as `sanitizeForLogging` matches it — case- and separator-insensitive (`API_KEY` is `apiKey`), and on any one word of a compound name, so `accessToken` (and `tokenCount`) are masked. The caller's `data` object is never modified. `ctx.log.error` adds `error: <message>`. `message` and `error` are reserved wire keys, written after `data`: a `message` in `data` never replaces the log line on the wire, and on `ctx.log.error` with an `Error` the `error` key is always that error's message. The process log line still carries the caller's own fields, except one reusing a canonical name the context already sets (`requestId`, `traceId`, `spanId`, `tenantId`, …) — there the context's value wins, so the line stays correlated to its request. Delivery is fire-and-forget — a client that never upgraded to SSE, set a higher level, or already disconnected drops the notification, and a failed send never fails the handler. Treat `ctx.log` as client-visible: it is no longer a server-only sink, so don't log anything there you wouldn't put in a tool result.
157
+ **Dual-sink.** Each call writes to Pino *and* mirrors onto the MCP wire as a `notifications/message` (the framework advertises the `logging` capability). One level check gates both sinks: `MCP_LOG_LEVEL` (or a runtime `logger.setLevel()`) is a floor for the client stream as well as the process log, compared on the RFC 5424 order, so a `notice` floor drops `info` from both. The SDK then filters by the client's own level — `logging/setLevel`, or the `io.modelcontextprotocol/logLevel` a 2026-07-28 request carries — which can only narrow the floor, never widen it. The wire payload is `{ message, ...data }` with every sensitive field masked as `[REDACTED]` at any depth: the same field list and matcher the logs are redacted with, extensible through `sanitization.setSensitiveFields`. A key is masked when some run of its adjacent words, joined, equals a sensitive name, case and separators ignored — words split at every character other than a letter or digit, at a lowercase letter followed by a capital, at the end of a run of capitals, and around each run of digits — so `API_KEY`, `x-api-key`, `accessToken`, `apiKey2`, and `tokenCount` are masked while `max_tokens`, `MAX_TOKENS`, and `tokenizer` are not. An `Error` in `data`, at any depth, goes on the wire as `{ type, message }` only — no stack, cause, or other own property such as a request URL; the process log writes it in full (`api-telemetry` Logs). The mirror keeps the process log's bounds: objects through 15 levels below the data root, one 16 levels down as `'[MaxDepth]'`, repeated content (an object or `toJSON()` result reached again, or a string of 1,024+ characters written again) charged about its written size against 1,000,000 characters and cut with `'[Truncated]'`, at most 400,000 reads a walk, one per object, field, and array element (so data a getter or Proxy builds on every read is cut too), at most 16 MiB (16,777,216 characters) of strings, field names, and primitives a walk writes, repeated or not, then `'[Truncated]'` and nothing more, a reference back to an enclosing object as `'[Circular]'`, and a value whose read throws (a getter, a Proxy trap, a `toJSON`) as `'[Unreadable]'`, with `data` that cannot be read at all (a revoked Proxy) written as `data: '[Unreadable]'` on both sinks — so no value in `data` can stall the handler or fail it. The caller's `data` object is never modified. `ctx.log.error` adds `error: <message>`. `message` and `error` are reserved wire keys, written after `data`: a `message` in `data` never replaces the log line on the wire, and on `ctx.log.error` with an `Error` the `error` key is always that error's message as a string — a Symbol as `'Symbol(…)'`, a number as its digits, and `'[Unreadable]'` when reading it throws or it is an object — and the notification is still sent. The process log line still carries the caller's own fields, except one reusing a canonical name the context already sets (`requestId`, `traceId`, `spanId`, `tenantId`, …) — there the context's value wins, so the line stays correlated to its request — and one named after a field the logger writes on the line itself (`level`, `time`, `msg`, `env`, `version`, `pid`, `hostname`, and `err` on `ctx.log.error` with an `Error`), which is written as `data_<name>` so the line keeps its own `level` and `version`. Delivery is fire-and-forget — a client that never upgraded to SSE, set a higher level, or already disconnected drops the notification, and a failed send never fails the handler. Treat `ctx.log` as client-visible: it is no longer a server-only sink, so don't log anything there you wouldn't put in a tool result.
158
158
 
159
159
  ### Methods
160
160
 
@@ -332,7 +332,7 @@ Always present, on every transport and both protocol eras. A handler that needs
332
332
 
333
333
  One code path serves both eras. A 2026-07-28 client fulfils the embedded requests and retries the call; for a 2025-era session the SDK's legacy shim fulfils the same returns by issuing real `elicitation/create` / `sampling/createMessage` / `roots/list` round trips and re-entering the handler itself.
334
334
 
335
- **A 2025-era client that declared no matching capability is refused, with an envelope.** URL-mode elicitation needs `elicitation.url`, form-mode needs `elicitation.form` (a bare `elicitation: {}` satisfies it), sampling needs `sampling` — `sampling.tools` when the request carries `tools` / `toolChoice` — and `roots/list` needs `roots`. `ctx.requestInput` runs the check on the result it builds and throws the refusal instead of the signal, so it never reaches the wire and the handler fails where it stands — the execution measurement records it as a failed call, its `Error in tool:<name>` record logs at `notice` (a property of the client's connection, not a server fault), and each family's usual error path shapes it. A tool gets `isError` with `structuredContent.error.code = -32600` (`InvalidRequest`), `data.reason: 'client_capability_missing'`, and a `data.recovery.hint` naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. A prompt's `generate` receives no `ctx`, so it has no `ctx.requestInput` to gate. The check runs on every round, so a handler that elicits first and samples second is gated again on the second. A return carrying only `requestState` asks the client for nothing and is never gated. On the 2026-07-28 leg the SDK owns this check and a violation surfaces as its `MissingRequiredClientCapabilityError` (`-32021`) instead.
335
+ **A 2025-era client that declared no matching capability is refused, with an envelope.** URL-mode elicitation needs `elicitation.url`, form-mode needs `elicitation.form` (a bare `elicitation: {}` satisfies it), sampling needs `sampling` — `sampling.tools` when the request carries `tools` / `toolChoice` — and `roots/list` needs `roots`. `ctx.requestInput` runs the check on the result it builds and throws the refusal instead of the signal, so it never reaches the wire and the handler fails where it stands — the execution measurement records it as a failed call, its `Error in tool:<name>` record logs at `notice` with no stack (a property of the client's connection, not a server fault), and each family's usual error path shapes it. A tool gets `isError` with `structuredContent.error.code = -32600` (`InvalidRequest`), `data.reason: 'client_capability_missing'`, and a `data.recovery.hint` naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. A prompt's `generate` receives no `ctx`, so it has no `ctx.requestInput` to gate. The check runs on every round, so a handler that elicits first and samples second is gated again on the second. A return carrying only `requestState` asks the client for nothing and is never gated. On the 2026-07-28 leg the SDK owns this check and a violation surfaces as its `MissingRequiredClientCapabilityError` (`-32021`) instead.
336
336
 
337
337
  **The refusal's hint ends at reconnecting** — ``Reconnect with a client that declares the `elicitation.form` capability.`` — and offers no other way to supply the answer, because a consent gate deliberately has no input field for it: the model would fill it in. A handler whose own arguments can stand in for the answer says so per call with the optional second argument, a sentence appended to the hint after a space:
338
338
 
@@ -4,7 +4,7 @@ description: >
4
4
  McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.19"
7
+ version: "1.21"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -63,7 +63,7 @@ export const fetchTool = tool('fetch_articles', {
63
63
  | Surface | Behavior |
64
64
  |:--------|:---------|
65
65
  | Compile time | `ctx.fail('typo')` is a TS error. Auto-completes declared reasons. |
66
- | Runtime | `ctx.fail(reason, msg?, data?, options?)` builds an `McpError(contract.code, msg, { ...data, reason }, options)` — `data.reason` is auto-populated from the contract and cannot be overridden by caller-supplied data (spread first, then `reason` written last), so observers see a stable identifier. `options` accepts `{ cause }` for ES2022 error chaining. |
66
+ | Runtime | `ctx.fail(reason, msg?, data?, options?)` builds an `McpError(contract.code, msg, { ...data, reason }, options)` — `data.reason` is auto-populated from the contract and cannot be overridden by caller-supplied data (spread first, then `reason` written last), so observers see a stable identifier. `options` accepts `{ cause }` for ES2022 error chaining. Its stack starts at the line that called `ctx.fail`, with the framework's own frame cut, as an error factory's does. |
67
67
  | Runtime (recovery) | A failure whose `data.reason` names a declared entry and carries no `data.recovery` gets `data.recovery.hint` set to the entry's `recovery` at the handler boundary — see below. |
68
68
  | Lint (devcheck) | Each `code` validated against `JsonRpcErrorCode`. Reasons validated as snake_case + unique within contract. `recovery` validated as non-empty and ≥ 5 words. Build-time only — not invoked at server startup. |
69
69
  | Lint (conformance) | If the handler `throw new McpError(JsonRpcErrorCode.X)` outside `ctx.fail`, conformance check warns when X isn't declared. The inverse is checked too: a declared reason no `ctx.fail` in the handler names warns as `error-contract-unthrown` (mark it `thrownBy: 'service'` when the service layer produces it). |
@@ -134,14 +134,14 @@ Values are the logger's own level names below `error` — `debug`, `info`, `noti
134
134
 
135
135
  | Surface | Under a declared severity |
136
136
  |:--------|:--------------------------|
137
- | The `Error in tool:<name>` log record | Emitted at the declared level. Same message, same structured fields, the stack included. |
137
+ | The `Error in tool:<name>` log record | Emitted at the declared level. Same message, same structured fields, the throw site's stack included — except the framework's own refusals, whose records carry no stack, an argument rejection's bounded as well (see below). |
138
138
  | `mcp.errors.classified` | Gains an `mcp.error.severity` attribute. The `reason` itself never becomes a metric attribute. |
139
139
  | `isError`, the JSON-RPC code, `structuredContent.error`, `content[]` | Byte-identical to the undeclared case. |
140
140
  | Span status, `mcp.tool.calls`, `mcp.tool.duration`, `mcp.tool.errors` | Unchanged — the call still failed, and splitting those series would redefine what an error rate means. |
141
141
 
142
142
  **Tools only.** Resolution happens in the tool handler factory, against the thrown error's `data.reason` — the same reason-to-entry lookup that fills `data.recovery`. Resources declare `errors[]` but write no failure record, so the field is accepted there and inert. A reason thrown below the handler that the contract never declared, an entry with no `severity`, and a non-`McpError` throw all keep `error`. A cancelled request keeps its own `info`, stack-free path regardless.
143
143
 
144
- **The framework's own refusals log at `notice`.** An argument rejection (`invalid_arguments`, raised only by the schema gate before the handler runs) and a `ctx.requestInput` the connection cannot serve (`client_capability_missing`) are routine caller or connection traffic, not server faults, so their `Error in tool:<name>` record — and the failure-payload record when `LOG_TOOL_FAILURE_PAYLOADS=true` — is emitted at `notice`, and `mcp.errors.classified` counts them with `mcp.error.severity: "notice"`. Nothing to declare; an `errors[]` entry naming either reason with its own `severity` still wins. The wire envelope and `mcp.tool.rejections` are unchanged, and a schema that wrongly rejects valid calls still shows per tool on `mcp.tool.rejections`.
144
+ **The framework's own refusals log at `notice`.** An argument rejection (`invalid_arguments`, raised only by the schema gate before the handler runs), a `ctx.requestInput` the connection cannot serve (`client_capability_missing`), and a missing-scope refusal (the `Forbidden` the inline `auth` check or `checkScopes` throws) are routine caller or connection traffic, not server faults, so their `Error in tool:<name>` record — and the failure-payload record when `LOG_TOOL_FAILURE_PAYLOADS=true` — is emitted at `notice`, and `mcp.errors.classified` counts them with `mcp.error.severity: "notice"`. Nothing to declare; an `errors[]` entry naming `invalid_arguments` or `client_capability_missing` with its own `severity` still wins, while a missing-scope refusal carries no `data.reason`, so its level is fixed. That refusal is recognized by where it was raised, never by its code: a handler's own `forbidden()`, an upstream 403 mapped by `httpErrorFromResponse`, and a missing auth context (`Unauthorized`) keep `error` and the stack. All three records log no stack, whatever level an entry declares, and an argument rejection's record is bounded whatever the caller sends: the message, `recovery.hint`, each issue's `message`, each `data.input` key, and every other string keep at most their first 1,024 characters and every array its first 10 entries, with the uncut length or count beside each cut (`originalMessageLength`, `<field>Length`, `<field>Count`, `<field>Lengths`). The wire envelope and `mcp.tool.rejections` are unchanged — the `-32602` result still carries every key and issue whole — and a schema that wrongly rejects valid calls still shows per tool on `mcp.tool.rejections`.
145
145
 
146
146
  **Skip the contract** for one-off internal tools or quick prototypes — `ctx` is plain `Context` (no `fail`) and you throw via [factories](#error-factories-fallback) directly. Behavior is identical at the wire; the contract just adds compile-time safety.
147
147
 
@@ -198,7 +198,7 @@ A best-effort call that catches and degrades must still rethrow on `ctx.signal?.
198
198
 
199
199
  ## Error Factories (fallback)
200
200
 
201
- Use when no contract entry fits — ad-hoc throws, tools without a contract, or service-layer code. Shorter than `new McpError(...)` and self-documenting. All return `McpError` instances and accept an optional `options` parameter for error chaining via `{ cause }`.
201
+ Use when no contract entry fits — ad-hoc throws, tools without a contract, or service-layer code. Shorter than `new McpError(...)` and self-documenting. All return `McpError` instances and accept an optional `options` parameter for error chaining via `{ cause }`. Each one's stack starts at the line that called it, with the factory's own frame cut.
202
202
 
203
203
  ```ts
204
204
  throw notFound('Item not found', { itemId: '123' });
@@ -206,7 +206,7 @@ throw validationError('Missing required field: name', { field: 'name' });
206
206
  throw unauthorized('Token expired');
207
207
 
208
208
  // With cause for error chaining
209
- throw serviceUnavailable('API call failed', { url }, { cause: error });
209
+ throw serviceUnavailable('API call failed', { endpoint: 'search' }, { cause: error });
210
210
  ```
211
211
 
212
212
  **Available factories:**
@@ -243,7 +243,7 @@ throw new McpError(code, message?, data?, options?)
243
243
 
244
244
  - `code` — a `JsonRpcErrorCode` enum value
245
245
  - `message` — optional human-readable description of the failure
246
- - `data` — optional structured data (plain object), returned to the client verbatim. Pass the explicit fields the caller acts on (the rejected key, a limit, a `reason`), never `ctx` or another request context: a handler `ctx` carries request metadata and, after an elicitation round, what the user typed. Framework helpers follow the same rule — a storage, parser, or formatter failure carries only its offending field or a `reason`, whatever context you pass them.
246
+ - `data` — optional structured data (plain object), returned to the client verbatim. Pass the explicit fields the caller acts on (the rejected key, a limit, a `reason`), never `ctx` or another request context: a handler `ctx` carries request metadata and, after an elicitation round, what the user typed. Framework helpers follow the same rule — a storage, parser, or formatter failure carries only its offending field or a `reason`, whatever context you pass them. A `filesystem` storage fault names the key, never the host path: `DatabaseError`, or `ValidationError` when a key segment is too long for the filesystem, with the raw `fs` error on `cause` for the log.
247
247
  - `options` — optional `{ cause?: unknown }` for error chaining
248
248
 
249
249
  **Example:**
@@ -311,7 +311,7 @@ The framework applies these steps in order — first match wins:
311
311
  8. **`AbortError` name** — `error.name === 'AbortError'` → `Timeout`.
312
312
  9. **Fallback** — `InternalError`.
313
313
 
314
- However it is reached, a `RequestCancelled` is logged at `info` with no stack — neither the thrown value's own nor one reached through its cause chain. Step 1 settles the completion log too, which carries `metrics.errorCode: "-32011"` alongside `isSuccess: false`; a raw `SdkError` that reaches the code through step 3 alone is not an `McpError`, so that log still reads `UNHANDLED_ERROR`.
314
+ However it is reached, a `RequestCancelled` is logged at `info` with no stack — neither the thrown value's own, nor one reached through its cause chain, nor the `originalStack` or `causeChain` node stacks the thrown `McpError`'s `data` carries, nor an `Error`'s anywhere in the record (`errorData`, `input`, the context's `extra`). Step 1 settles the completion log too, which carries `metrics.errorCode: "-32011"` alongside `isSuccess: false`; a raw `SdkError` that reaches the code through step 3 alone is not an `McpError`, so that log still reads `UNHANDLED_ERROR`.
315
315
 
316
316
  The code this ladder picks is the one the caller receives, and it is also the origin every error counter records: `mcp.tool.error_category`, `mcp.prompt.error_category`, and `mcp.error.category` on `mcp.errors.classified` all bucket that same code, so a plain `Error('Request timed out')` files as `upstream` everywhere, never `server` on one counter and `upstream` on another. See `api-telemetry`'s Error category.
317
317
 
@@ -407,12 +407,12 @@ Recovery: Use pubmed_search_articles to discover valid PMIDs.
407
407
  Important properties:
408
408
  - **`_meta.error` is NOT emitted.** Error code/data live on `structuredContent.error` instead. Don't read `_meta.error` in clients or tests — it doesn't exist.
409
409
  - **`data` propagation is restricted** to explicitly-thrown `McpError.data`, `ZodError.issues`, and the request id. Auto-classified plain errors (`TypeError`, network errors, etc.) emit `code`, `message`, and `data: { requestId }` only, so internal classification context never leaks to clients.
410
- - **`data.requestId` names the request.** The framework sets it on every error envelope it builds — a tool result (handler throws, argument rejections, auth refusals, output-contract failures), a failed resource read, a failed prompt, and the JSON-RPC errors `httpErrorHandler` returns — to the `requestId` that call's log records carry. On a tool, resource, or prompt call that is a generated `XXXXX-XXXXX` token, or the client's JSON-RPC id when that id is a string; `httpErrorHandler` generates its own token, the one on its `Client error:` record. A failure reported from the client resolves to its `Error in tool:<name>` record by that value. A resource read refused before it is measured (an auth refusal, or URI variables that fail `params`) carries an id no log record shares, since resources write no failure record of their own. It is added where the envelope is built, never to the thrown `McpError.data`, so `ErrorHandler.handleError` / `tryCatch` results and the log record's `errorData` stay context-free; it replaces a thrown `data.requestId`, the way canonical fields win in log records. Two envelopes go without it: a resource `-32602` whose `data` is exactly `{ uri }` (the resource-not-found shape clients match exactly), and `runToolContract` results, which have no real request. It closes the `content[]` terms line, alone as `(request <id>)` when there is no `reason` or `retryable` — so a test pinning `content[0].text` exactly sees it.
410
+ - **`data.requestId` names the request.** The framework sets it on every error envelope it builds — a tool result (handler throws, argument rejections, auth refusals, output-contract failures), a failed resource read, a failed prompt, and the JSON-RPC errors `httpErrorHandler` returns — to the `requestId` that call's log records carry. It is always a generated `XXXXX-XXXXX` token, one per call — never the client's JSON-RPC id, which the call's records carry as `jsonRpcId` and the response keeps as its `id`. `httpErrorHandler`'s is the token on its `Client error:` record. A failure reported from the client resolves to its `Error in tool:<name>` record by that value. A resource read refused before it is measured (an auth refusal, or URI variables that fail `params`) carries an id no log record shares, since resources write no failure record of their own. It is added where the envelope is built, never to the thrown `McpError.data`, so `ErrorHandler.handleError` / `tryCatch` results and the log record's `errorData` stay context-free; it replaces a thrown `data.requestId`, the way canonical fields win in log records. Two envelopes go without it: a resource `-32602` whose `data` is exactly `{ uri }` (the resource-not-found shape clients match exactly), and `runToolContract` results, which have no real request. It closes the `content[]` terms line, alone as `(request <id>)` when there is no `reason` or `retryable` — so a test pinning `content[0].text` exactly sees it.
411
411
  - **Recovery hint mirroring is automatic, unless the hint repeats the message.** When the thrown `McpError` carries `data.recovery.hint`, the handler factory appends it to the `content[]` text so the markdown surface matches the JSON surface. Authors don't need to format the hint manually. The one exception is a hint the trimmed message already contains verbatim (case-sensitively) — an argument rejection whose every hint sentence restates an issue, where the hint is the message's issue text verbatim, and an author hint that restates its own message. There the line adds no next step, so it is dropped from the text; `structuredContent.error.data.recovery.hint` stays populated either way.
412
412
  - **`reason`, `retryable`, and `requestId` render as a trailing term line.** `(reason malformed_id · not retryable · request UTFAC-QE0MB)` closes the text whenever `data.reason` is a non-empty string, `data.retryable` is a boolean, or `data.requestId` is a non-empty string — `retryable` for `true`, `not retryable` for `false`, in that order. None present (an `McpError` with no `data` built outside a request, as `runToolContract` does) appends nothing at all. The numeric `code` and `data.issues` stay JSON-only on purpose: the code is the one envelope field a model cannot act on, and the message already renders each issue as a sentence. A consumer test pinning `content[0].text` exactly, rather than asserting it contains the diagnostic, therefore moves for any error carrying a reason.
413
413
  - **Argument-schema rejection is a tool error with the same envelope.** An unknown root key, a wrong type, a missing required field, or a failed constraint returns `isError: true` with `structuredContent.error.code = -32602` (`InvalidParams`) and the readable `Invalid arguments for tool <name>: …` diagnostic in `content[]`. The handler never runs. Two neighbouring failures keep the protocol error path instead, arriving as a JSON-RPC error rather than a tool result: an unknown or disabled tool name, and a malformed request envelope.
414
- - **`invalid_arguments` is the framework-owned reason on every argument rejection.** The rejection carries `data.reason: "invalid_arguments"` and a `data.recovery.hint` the framework synthesizes from the Zod issues, the arguments as sent, and the root schema — an unknown root key names the root properties the tool does accept, an unknown key inside a nested strict object names its full path and that object's own keys (`Unknown key opts.b. opts accepts: a.`, `Unknown key items.1.b. items.1 accepts: name.`), a wrong type names the type to send instead (a fractional number on an integer field reads `Send rows as an integer, not a fractional number.`), missing fields collapse into one `Provide …` sentence, and anything else restates its diagnostic line, field path included (`start: Must be a parseable ISO 8601 date`), so identical constraints on different fields stay distinguishable. When every sentence restates an issue, the hint is the message's issue text verbatim, and its `Recovery:` line is dropped from `content[]`. Beside a framework sentence each restatement is terminated and a repeated sentence appears once: `start: Must be a parseable ISO 8601 date. Send n as a number, not a string.` When pre-validation rewrote or dropped a key the caller wrote, the rejection says so, since its issues name only the keys that were validated: `data.input` carries `{ aliased: [{ alias, target }], ignored: [...] }` — keys only, in argument order, `ignored` holding the undeclared underscore-prefixed keys the drop discarded — and the hint closes with `Validated query as targetQuery.` / `Dropped undeclared key _max.`, framework sentences that keep the `Recovery:` line. An ignore-list drop (`_meta`, `toolCallId`, a server's `input.ignoreKeys`) is a client artifact and is never reported, and a rejection the step changed nothing on carries no `data.input`. The reason renders on the closing `(reason invalid_arguments · request <id>)`; this path sets no `retryable`. Its `Error in tool:<name>` record logs at `notice`, not `error`. Authors declare nothing for this: the rejection happens before the handler and the hint is derived from the schema.
415
- - **`client_capability_missing` is the other framework-owned reason.** When a handler returns `ctx.requestInput({ inputRequests: … })` on a 2025-era connection whose client declared no matching capability, `ctx.requestInput` throws this failure in place of the input-required signal, before anything reaches the wire. It is an ordinary handler throw from there on, logged at `notice`: measured as the failed call it is, and shaped by the family's usual error path — a tool gets `structuredContent.error.code = -32600` (`InvalidRequest`), `data.reason: "client_capability_missing"`, and a `data.recovery.hint` naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. The hint ends at reconnecting with a client that declares the capability, since a consent gate has no argument that could stand in for its answer; a handler whose arguments can appends its own sentence per call with `ctx.requestInput(spec, { fallbackHint })`. Like `invalid_arguments`, a definition cannot declare it in `errors[]`: it names a property of the connection, not a domain outcome. See `api-context`'s `ctx.requestInput`.
414
+ - **`invalid_arguments` is the framework-owned reason on every argument rejection.** The rejection carries `data.reason: "invalid_arguments"` and a `data.recovery.hint` the framework synthesizes from the Zod issues, the arguments as sent, and the root schema — an unknown root key names the root properties the tool does accept, an unknown key inside a nested strict object names its full path and that object's own keys (`Unknown key opts.b. opts accepts: a.`, `Unknown key items.1.b. items.1 accepts: name.`), a wrong type names the type to send instead (a fractional number on an integer field reads `Send rows as an integer, not a fractional number.`), missing fields collapse into one `Provide …` sentence, and anything else restates its diagnostic line, field path included (`start: Must be a parseable ISO 8601 date`), so identical constraints on different fields stay distinguishable. When every sentence restates an issue, the hint is the message's issue text verbatim, and its `Recovery:` line is dropped from `content[]`. Beside a framework sentence each restatement is terminated and a repeated sentence appears once: `start: Must be a parseable ISO 8601 date. Send n as a number, not a string.` When pre-validation rewrote or dropped a key the caller wrote, the rejection says so, since its issues name only the keys that were validated: `data.input` carries `{ aliased: [{ alias, target }], ignored: [...] }` — keys only, in argument order, `ignored` holding the undeclared underscore-prefixed keys the drop discarded — and the hint closes with `Validated query as targetQuery.` / `Dropped undeclared key _max.`, framework sentences that keep the `Recovery:` line. An ignore-list drop (`_meta`, `toolCallId`, a server's `input.ignoreKeys`) is a client artifact and is never reported, and a rejection the step changed nothing on carries no `data.input`. The reason renders on the closing `(reason invalid_arguments · request <id>)`; this path sets no `retryable`. Its `Error in tool:<name>` record logs at `notice`, not `error`, with no stack and its caller-sized strings and arrays capped (see `severity` above). Authors declare nothing for this: the rejection happens before the handler and the hint is derived from the schema.
415
+ - **`client_capability_missing` is the other framework-owned reason.** When a handler returns `ctx.requestInput({ inputRequests: … })` on a 2025-era connection whose client declared no matching capability, `ctx.requestInput` throws this failure in place of the input-required signal, before anything reaches the wire. It is an ordinary handler throw from there on, logged at `notice` with no stack: measured as the failed call it is, and shaped by the family's usual error path — a tool gets `structuredContent.error.code = -32600` (`InvalidRequest`), `data.reason: "client_capability_missing"`, and a `data.recovery.hint` naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. The hint ends at reconnecting with a client that declares the capability, since a consent gate has no argument that could stand in for its answer; a handler whose arguments can appends its own sentence per call with `ctx.requestInput(spec, { fallbackHint })`. Like `invalid_arguments`, a definition cannot declare it in `errors[]`: it names a property of the connection, not a domain outcome. See `api-context`'s `ctx.requestInput`.
416
416
  - **A schema constraint cannot carry a *declared* reason.** Because the handler never runs, a rejection by `.max()`, `.regex()`, `.min()`, or any other Zod refinement bypasses `errors[]` entirely: it arrives as `InvalidParams` with `data.issues` under the framework's `invalid_arguments`, never the `reason` and authored `recovery` of a contract entry — so a caller has nothing tool-specific to branch on and gets only the schema-derived hint. Decide per constraint which surface it belongs on. A bound that is purely structural — the input is the wrong shape and no guidance beyond the diagnostic would help — belongs on the schema, where it also advertises itself in `inputSchema`. A bound a caller is expected to recover from belongs in the handler as `ctx.fail('reason', message)` against a declared `errors[]` entry, whose `recovery` the framework puts on the wire, with the limit restated in the field's `.describe()` so it is still visible before the call. Enforcing the same bound in both places is the trap: the schema wins, and the contract entry becomes unreachable while still reading as covered.
417
417
  - **A rejected value never reaches the client.** The rendered sentence distinguishes an omitted field from a wrong one (`what: Missing required field. Expected one of "os"|"cpu"` rather than the invalid-option text), and a union renders the branch that says what would have been accepted instead of Zod's `Invalid input` placeholder. Both read the arguments in-process for the absent/present bit and the arriving type only — `data.issues` ships the Zod issues as-is, and no value the caller sent is copied onto them.
418
418
  - **A union branch names its own field.** Each branch issue is prefixed with the path it names relative to that branch, so two alternatives differing only in which field they require stay distinguishable: `spec: kind: Invalid option: expected one of "x"|"y"; n: Invalid input: expected number, received undefined or other: Invalid input: expected string, received undefined`. Issues *within* one branch join on `; `, across branches on ` or `, and top-level issues on `, ` — three nestings, three separators. A scalar branch carries no path and renders as before. `data.issues` still ships the raw nested Zod issues, and `data.recovery.hint` restates the same line, field path included.
@@ -451,7 +451,7 @@ const result = await ErrorHandler.tryCatch(
451
451
  () => externalApi.fetch(url),
452
452
  {
453
453
  operation: 'ExternalApi.fetch',
454
- context: { url },
454
+ context: { extra: { endpoint: 'fetch' } },
455
455
  errorCode: JsonRpcErrorCode.ServiceUnavailable,
456
456
  },
457
457
  );
@@ -465,20 +465,22 @@ const parsed = await ErrorHandler.tryCatch(
465
465
  );
466
466
  ```
467
467
 
468
- `tryCatch` always logs and rethrows — it never swallows errors. The `fn` argument may be synchronous or return a `Promise`; both are handled via `Promise.resolve(fn())`.
468
+ `tryCatch` always logs and rethrows — it never swallows errors. The `fn` argument may be synchronous or return a `Promise`; both are handled via `Promise.resolve(fn())`. The rethrown `McpError` carries the caught error's stack verbatim, so it starts at the throw site and its first line names the class that was thrown.
469
469
 
470
- **The thrown error's `data` is wire-visible.** A handler that lets it propagate forwards it as `structuredContent.error.data` (tools) or JSON-RPC `error.data` (resources, prompts). It carries the caught `McpError`'s own `data`, `originalErrorName`, `originalMessage`, and `rootCause` (`{ name, message }`) — never a stack and never `context`: `originalStack`, the full `causeChain`, and every `context` field (`requestId`, `sessionId`, `traceId`, `tenantId`, `extra`, …) go to the log record only. A field the caller should act on belongs in the thrown `McpError`'s `data`, not in `context`. (The handler factory adds the call's own `data.requestId` when it builds the envelope; that value never comes from `context` here.)
470
+ **A field that cannot be read is written as `'[Unreadable]'`.** When reading the caught error's `name`, `message`, `stack`, or `cause` throws — a getter, a revoked `Proxy` on `cause` — the record writes that field as `'[Unreadable]'` (the rethrown `McpError` then keeps its own stack), and an unreadable cause ends `causeChain` as a node whose `name` and `message` are both `'[Unreadable]'`. A caught value that cannot be inspected at all, such as a revoked `Proxy`, is handled as a non-Error whose name and message are `'[Unreadable]'`; a caught `McpError` whose `code` cannot be read is classified `InternalError`, and one whose `data` cannot be read or copied (a getter, a revoked `Proxy`) is handled without it, under an `errorMapper` that returns the error it was given too. The record is written and `tryCatch` still throws the rebuilt `McpError`. A tool, resource, or prompt handler that threw such a value — an Error whose `name`, `message`, `stack`, or `cause` cannot be read, an `McpError` whose `code` or `data` cannot be read, a revoked `Proxy` — gets its normal error envelope with `data.requestId`, and its record: the code wherever it can be read, `'[Unreadable]'` for a message that cannot, the thrown `data` left out when it cannot be read, and, for a declared failure, its contract's `recovery` hint. That holds when the value arrives through `withSpan` or through `tryCatch` with an identity `errorMapper`. A `name` or `message` that is not a string is written as text: `String` converts any other primitive (`404` reads `'404'`, a Symbol `'Symbol(…)'`), and an object, whose conversion would run its own `toString`, is `'[Unreadable]'`. Each field of a thrown `McpError`'s `data` that the wire cannot carry — one that throws on read at any depth, a `BigInt`, a cycle, a `toJSON` that throws — is `'[Unreadable]'` on the envelope and in the record, since a response holding it would never be sent and the client would wait; every other field goes out exactly as thrown. `determineErrorCode`, `classifyOnly`, `formatError` (whose `data` is then `{}`), and `mapError` never throw on the value they inspect either.
471
+
472
+ **The thrown error's `data` is wire-visible.** A handler that lets it propagate forwards it as `structuredContent.error.data` (tools) or JSON-RPC `error.data` (resources, prompts). It carries the caught `McpError`'s own `data`, `originalErrorName`, and `originalMessage` — nothing derived from a cause, never a stack, and never `context`: `rootCause` (`{ name, message }` of the deepest cause), the full `causeChain`, the throw-site stack, and every `context` field (`requestId`, `sessionId`, `traceId`, `tenantId`, `extra`, …) go to the log record only. A message redacted at the throw site therefore stays redacted on the wire while the raw error rides `cause` into the log. A field the caller should act on belongs in the thrown `McpError`'s `data`, not in `context`. (The handler factory adds the call's own `data.requestId` when it builds the envelope; that value never comes from `context` here.)
471
473
 
472
474
  **Options** (`Omit<ErrorHandlerOptions, 'rethrow'>`):
473
475
 
474
476
  | Option | Type | Required | Purpose |
475
477
  |:-------|:-----|:--------:|:--------|
476
478
  | `operation` | `string` | Yes | Name logged with the error |
477
- | `context` | `ErrorContext` | No | Structured fields merged into the log record only — never the thrown error's client-visible `data`; `requestId` and `timestamp` receive special treatment |
479
+ | `context` | `ErrorContext` | No | Structured fields merged into the log record only — never the thrown error's client-visible `data`; `requestId` and `timestamp` receive special treatment. The handler's own fields (`errorCode`, the type names, `errorData`, `stack`) lead the record, so the log walk reaches `errorData` before a large `extra` or `input` can spend its bound on what it writes, and no `extra` key replaces one |
478
480
  | `errorCode` | `JsonRpcErrorCode` | No | Code used if the caught error is not already an `McpError` |
479
481
  | `input` | `unknown` | No | Input value sanitized and logged alongside the error |
480
482
  | `critical` | `boolean` | No | Marks the error as critical in logs (default `false`) |
481
- | `includeStack` | `boolean` | No | Include stack trace in log output (default `true`) |
483
+ | `includeStack` | `boolean` | No | Stack traces in the log record (default `true`: the record's `stack` is the throw site's — none for a thrown value without a stack, and never the context's `extra.stack` — and each stack is written once: a `causeChain` node carrying the record's stack, or the same stack as the node before it, is written without it). `false` makes the record stack-free, as a cancellation's always is: no `stack`, no `errorData.originalStack`, no `causeChain` node `stack` or node `data.originalStack`, and every `Error` in the record (`errorData`, `input`, the context's `extra`) written without one — whoever supplied the field, a caught `McpError`'s `data` included. Any other key named `stack` is the caller's data, written as given. The chain itself stays; the thrown error and the span exception are unaffected |
482
484
  | `errorMapper` | `(error: unknown) => Error` | No | Custom transform applied instead of default `McpError` wrapping |
483
485
 
484
486
  ---
@@ -507,7 +509,7 @@ Full status table:
507
509
 
508
510
  | Status | Code |
509
511
  |:-------|:-----|
510
- | 3xx | `InvalidRequest` — reachable under `redirect: 'manual'`, and outside `withRetry`'s transient set since re-issuing returns the same redirect |
512
+ | 3xx | `InvalidRequest` — reachable when not followed (`redirect: 'manual'`, or no `Location`, a 304 included), and outside `withRetry`'s transient set since re-issuing returns the same redirect |
511
513
  | 400 | `InvalidParams` |
512
514
  | 401 | `Unauthorized` |
513
515
  | 402, 403 | `Forbidden` |
@@ -4,7 +4,7 @@ description: >
4
4
  MCP definition linter rules reference. Use when `bun run lint:mcp` or `bun run devcheck` reports a lint error or warning (`format-parity`, `schema-is-object`, `name-format`, `server-json-*`, etc.) and you need to understand the rule, its severity, and how to fix it. Every rule ID the linter emits has an entry in this doc.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.21"
7
+ version: "1.22"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -266,8 +266,9 @@ Evaluated on the emitted schema rather than on the Zod schema, because the two d
266
266
 
267
267
  | What you wrote | What is emitted |
268
268
  |:--|:--|
269
- | `z.enum([1, 2, 3, 4, 5])` — a numeric array handed to a string-only constructor | `{"type": "string", "enum": []}` |
270
- | `z.enum([])` | `{"type": "string", "enum": []}` |
269
+ | `z.enum([1, 2, 3, 4, 5])` — a numeric array handed to a string-only constructor | `{"not": {}}` |
270
+ | `z.enum([])` | `{"not": {}}` |
271
+ | `.meta({ enum: [] })` / `.meta({ oneOf: [] })` / `.meta({ type: [] })` | the empty set as written |
271
272
  | `z.union([])` | `{"anyOf": []}` |
272
273
  | `z.never()` | `{"not": {}}` |
273
274
 
@@ -448,7 +449,7 @@ Also applies to resources and prompts (same rule ID, different `definitionType`)
448
449
 
449
450
  **Severity:** error
450
451
 
451
- Every tool must have a `handler` function (or `taskHandlers` object for task tools). Every resource must have a `handler`. Definitions without handlers can't do anything at runtime.
452
+ Every tool must have a `handler` function. Every resource must have a `handler`. Definitions without handlers can't do anything at runtime.
452
453
 
453
454
  Also applies to resources (same rule ID, different `definitionType`).
454
455
 
@@ -662,7 +663,7 @@ Most of these are mechanical — fix the manifest field named in the diagnostic'
662
663
 
663
664
  ## Landing config rules
664
665
 
665
- Validate the `landing` config passed to `createApp()` (the config object that drives the framework's landing page). Run only when `input.landing` is provided to `validateDefinitions`. All errors — landing config that's structurally broken would render incorrectly on the public page.
666
+ Validate the `landing` config passed to `createApp()` (the config object that drives the framework's landing page). Run only when `input.landing` is provided to `validateDefinitions`. Structural breakage is an error — it would render incorrectly on the public page. Input the page tolerates (extras it drops, an empty override it falls back from, an unconventional env-var name) is a warning.
666
667
 
667
668
  | Rule | Severity | Catches |
668
669
  |:-----|:---------|:--------|
@@ -672,20 +673,20 @@ Validate the `landing` config passed to `createApp()` (the config object that dr
672
673
  | `landing-logo-type` | error | `logo` is present but not a string |
673
674
  | `landing-logo-size` | error | `logo` is too long for inline rendering |
674
675
  | `landing-links-type` | error | `links` is present but not an array |
675
- | `landing-links-count` | error | `links` exceeds the max count |
676
+ | `landing-links-count` | warning | `links` exceeds the max count — extras are dropped |
676
677
  | `landing-link-shape` | error | A `links[]` entry is not a plain object |
677
678
  | `landing-link-href` | error | A link entry's `href` is missing or not a non-empty string |
678
679
  | `landing-link-label` | error | A link entry's `label` is missing or not a non-empty string |
679
680
  | `landing-repo-root-type` | error | `repoRoot` is present but not a string |
680
681
  | `landing-repo-root-shape` | error | `repoRoot` is not a recognized GitHub URL shape |
681
682
  | `landing-env-example-type` | error | `envExample` is present but not a plain object |
682
- | `landing-env-example-count` | error | `envExample` has too many entries |
683
- | `landing-env-example-key` | error | An `envExample` key is empty or invalid |
683
+ | `landing-env-example-count` | warning | `envExample` has too many entries — extras are dropped |
684
+ | `landing-env-example-key` | warning | An `envExample` key is not SCREAMING_SNAKE_CASE |
684
685
  | `landing-env-example-value` | error | An `envExample` value is not a string |
685
686
  | `landing-connect-snippets-type` | error | `connectSnippets` is present but not a plain object |
686
- | `landing-connect-snippets-key` | error | A `connectSnippets` key is empty |
687
+ | `landing-connect-snippets-key` | warning | A `connectSnippets` key is not a recognized tab id — it is dropped |
687
688
  | `landing-connect-snippets-value` | error | A `connectSnippets` value is not a string |
688
- | `landing-connect-snippets-empty` | error | A `connectSnippets` value is an empty string |
689
+ | `landing-connect-snippets-empty` | warning | A `connectSnippets` value is an empty string — the derived snippet is used |
689
690
  | `landing-theme-type` | error | `theme` is present but not a plain object |
690
691
  | `landing-theme-accent` | error | `theme.accent` is present but not a string |
691
692
  | `landing-theme-accent-format` | error | `theme.accent` doesn't match the expected color format |
@@ -4,7 +4,7 @@ description: >
4
4
  Stand up a persistent, self-refreshing local mirror of a bulk upstream dataset with the MirrorService (@cyanheads/mcp-ts-core/mirror). Use when a server wraps a large or slow API and should query a synced local index (embedded SQLite + FTS5) instead of paginating the live API per request.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.4"
7
+ version: "1.5"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -72,6 +72,8 @@ Why they can't merge: during a from-scratch init the records aren't ordered by t
72
72
  | `schema_version` + migration *runner* | Scheduling + init/refresh bootstrap (see below) |
73
73
  | Generic `query()` + the raw-handle escape hatch | Server-specific access paths via the raw handle |
74
74
 
75
+ A store that cannot be opened or initialized (a lock held past `busyTimeoutMs`, an unreadable or corrupted file, a parent directory that cannot be created, a migration that throws) rejects with `DatabaseError` (`-32010`). The caller sees the store's file name and a `data.recovery.hint`, never its directory; the driver or filesystem error stays on `cause` for the log.
76
+
75
77
  ## Querying
76
78
 
77
79
  `query({ match?, filters?, sort?, limit, offset })` covers the common case:
@@ -4,7 +4,7 @@ description: >
4
4
  Catalog of OpenTelemetry instrumentation built into framework `@cyanheads/mcp-ts-core` — spans, metrics, completion logs, env config, runtime caveats, custom instrumentation patterns, and cardinality rules. Use when enabling OTel export, adding custom spans or metrics in services, debugging missing telemetry, looking up attribute names, or deciding what's safe to put on a metric attribute vs. a span.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.17"
7
+ version: "1.19"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -41,7 +41,7 @@ Metrics push via `PeriodicExportingMetricReader` every **15 seconds**. Traces us
41
41
 
42
42
  Traces and metrics endpoints resolve per the [OTLP exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#endpoint-urls-for-otlphttp): the signal-specific variable as-is, else the base plus the signal path. A signal with no resolved endpoint exports nothing, and `NodeSDK`'s own `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` defaults are not consulted.
43
43
 
44
- Log records export only when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` is set. The base endpoint alone never turns it on, so a deployment exporting traces and metrics keeps its logs local until it opts in. When set, every record the framework logger writes — after the `MCP_LOG_LEVEL` filter and the rate limit, with the same field redaction as the pino output — is also sent through a `BatchLogRecordProcessor`, with its MCP level as the severity and the active span's trace context (a handler's `ctx.log` record joins its `tool_execution:*` span). `interactions.log` transcripts are never exported. Log export needs three more optional peers: `bun add @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/api-logs`.
44
+ Log records export only when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` is set. The base endpoint alone never turns it on, so a deployment exporting traces and metrics keeps its logs local until it opts in. When set, every record the framework logger writes — after the `MCP_LOG_LEVEL` filter and the rate limit, with the same fields as the pino output (the error argument as the `exception.*` attributes rather than an `err` field), redacted the same way — is also sent through a `BatchLogRecordProcessor`, with its MCP level as the severity and the active span's trace context (a handler's `ctx.log` record joins its `tool_execution:*` span). `interactions.log` transcripts are never exported. Log export needs three more optional peers: `bun add @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/api-logs`.
45
45
 
46
46
  ---
47
47
 
@@ -157,7 +157,7 @@ The three `mcp.input.*` counters are the pre-validation step's metrics. Each mar
157
157
 
158
158
  **Every label is author- or framework-defined — the caller's own key text is never one.** `mcp.input.ignore_rule` is the ignore-list entry that matched or the fixed `underscore_prefix`, bounded by the list's length plus one. `mcp.input.aliased` is labelled by the canonical `mcp.input.target` (a declared property of the tool) and `mcp.input.alias_kind`, not by the alias the caller sent — the case-style half accepts every `-`/`_`/case permutation of a declared key, so labelling the alias would put a caller-controlled set on a permanent series. That is the unbounded-label leak removed from the rate-limiter counter in 0.9.0: a metric attribute set lives until process restart, so anything the caller names belongs on a span or in a log, never on a counter.
159
159
 
160
- **To find the raw key, read the debug log**, which carries `ignoredKey` / `alias` alongside the bounded rule and target. The counter tells you a client artifact exists and how often; the log tells you what it is called, which is what you need before extending `input.ignoreKeys`, declaring an `inputAliases` entry, or renaming a parameter.
160
+ **To find the raw key, read the debug log**, which carries `ignoredKey` / `alias` alongside the bounded rule and target — at most its first 1,024 characters, with `ignoredKeyLength` / `aliasLength` holding the uncut length of a longer key. The counter tells you a client artifact exists and how often; the log tells you what it is called, which is what you need before extending `input.ignoreKeys`, declaring an `inputAliases` entry, or renaming a parameter.
161
161
 
162
162
  ### Outbound pacer
163
163
 
@@ -216,7 +216,7 @@ A definition may put `severity` on an `errors[]` entry — `debug`, `info`, `not
216
216
  - The `Error in tool:<name>` log record is emitted at that level instead of `error`, with the same message and structured fields.
217
217
  - `mcp.errors.classified` gains `mcp.error.severity` on that record. It is set only when a severity resolved below `error`.
218
218
 
219
- The framework's own refusals resolve one without a declaration: an argument rejection (`invalid_arguments`) and a `ctx.requestInput` the client connection cannot serve (`client_capability_missing`) log at `notice`, so even a server that declares no severity sees `mcp.error.severity: "notice"` on those `mcp.errors.classified` increments — a bounded split a dashboard can use to separate caller rejections from faults. An argument rejection opens no execution span and reaches no call counter either way; it still counts once on `mcp.tool.rejections`.
219
+ The framework's own refusals resolve one without a declaration: an argument rejection (`invalid_arguments`), a `ctx.requestInput` the client connection cannot serve (`client_capability_missing`), and a missing-scope refusal from the inline `auth` check or `checkScopes` log at `notice`, so even a server that declares no severity sees `mcp.error.severity: "notice"` on those `mcp.errors.classified` increments — a bounded split a dashboard can use to separate caller rejections from faults. A missing auth context (`-32006`), a handler's own `forbidden()`, and an upstream 403 stay at `error`. An argument rejection and an inline missing-scope refusal open no execution span and reach no call counter either way; each still counts once on `mcp.tool.rejections`. None of the three refusal records carries a stack, whatever level an `errors[]` entry declares.
220
220
 
221
221
  The call still failed: the execution span keeps `SpanStatusCode.ERROR` and its recorded exception, `mcp.tool.calls` / `mcp.tool.duration` / `mcp.tool.errors` record the same values, and the completion log still reads `isSuccess: false`. Splitting those series on an authoring decision would redefine what an error rate means. Tools only — resources write no failure record of their own. A cancelled request keeps its own `info`, stack-free path whatever the contract declares. See `api-errors`.
222
222
 
@@ -224,7 +224,7 @@ The call still failed: the execution span keeps `SpanStatusCode.ERROR` and its r
224
224
 
225
225
  | Metric | Type | Unit | Attributes |
226
226
  |:-------|:-----|:-----|:-----------|
227
- | `mcp.errors.classified` | counter | `{errors}` | `mcp.error.classified_code` (JSON-RPC code), `mcp.error.category` (`upstream`/`server`/`client`, as in [Error category](#error-category)), `operation`, and `mcp.error.severity` when a tool failure's level resolved below `error` — a declared severity, or `notice` for an `invalid_arguments` / `client_capability_missing` refusal |
227
+ | `mcp.errors.classified` | counter | `{errors}` | `mcp.error.classified_code` (JSON-RPC code), `mcp.error.category` (`upstream`/`server`/`client`, as in [Error category](#error-category)), `operation`, and `mcp.error.severity` when a tool failure's level resolved below `error` — a declared severity, or `notice` for an `invalid_arguments` / `client_capability_missing` refusal or a missing-scope refusal |
228
228
  | `mcp.ratelimit.rejections` | counter | `{rejections}` | — (the rate-limit key is caller-supplied and typically per-client, so it would materialize an unbounded series in the meter; per-key attribution lives on the span instead) |
229
229
  | `http.client.request.duration` | histogram | `s` | `http.request.method`, `server.address`, `http.response.status_code` (when > 0; absent on network errors before a response is received) |
230
230
 
@@ -247,6 +247,8 @@ Auto-registered when `process.memoryUsage` / `process.uptime` / `perf_hooks` are
247
247
 
248
248
  Every framework log record carries `requestId`, `traceId`, `spanId`, and `tenantId` from the request context, so every log line is searchable by trace. `@opentelemetry/instrumentation-pino` does not touch these records: it patches only a `pino` loaded after the SDK starts. To ship the records to the same backend as traces, set `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (see Enabling export).
249
249
 
250
+ **Errors and nested data.** An `Error` anywhere in a record — under any key, at any depth, and the `err` field an error argument rides in the process log, which leads the record so a line the walk cuts still says what failed — is written as `type` (its `name`), `message`, `stack`, a string `code` (or an `McpError`'s numeric `code` and its `data`), and `cause` and an `AggregateError`'s `errors` in the same shape. A `cause` or member whose stack equals its parent's is written without it, so a rethrown `tryCatch` error and the original on its `cause` carry the throw-site stack once. No other own property is written, so a runtime's request URL (`path`, `input`) or file path (`sourceURL`) stays out of the log; a URL inside an error's own message is written as-is. Objects are kept through 15 levels below the record root, and one 16 levels down is written as `'[MaxDepth]'`. A reference back to an enclosing object is written as `'[Circular]'`. One walk writes at most 16 MiB (16,777,216 characters) of strings, field names, and primitives, repeated or not, each charged about the characters it writes (a string with its quotes, a field name with its quotes, colon, and comma), then writes `'[Truncated]'` — for a field name, `'[Truncated]': '[Truncated]'` — and stops: a 10 MB string is written whole and a 20 MB one is cut, and data a getter builds on every read with 50 fields of a 1,000-character string per object writes 16 MiB, not 284 MB. Repeated content — an object reached again through a shared reference and everything beneath it, or a string or field name of 1,024 characters or more written again — is also charged against 1,000,000 per record and written as `'[Truncated]'` where that runs out. A later repeat that still fits is written, so 20,000 rows sharing one `tags: []` come out whole, while a graph whose 17 objects each refer to the next three times writes 0.4 MB, not 380 MB. One walk also makes at most 400,000 reads — one per object, one per field or array element that is not an object (a redacted field included), ten per object 16 levels down — then writes `'[Truncated]'` and stops, which bounds data a getter, Proxy, or `toJSON()` builds on every read: 0.8 MB in milliseconds, not 308 MB in seconds, and 3.7 MB when each object it builds carries 200 numeric fields. A record of 100,000 distinct one-field objects takes half those reads and is written whole. A value whose read throws — a getter, a Proxy trap, a revoked Proxy — is written as `'[Unreadable]'`, and a class instance (`AbortSignal`, `Map`) is dropped unread. A key is redacted at every depth kept when some run of its adjacent words, joined, equals a sensitive field name, case and separators ignored. Words split at every character other than a letter or digit, at a lowercase letter followed by a capital, at the end of a run of capitals, and around each run of digits: `apiKey`, `API_KEY`, `x-api-key`, `accessToken`, `upstream_private_key`, and `apiKey2` are redacted, `max_tokens`, `MAX_TOKENS`, and `tokenizer` are not. That walk is the only redaction, and `sanitization.setSensitiveFields` extends it. The correlation fields a record's context supplies at its root — `requestId`, `sessionId`, `tenantId`, `traceId`, `spanId`, `timestamp`, `operation` — are never redacted. An added name matching one (`session_id`, `id`) still redacts a caller's own key of that name, and the same key on `interactions.log`, which carries no record context. stderr, the files, and `interactions.log` write the same object. A record key named after a field either pino logger writes on its lines itself — `level`, `time`, `msg`, `env`, `version`, `pid`, and `hostname`, and `err` on a line carrying an error argument — is written as `data_<name>` in the process log and `interactions.log` alike (`data_data_<name>` when that name is taken too), so the line carries one of each, its own: a transport routes it by its own `level` (a caller's `level: 60` on an info record stays out of `error.log`), and a parser reads the server's `version`, not the caller's. The OTLP export writes it too, except the error argument, which goes out as the `exception.*` attributes (type, message, stack), so its `cause`, `code`, and `data` stay in the process log. The `ctx.log` mirror to the client applies the same bounds, markers, and matcher, and writes an `Error` as `{ type, message }` only.
251
+
250
252
  For domain logging inside handlers, use `ctx.log` (`debug`/`info`/`notice`/`warning`/`error`) — auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. The completion log emitted at the end of every handler — at `info`, whatever the outcome — carries a `metrics` payload, with fields tuned to each surface:
251
253
 
252
254
  | Handler | Log message | `metrics` fields |
@@ -257,11 +259,15 @@ For domain logging inside handlers, use `ctx.log` (`debug`/`info`/`notice`/`warn
257
259
 
258
260
  Every record of a resource read — scope checks, the handler's `ctx.log` lines, the completion record — carries the read's URI as `resourceUri`, capped like `mcp.resource.uri`, plus `resourceUriLength` when the cap cut it. The handler's `ctx.uri` and the response keep the full URI.
259
261
 
260
- A failed tool call or prompt adds exactly one `Error in tool:<name>` / `Error in prompt:<name>` record. Each call — prompts included — logs under its own `requestId`, and the client receives that value as `data.requestId` on the call's error envelope, so a reported failure resolves to its records.
262
+ Every record of a tool call, resource read, or `prompts/get` carries the client's JSON-RPC id as `jsonRpcId`, string or number as sent; a string over 1,024 characters keeps at most its first 1,024, plus `jsonRpcIdLength` with the uncut length. `httpErrorHandler`'s records carry the request body's id the same way. It is a log field only — no span or metric attribute carries it — and never the call's `requestId`; the response `id` returns it unchanged. A 2025-era `ctx.requestInput` round trip, which the SDK serves by re-entering the handler, logs each entry under a new `requestId`; the shared `jsonRpcId` ties them together.
263
+
264
+ A failed tool call or prompt adds exactly one `Error in tool:<name>` / `Error in prompt:<name>` record. Each call — prompts included — logs under its own generated `requestId`, and the client receives that value as `data.requestId` on the call's error envelope, so a reported failure resolves to its records.
265
+
266
+ An argument rejection's `Error in tool:<name>` record is bounded whatever the caller sends: every string it takes from the rejection — the message (so `msg` and `errorData.originalMessage`), `recovery.hint`, each issue's `message`, each `input` key — keeps at most its first 1,024 characters, and every array its first 10 entries. Each cut records the uncut length or count beside it: `originalMessageLength` for the message, `<field>Length` beside a cut string, `<field>Count` beside a cut array, and `<field>Lengths` — the uncut length of each entry kept — beside an array one of whose entries was cut. A rejection within the caps carries no such field. The `-32602` result the caller receives is built from the uncut rejection.
261
267
 
262
268
  ### Failed-call payloads
263
269
 
264
- Off by default. With `LOG_TOOL_FAILURE_PAYLOADS=true`, a failed tool call writes one more record right after its `Error in tool:<name>` record: message `Tool failure payload: <name>`, the same request context (`requestId`, `traceId`, `spanId`, `toolName`), and the same level, a declared `severity` and the `notice` of an argument rejection included. A payload record below `MCP_LOG_LEVEL` is dropped with its `Error in tool:` record, so at `warning` or above an argument rejection writes neither.
270
+ Off by default. With `LOG_TOOL_FAILURE_PAYLOADS=true`, a failed tool call writes one more record right after its `Error in tool:<name>` record: message `Tool failure payload: <name>`, the same request context (`requestId`, `traceId`, `spanId`, `toolName`), and the same level, a declared `severity` and the `notice` of a framework refusal included. A payload record below `MCP_LOG_LEVEL` is dropped with its `Error in tool:` record, so at `warning` or above an argument rejection writes neither.
265
271
 
266
272
  | Field | Content |
267
273
  |:------|:--------|
@@ -269,7 +275,7 @@ Off by default. With `LOG_TOOL_FAILURE_PAYLOADS=true`, a failed tool call writes
269
275
  | `toolResult` | The `CallToolResult` the tool returned. On 2026-07-28 the SDK adds `resultType` and `_meta` serverInfo on the wire after the record is written |
270
276
  | `toolInputTruncated` / `toolResultTruncated` | Whether that payload was cut at `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` (default `16384`) |
271
277
 
272
- Each payload is redacted with `sanitization.sanitizeForLogging`, serialized, then cut on a UTF-8 character boundary, each on its own. They are strings, not objects, because the logger drops values nested deeper than four levels. Covered: `auth` refusals, argument rejections (`-32602`), handler throws, and output/enrichment contract failures. Nothing is written for a success, a `RequestCancelled`, or an `input_required` return, nor for resource and prompt failures.
278
+ Each payload is redacted with `sanitization.sanitizeForLogging`, serialized, then cut on a UTF-8 character boundary, each on its own. They are strings, not objects, so the byte cap bounds each one and a payload of any depth is written whole up to it — within the 16 MiB of characters one record's walk writes — never cut at the logger's 16-level depth bound. Covered: `auth` refusals, argument rejections (`-32602`), handler throws, and output/enrichment contract failures. Nothing is written for a success, a `RequestCancelled`, or an `input_required` return, nor for resource and prompt failures.
273
279
 
274
280
  The record goes wherever the error record goes: stderr, `combined.log`, and OTLP when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` is set. On Workers, where no file sink exists, set the flag as a Worker binding. It passes the `MCP_LOG_LEVEL` filter and the rate limit like any record, and its message is constant per tool, so when one tool fails more than `MCP_LOG_RATE_LIMIT_THRESHOLD` times in a window, only the first payloads are kept. **Redaction matches key names only.** A secret inside a free-form value, such as a token pasted into a `query` or a connection string in an error message, is written as-is. Enable it only where the log store is trusted with caller data.
275
281