@cyanheads/mcp-ts-core 0.13.7 → 0.13.8

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 (210) hide show
  1. package/AGENTS.md +3 -3
  2. package/CLAUDE.md +3 -3
  3. package/README.md +3 -1
  4. package/changelog/0.13.x/0.13.8.md +101 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/index.js +19 -0
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/core/app.d.ts.map +1 -1
  10. package/dist/core/app.js +14 -2
  11. package/dist/core/app.js.map +1 -1
  12. package/dist/core/context.d.ts.map +1 -1
  13. package/dist/core/context.js +13 -3
  14. package/dist/core/context.js.map +1 -1
  15. package/dist/core/worker.d.ts +2 -0
  16. package/dist/core/worker.d.ts.map +1 -1
  17. package/dist/core/worker.js +2 -0
  18. package/dist/core/worker.js.map +1 -1
  19. package/dist/linter/rules/enrichment-rules.d.ts +3 -2
  20. package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
  21. package/dist/linter/rules/enrichment-rules.js +9 -2
  22. package/dist/linter/rules/enrichment-rules.js.map +1 -1
  23. package/dist/linter/rules/handler-body-rules.d.ts.map +1 -1
  24. package/dist/linter/rules/handler-body-rules.js +10 -4
  25. package/dist/linter/rules/handler-body-rules.js.map +1 -1
  26. package/dist/linter/rules/schema-rules.d.ts +5 -0
  27. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  28. package/dist/linter/rules/schema-rules.js +44 -17
  29. package/dist/linter/rules/schema-rules.js.map +1 -1
  30. package/dist/mcp-server/outputContract.d.ts +33 -0
  31. package/dist/mcp-server/outputContract.d.ts.map +1 -0
  32. package/dist/mcp-server/outputContract.js +43 -0
  33. package/dist/mcp-server/outputContract.js.map +1 -0
  34. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  35. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +10 -2
  36. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  37. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +16 -5
  38. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  39. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +65 -13
  40. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  41. package/dist/mcp-server/transports/auth/lib/authUtils.js +4 -1
  42. package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
  43. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts.map +1 -1
  44. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +1 -1
  45. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js.map +1 -1
  46. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts.map +1 -1
  47. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +2 -5
  48. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js.map +1 -1
  49. package/dist/mcp-server/transports/http/sessionStore.d.ts.map +1 -1
  50. package/dist/mcp-server/transports/http/sessionStore.js +2 -2
  51. package/dist/mcp-server/transports/http/sessionStore.js.map +1 -1
  52. package/dist/services/canvas/core/CanvasRegistry.js +1 -1
  53. package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
  54. package/dist/services/canvas/core/DataCanvas.d.ts.map +1 -1
  55. package/dist/services/canvas/core/DataCanvas.js +7 -5
  56. package/dist/services/canvas/core/DataCanvas.js.map +1 -1
  57. package/dist/services/canvas/core/canvasFactory.d.ts.map +1 -1
  58. package/dist/services/canvas/core/canvasFactory.js +2 -2
  59. package/dist/services/canvas/core/canvasFactory.js.map +1 -1
  60. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  61. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +25 -16
  62. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  63. package/dist/services/llm/providers/openrouter.provider.js +1 -1
  64. package/dist/services/llm/providers/openrouter.provider.js.map +1 -1
  65. package/dist/services/speech/providers/elevenlabs.provider.js +3 -3
  66. package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
  67. package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
  68. package/dist/services/speech/providers/whisper.provider.js +5 -5
  69. package/dist/services/speech/providers/whisper.provider.js.map +1 -1
  70. package/dist/storage/core/StorageService.d.ts.map +1 -1
  71. package/dist/storage/core/StorageService.js +3 -6
  72. package/dist/storage/core/StorageService.js.map +1 -1
  73. package/dist/storage/core/storageFactory.d.ts.map +1 -1
  74. package/dist/storage/core/storageFactory.js +12 -15
  75. package/dist/storage/core/storageFactory.js.map +1 -1
  76. package/dist/storage/core/storageValidation.d.ts +13 -13
  77. package/dist/storage/core/storageValidation.d.ts.map +1 -1
  78. package/dist/storage/core/storageValidation.js +49 -125
  79. package/dist/storage/core/storageValidation.js.map +1 -1
  80. package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
  81. package/dist/storage/providers/cloudflare/d1Provider.js +5 -3
  82. package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
  83. package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
  84. package/dist/storage/providers/cloudflare/kvProvider.js +1 -1
  85. package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
  86. package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
  87. package/dist/storage/providers/cloudflare/r2Provider.js +3 -3
  88. package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
  89. package/dist/storage/providers/fileSystem/fileSystemProvider.js +4 -4
  90. package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
  91. package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +1 -1
  92. package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
  93. package/dist/storage/providers/inMemory/inMemoryProvider.js +6 -5
  94. package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
  95. package/dist/testing/fuzz.d.ts.map +1 -1
  96. package/dist/testing/fuzz.js +7 -1
  97. package/dist/testing/fuzz.js.map +1 -1
  98. package/dist/testing/index.d.ts +15 -2
  99. package/dist/testing/index.d.ts.map +1 -1
  100. package/dist/testing/index.js +51 -6
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/types-global/errors.d.ts +7 -4
  103. package/dist/types-global/errors.d.ts.map +1 -1
  104. package/dist/types-global/errors.js.map +1 -1
  105. package/dist/utils/formatting/codeSpan.d.ts +27 -0
  106. package/dist/utils/formatting/codeSpan.d.ts.map +1 -0
  107. package/dist/utils/formatting/codeSpan.js +42 -0
  108. package/dist/utils/formatting/codeSpan.js.map +1 -0
  109. package/dist/utils/formatting/diffFormatter.d.ts.map +1 -1
  110. package/dist/utils/formatting/diffFormatter.js +7 -15
  111. package/dist/utils/formatting/diffFormatter.js.map +1 -1
  112. package/dist/utils/formatting/markdownBuilder.d.ts +12 -5
  113. package/dist/utils/formatting/markdownBuilder.d.ts.map +1 -1
  114. package/dist/utils/formatting/markdownBuilder.js +14 -2
  115. package/dist/utils/formatting/markdownBuilder.js.map +1 -1
  116. package/dist/utils/formatting/tableFormatter.d.ts.map +1 -1
  117. package/dist/utils/formatting/tableFormatter.js +5 -9
  118. package/dist/utils/formatting/tableFormatter.js.map +1 -1
  119. package/dist/utils/formatting/treeFormatter.d.ts.map +1 -1
  120. package/dist/utils/formatting/treeFormatter.js +5 -9
  121. package/dist/utils/formatting/treeFormatter.js.map +1 -1
  122. package/dist/utils/internal/error-handler/errorHandler.d.ts +17 -10
  123. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  124. package/dist/utils/internal/error-handler/errorHandler.js +47 -26
  125. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  126. package/dist/utils/internal/error-handler/mappings.d.ts +17 -1
  127. package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
  128. package/dist/utils/internal/error-handler/mappings.js +22 -1
  129. package/dist/utils/internal/error-handler/mappings.js.map +1 -1
  130. package/dist/utils/internal/error-handler/types.d.ts +2 -0
  131. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  132. package/dist/utils/internal/logger.d.ts +75 -3
  133. package/dist/utils/internal/logger.d.ts.map +1 -1
  134. package/dist/utils/internal/logger.js +181 -52
  135. package/dist/utils/internal/logger.js.map +1 -1
  136. package/dist/utils/internal/performance.d.ts +11 -0
  137. package/dist/utils/internal/performance.d.ts.map +1 -1
  138. package/dist/utils/internal/performance.js +46 -12
  139. package/dist/utils/internal/performance.js.map +1 -1
  140. package/dist/utils/network/fetchWithTimeout.d.ts +11 -5
  141. package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
  142. package/dist/utils/network/fetchWithTimeout.js +50 -23
  143. package/dist/utils/network/fetchWithTimeout.js.map +1 -1
  144. package/dist/utils/network/retry.d.ts +16 -8
  145. package/dist/utils/network/retry.d.ts.map +1 -1
  146. package/dist/utils/network/retry.js +19 -8
  147. package/dist/utils/network/retry.js.map +1 -1
  148. package/dist/utils/overflow/outlineOnOverflow.d.ts +18 -2
  149. package/dist/utils/overflow/outlineOnOverflow.d.ts.map +1 -1
  150. package/dist/utils/overflow/outlineOnOverflow.js +28 -3
  151. package/dist/utils/overflow/outlineOnOverflow.js.map +1 -1
  152. package/dist/utils/pagination/pagination.d.ts +3 -1
  153. package/dist/utils/pagination/pagination.d.ts.map +1 -1
  154. package/dist/utils/pagination/pagination.js +10 -2
  155. package/dist/utils/pagination/pagination.js.map +1 -1
  156. package/dist/utils/parsing/csvParser.d.ts.map +1 -1
  157. package/dist/utils/parsing/csvParser.js +4 -2
  158. package/dist/utils/parsing/csvParser.js.map +1 -1
  159. package/dist/utils/parsing/htmlExtractor.js +1 -1
  160. package/dist/utils/parsing/htmlExtractor.js.map +1 -1
  161. package/dist/utils/parsing/jsonParser.d.ts.map +1 -1
  162. package/dist/utils/parsing/jsonParser.js +3 -1
  163. package/dist/utils/parsing/jsonParser.js.map +1 -1
  164. package/dist/utils/parsing/xmlParser.d.ts.map +1 -1
  165. package/dist/utils/parsing/xmlParser.js +3 -1
  166. package/dist/utils/parsing/xmlParser.js.map +1 -1
  167. package/dist/utils/parsing/yamlParser.d.ts.map +1 -1
  168. package/dist/utils/parsing/yamlParser.js +3 -1
  169. package/dist/utils/parsing/yamlParser.js.map +1 -1
  170. package/dist/utils/security/idGenerator.d.ts.map +1 -1
  171. package/dist/utils/security/idGenerator.js +20 -4
  172. package/dist/utils/security/idGenerator.js.map +1 -1
  173. package/dist/utils/security/sanitization.d.ts +31 -0
  174. package/dist/utils/security/sanitization.d.ts.map +1 -1
  175. package/dist/utils/security/sanitization.js +98 -11
  176. package/dist/utils/security/sanitization.js.map +1 -1
  177. package/dist/utils/telemetry/attributes.d.ts +16 -1
  178. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  179. package/dist/utils/telemetry/attributes.js +16 -1
  180. package/dist/utils/telemetry/attributes.js.map +1 -1
  181. package/dist/utils/telemetry/instrumentation.d.ts +9 -3
  182. package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
  183. package/dist/utils/telemetry/instrumentation.js +85 -13
  184. package/dist/utils/telemetry/instrumentation.js.map +1 -1
  185. package/framework-skills/api-auth/SKILL.md +3 -1
  186. package/framework-skills/api-canvas/SKILL.md +3 -3
  187. package/framework-skills/api-config/SKILL.md +6 -3
  188. package/framework-skills/api-context/SKILL.md +3 -3
  189. package/framework-skills/api-errors/SKILL.md +16 -11
  190. package/framework-skills/api-linter/SKILL.md +7 -3
  191. package/framework-skills/api-telemetry/SKILL.md +31 -11
  192. package/framework-skills/api-testing/SKILL.md +5 -3
  193. package/framework-skills/api-utils/SKILL.md +9 -9
  194. package/framework-skills/api-utils/references/formatting.md +1 -1
  195. package/framework-skills/api-utils/references/parsing.md +2 -2
  196. package/framework-skills/api-utils/references/security.md +6 -4
  197. package/framework-skills/git-wrapup/SKILL.md +5 -2
  198. package/framework-skills/maintenance/SKILL.md +2 -2
  199. package/framework-skills/polish-docs-meta/SKILL.md +1 -1
  200. package/framework-skills/polish-docs-meta/references/readme.md +1 -0
  201. package/framework-skills/release-and-publish/SKILL.md +2 -2
  202. package/framework-skills/techniques/SKILL.md +1 -1
  203. package/framework-skills/techniques/references/outline-on-overflow.md +12 -7
  204. package/package.json +18 -3
  205. package/scripts/check-skill-versions.ts +103 -22
  206. package/scripts/devcheck.ts +4 -3
  207. package/scripts/lint-packaging.ts +38 -1
  208. package/templates/.env.example +4 -0
  209. package/templates/Dockerfile +26 -6
  210. package/templates/package.json +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","sourceRoot":"","sources":["../../../src/utils/telemetry/instrumentation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","sourceRoot":"","sources":["../../../src/utils/telemetry/instrumentation.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.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -34,6 +34,8 @@ const myTool = tool('my_tool', {
34
34
 
35
35
  When `MCP_AUTH_MODE=none`, auth checks are skipped and defaults are allowed.
36
36
 
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
+
37
39
  ---
38
40
 
39
41
  ## 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.4"
7
+ version: "2.5"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -168,9 +168,9 @@ await instance.registerTable('recent_fetch', rows, { ttlMs: 30 * 60 * 1000 });
168
168
 
169
169
  Run SQL across registered tables. Returns at most `rowLimit` rows (default 10 000). When the result exceeds `rowLimit`, the response carries `truncated: true` and `rowCount` reflects the number of materialized rows (not the full result set). For full result sets and exact counts, pass `registerAs` — the result is materialized as a new canvas table; the response carries a `preview` slice and the exact `rowCount`.
170
170
 
171
- Querying a table that does not exist throws `NotFound` (`data.reason: 'missing_table'`) with a recovery hint to re-run the tool that staged the table or list what is currently staged. This happens when a table has expired (per-table TTL), been dropped, or the name is mistyped. The error is `NotFound`, not `ValidationError` — agents should re-stage, not fix the SQL shape. A well-formed but unknown or expired `canvas_id` fails the same way (`data.reason: 'canvas_not_found'`, with its own recovery hint) — thrown by `acquire()` and every canvas operation. An id that fails the format check is a different failure: `ValidationError` with `data.reason: 'canvas_id_malformed'`, raised before the lookup on each of the three entry points that take a caller-supplied id — `acquire`, `drop` (which previously reported it as a silent `false`), and `importFrom`'s source id.
171
+ Querying a table that does not exist throws `NotFound` (`data.reason: 'missing_table'`, `data.tableName` carrying the full name as DuckDB reports it, spaces included) with a recovery hint to re-run the tool that staged the table or list what is currently staged. This happens when a table has expired (per-table TTL), been dropped, or the name is mistyped. The error is `NotFound`, not `ValidationError` — agents should re-stage, not fix the SQL shape. Only a read-shaped statement qualifies (one starting `SELECT`, `WITH`, or DuckDB's FROM-first `FROM`): a `DROP`, `DELETE`, `INSERT`, `UPDATE`, or `ALTER` naming a missing table is `non_select_statement`, since re-staging would not make it pass. A well-formed but unknown or expired `canvas_id` fails the same way (`data.reason: 'canvas_not_found'`, with its own recovery hint) — thrown by `acquire()` and every canvas operation. An id that fails the format check is a different failure: `ValidationError` with `data.reason: 'canvas_id_malformed'`, raised before the lookup on each of the three entry points that take a caller-supplied id — `acquire`, `drop` (which previously reported it as a silent `false`), and `importFrom`'s source id.
172
172
 
173
- A `SELECT` that parses but fails to prepare for any other reason — a mistyped column, an unknown function, an invalid expression — throws `ValidationError` (`data.reason: 'invalid_sql'`) and preserves the DuckDB binder detail in `data.binderMessage` (e.g. `Referenced column "x" not found...`, often with a candidate suggestion). This is distinct from `non_select_statement`, reserved for statements that genuinely aren't `SELECT`s — here the shape is fine, so the agent should fix the named column or function.
173
+ A `SELECT` that parses but fails to prepare for any other reason — a mistyped column, an unknown scalar or table function, type, or collation, a schema the canvas does not have, an invalid expression — throws `ValidationError` (`data.reason: 'invalid_sql'`) and preserves the DuckDB binder detail in `data.binderMessage` (e.g. `Referenced column "x" not found...`, often with a candidate suggestion). This is distinct from `non_select_statement`, reserved for statements that genuinely aren't `SELECT`s — here the shape is fine, so the agent should fix the named column or function. DuckDB's FROM-first form (`FROM t`, `FROM t SELECT a`) is a `SELECT`: it passes the gate, and one that fails to prepare is classified the same way.
174
174
 
175
175
  A `SELECT` that prepares and then fails on the staged data throws `ValidationError` (`data.reason: 'sql_execution_error'`) with the engine message preserved and a hint pointing at `TRY_CAST` or filtering the offending rows. The split follows DuckDB's own execution-error classes — `Conversion Error`, `Invalid Input Error`, `Out of Range Error` — matched on the message prefix. Engine faults (`IO Error`, `INTERNAL Error`, `Out of Memory Error`, and anything unmatched) stay `DatabaseError`, so an export or import failing on I/O is never reported to the caller as bad SQL. `DUCKDB_ERROR_REASONS` exports these alongside `SQL_GATE_REASONS`.
176
176
 
@@ -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.20"
7
+ version: "1.21"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -93,7 +93,9 @@ await createApp({ sessionMode: { default: 'stateful', require: 'stateful' } });
93
93
  |:--------|:-----------------|:--------|:------|
94
94
  | `NODE_ENV` | `environment` | `development` | Aliases: `dev`→`development`, `prod`→`production`, `test`→`testing` |
95
95
  | `MCP_LOG_LEVEL` | `logLevel` | `debug` | Aliases: `warn`→`warning`, `err`→`error`, `fatal`/`silent`→`emerg`, `trace`→`debug`, `information`→`info` |
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 |
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
+ | `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
+ | `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` |
97
99
 
98
100
  ### Transport
99
101
 
@@ -212,8 +214,9 @@ Activated when `SUPABASE_URL` is set.
212
214
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | — | — | OTLP/HTTP base URL; resolves `tracesEndpoint` to `<base>/v1/traces` and `metricsEndpoint` to `<base>/v1/metrics` (path prefix kept) when the signal-specific variable is unset |
213
215
  | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `openTelemetry.tracesEndpoint` | — | OTLP traces endpoint URL; overrides the base, used as-is |
214
216
  | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `openTelemetry.metricsEndpoint` | — | OTLP metrics endpoint URL; overrides the base, used as-is |
217
+ | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `openTelemetry.logsEndpoint` | — | OTLP logs endpoint URL; the only switch for log record export, never derived from the base. Needs the optional peers `@opentelemetry/sdk-logs`, `@opentelemetry/exporter-logs-otlp-http`, `@opentelemetry/api-logs` |
215
218
  | `OTEL_TRACES_SAMPLER_ARG` | `openTelemetry.samplingRatio` | `1.0` | 0–1; fraction of traces to export |
216
- | `OTEL_LOG_LEVEL` | `openTelemetry.logLevel` | `INFO` | OTel SDK internal log level: `NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `VERBOSE` \| `ALL` |
219
+ | `OTEL_LOG_LEVEL` | `openTelemetry.logLevel` | `INFO` | OTel SDK internal log level: `NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `VERBOSE` \| `ALL`; aliases `warning`→`WARN`, `err`→`ERROR`, `information`→`INFO`. Diag output goes to stderr at every level |
217
220
 
218
221
  ---
219
222
 
@@ -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.enrich`, `ctx.content`), and when to use each.
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
  ---
@@ -109,7 +109,7 @@ await fetchUser('123', ctx); // ctx is a Context — no conversion
109
109
 
110
110
  `RequestContext` has **no index signature**. Its fields are exactly: `auth`, `extra`, `operation`, `requestId`, `sessionId`, `spanId`, `tenantId`, `timestamp`, `traceId`. A misspelled canonical field (`tenatId`) is a compile error instead of a silently-ignored key.
111
111
 
112
- Operation-specific correlation data goes in **`extra`** — the one deliberate open bag (`Readonly<Record<string, unknown>>`). The logger flattens `extra` into the emitted line, so log output looks the same as a top-level spread.
112
+ Operation-specific correlation data goes in **`extra`** — the one deliberate open bag (`Readonly<Record<string, unknown>>`). The logger flattens `extra` into the emitted line, so log output looks the same as a top-level spread — except that an `extra` key named like a canonical field the context sets never replaces it.
113
113
 
114
114
  ### Adding correlation data
115
115
 
@@ -149,7 +149,7 @@ Never re-open the shape to get past a type error: no index signature, no widenin
149
149
 
150
150
  Request-scoped structured logger. Every log line is automatically annotated with `requestId`, `traceId`, and `tenantId` — no manual spreading needed.
151
151
 
152
- **Dual-sink.** Each call writes to Pino *and* mirrors onto the MCP wire as a `notifications/message` (the framework advertises the `logging` capability, and the SDK filters by the level the client set via `logging/setLevel`). The wire payload is `{ message, ...data }`; `ctx.log.error` adds `error: <message>`. 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.
152
+ **Dual-sink.** Each call writes to Pino *and* mirrors onto the MCP wire as a `notifications/message` (the framework advertises the `logging` capability, and the SDK filters by the level the client set via `logging/setLevel`). The wire payload is `{ message, ...data }`; `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.
153
153
 
154
154
  ### Methods
155
155
 
@@ -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.16"
7
+ version: "1.17"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -256,7 +256,7 @@ throw new McpError(code, message?, data?, options?)
256
256
 
257
257
  - `code` — a `JsonRpcErrorCode` enum value
258
258
  - `message` — optional human-readable description of the failure
259
- - `data` — optional structured context (plain object)
259
+ - `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.
260
260
  - `options` — optional `{ cause?: unknown }` for error chaining
261
261
 
262
262
  **Example:**
@@ -316,21 +316,26 @@ The framework applies these steps in order — first match wins:
316
316
 
317
317
  1. **Request signal aborted** — `ctx.signal.aborted` is `true` when the handler unwinds → `RequestCancelled`. Resolved before the thrown value is classified at all — by the tool and resource handler factories, and by the HTTP transport's error handler against the inbound request's signal, which catches a caller that hangs up before any handler runs (mid-body, say) and answers it 499 — so it outranks every step below, `McpError` included: the caller withdrew the request, and what the handler threw on the way out does not change that. Covers every shape an abort leaves behind — a `notifications/cancelled` `reason` string, the `DOMException` named `AbortError` a reason-less cancellation produces, a service's own `McpError`, and the SDK's `SdkError(ConnectionClosed)` on transport close. The accepted cost is that an unrelated fault raised after the abort is recorded as a cancellation too; it is bounded, because the SDK writes no response for a request whose signal it aborted. A handler that throws while the signal is live is untouched by this step.
318
318
  2. **`McpError` instance** — `error.code` is preserved as-is; no classification needed.
319
- 3. **SDK transport-closed rejection** — an `SdkError` carrying `SdkErrorCode.ConnectionClosed` → `RequestCancelled`. The SDK rejects every in-flight request when the transport closes, which is what a client disconnect looks like from inside a handler. Matched on the code, not the message: one of its wordings says "aborted" and would otherwise be caught by the generic abort pattern in step 6 and read as a `Timeout`. Still the rule for a throw raised where no request signal is in scope — a service, an outbound leg, a background task.
320
- 4. **JS constructor name** — matched against a fixed table (e.g. `ZodError` → `ValidationError`, `SyntaxError` → `ValidationError`). Note: `TypeError` is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.
321
- 5. **Provider-specific patterns** — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are more specific (e.g. `status code 429` beats the generic `rate limit` pattern).
322
- 6. **Common message/name patterns** — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
323
- 7. **`AbortError` name** — `error.name === 'AbortError'` → `Timeout`.
324
- 8. **Fallback** — `InternalError`.
319
+ 3. **SDK transport-closed rejection** — an `SdkError` carrying `SdkErrorCode.ConnectionClosed` → `RequestCancelled`. The SDK rejects every in-flight request when the transport closes, which is what a client disconnect looks like from inside a handler. Matched on the code, not the message: one of its wordings says "aborted" and would otherwise be caught by the generic abort pattern in step 7 and read as a `Timeout`. Still the rule for a throw raised where no request signal is in scope — a service, an outbound leg, a background task.
320
+ 4. **Engine resource limit** — a `RangeError` whose **whole** message is one the engine raises when it runs out of a resource → `InternalError`: `Maximum call stack size exceeded` (JavaScriptCore adds a trailing period) and the maximum string size (V8 `Invalid string length`, JavaScriptCore `Out of memory`). A handler that recurses without bound names nothing a caller can change, so it is a server fault. Every other `RangeError` — `new Array(-1)`, `(1).toFixed(101)`, an invalid date, `1n / 0n`, or one whose message merely contains a limit text — continues to step 5.
321
+ 5. **JS constructor name** — matched against a fixed table (e.g. `ZodError` → `ValidationError`, `SyntaxError` → `ValidationError`). Note: `TypeError` is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.
322
+ 6. **Provider-specific patterns** — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are more specific (e.g. `status code 429` beats the generic `rate limit` pattern).
323
+ 7. **Common message/name patterns** — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
324
+ 8. **`AbortError` name** — `error.name === 'AbortError'` → `Timeout`.
325
+ 9. **Fallback** — `InternalError`.
325
326
 
326
327
  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`.
327
328
 
329
+ 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.
330
+
331
+ **The framework's own output-contract parses are not caller errors.** A result that breaks the definition's `output` schema (tools and resources) or its `enrichment` block fails as `InternalError` (`-32603`), with a message naming the definition and the contract — `Tool my_tool returned output that does not match its output schema: items.0.id: …` — and no `data`. It is the handler's bug, so it files as `server`, not the `ValidationError` a raw `ZodError` would get. A `ZodError` the handler throws from its own validation keeps `ValidationError`.
332
+
328
333
  ### JS Constructor Name Mappings
329
334
 
330
335
  | Constructor | Mapped Code |
331
336
  |:------------|:------------|
332
337
  | `SyntaxError` | `ValidationError` |
333
- | `RangeError` | `ValidationError` |
338
+ | `RangeError` | `ValidationError` (an engine resource limit is settled first, as `InternalError` — step 4) |
334
339
  | `URIError` | `ValidationError` |
335
340
  | `ZodError` | `ValidationError` |
336
341
  | `ReferenceError` | `InternalError` |
@@ -464,14 +469,14 @@ const parsed = await ErrorHandler.tryCatch(
464
469
 
465
470
  `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())`.
466
471
 
467
- **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 `originalErrorName`, `originalMessage`, `rootCause` (`{ name, message }`), and the canonical fields and `extra` of `context`, but never a stack: `originalStack` and the full `causeChain` go to the log record only.
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`, `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`.
468
473
 
469
474
  **Options** (`Omit<ErrorHandlerOptions, 'rethrow'>`):
470
475
 
471
476
  | Option | Type | Required | Purpose |
472
477
  |:-------|:-----|:--------:|:--------|
473
478
  | `operation` | `string` | Yes | Name logged with the error |
474
- | `context` | `ErrorContext` | No | Structured fields merged into the log record and 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 |
475
480
  | `errorCode` | `JsonRpcErrorCode` | No | Code used if the caught error is not already an `McpError` |
476
481
  | `input` | `unknown` | No | Input value sanitized and logged alongside the error |
477
482
  | `critical` | `boolean` | No | Marks the error as critical in logs (default `false`) |
@@ -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.18"
7
+ version: "1.19"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -201,6 +201,8 @@ Every field in `input`, `output`, `params`, or `args` needs a `.describe('...')`
201
201
  | `z.union([..., z.literal(X), ...])` literal option | **No** | No — outer union describe is sufficient |
202
202
  | A tool `input` root that is a `z.discriminatedUnion(...)` — its variant objects | Yes, their **fields** | No, not on the variant itself — it is a root, and roots carry no describe |
203
203
 
204
+ A self-referential schema — a Zod 4 getter that returns the schema itself (`get children() { return z.array(Node) }`) — is walked once. The walk tracks the schemas on its current path and stops when one re-enters, so a missing `.describe()` inside the recursive schema is reported at its first occurrence, not once per level. The guard is per path: a non-recursive schema reused at two sibling paths is reported at both.
205
+
204
206
  The asymmetry that catches agents: inside `z.union([z.string(), z.array(z.string())])`, the outer `z.string()` option **does** need a describe (unions walk non-literal options), but the `z.string()` inside the inner array does **not** (arrays don't walk primitive elements). If the linter didn't flag a path, don't add a describe there — the redundant describe ships to the JSON Schema as clutter.
205
207
 
206
208
  **Literal variants are exempt** because they carry no independent semantic content — they're structural markers. The canonical case is form-client blank tolerance, where a `z.literal('')` variant is threaded into a union alongside a validated string so empty submissions from MCP Inspector / web UIs round-trip without breaking schema-level validation:
@@ -681,7 +683,7 @@ Heuristic source-text checks that scan `handler.toString()` for common error-han
681
683
 
682
684
  **Severity:** warning
683
685
 
684
- Fires when a handler contains `throw new Error(...)`. Plain `Error` doesn't carry a JSON-RPC code — the framework's auto-classifier degrades to `InternalError`, hiding the actual failure mode.
686
+ Fires when a handler contains `throw new Error(...)`, or `throw Error(...)` — the spelling Bun's transpiler prints for the same code, since it drops `new` from built-in error constructors. Plain `Error` doesn't carry a JSON-RPC code — the framework's auto-classifier degrades to `InternalError`, hiding the actual failure mode. Other built-ins (`TypeError`, `RangeError`) are not flagged in either spelling.
685
687
 
686
688
  Plain `Error` is acceptable for "don't care" cases where the specific code doesn't matter (per CLAUDE.md/AGENTS.md: "plain `Error` for don't-care cases"). This rule targets domain-specific failures that deserve a concrete code — upgrade those to factories or `ctx.fail`, and accept the warning for the rest.
687
689
 
@@ -713,7 +715,7 @@ throw notFound('Item missing');
713
715
 
714
716
  **Severity:** warning
715
717
 
716
- Fires when a `catch (e)` block throws a structured `McpError` (or factory) without passing `{ cause: e }`. Dropping the cause loses the original stack trace — observability platforms and `pino-pretty` rely on it to render error chains.
718
+ Fires when a `catch (e)` block throws a structured `McpError` (or factory) without passing `{ cause: e }`. Dropping the cause loses the original stack trace — observability platforms and `pino-pretty` rely on it to render error chains. When the catch binding is itself named `cause`, the `{ cause }` shorthand satisfies the rule — it is also how Bun's transpiler prints `{ cause: cause }`.
717
719
 
718
720
  **Fix:** thread the cause through the 4th `McpError` argument or factory options:
719
721
 
@@ -1003,6 +1005,8 @@ Fires when an enrichment key matches an `output` key. The effective output schem
1003
1005
 
1004
1006
  Advisory. Fires when a tool has **no** `enrichment` block but an `output` field whose name strongly signals agent-facing context (`notice`, `effectiveQuery`, `queryEcho`) rather than domain payload.
1005
1007
 
1008
+ **Exempt:** a `notice` in an `output` that also declares a `sections` array — the outline-on-overflow arm (`OUTLINE_VARIANT`, see the `techniques` skill). There the notice is the re-call instruction that replaces the document, main-body payload by design, and enrichment can only add to a payload, never replace it.
1009
+
1006
1010
  **Fix:** move the field into an `enrichment` block and populate it via `ctx.enrich(...)` — it reaches both client surfaces without a `format()` entry. Ignore if the field is genuinely domain data. Deliberately conservative — common domain fields like `totalCount` are not flagged.
1007
1011
 
1008
1012
  ### enrichment-trailer-render
@@ -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.13"
7
+ version: "1.14"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -13,7 +13,7 @@ metadata:
13
13
 
14
14
  The framework auto-instruments every tool, resource, prompt, storage, LLM, speech, and graph call — each gets its own span and the standard counters/histograms. HTTP server requests pick up spans from `HttpInstrumentation` (all Node.js HTTP traffic, skips `/healthz`) plus `httpInstrumentationMiddleware` from `@hono/otel` on the MCP HTTP endpoint when installed (optional Tier 3 peer — `bun add @hono/otel`). On Bun, `HttpInstrumentation` silently no-ops and `@hono/otel` is the only HTTP coverage. Auth checks and session lifecycle are tracked as **metrics only** — auth decorates the active HTTP span with attributes, sessions emit counters.
15
15
 
16
- `requestId`, `traceId`, and `tenantId` correlate automatically across spans, metrics, and logs. Pino logs get `trace_id`/`span_id` injected when a span is active.
16
+ `requestId`, `traceId`, and `tenantId` correlate automatically across spans, metrics, and logs. Framework log records carry `traceId`/`spanId` from the request context.
17
17
 
18
18
  A handler's `ctx.traceId` / `ctx.spanId` name the execution span it runs in — `tool_execution:<name>` or `resource_read:<name>` — not the enclosing HTTP request span. Under HTTP the trace ID is the request's, so handler logs join to the request; the span ID is the child execution's, so they join to that span's attributes and duration. On stdio, where no transport span exists, both are still populated from the execution span the framework opens. Both are `undefined` when telemetry is disabled: the non-recording span a disabled pipeline produces carries all-zero IDs, and the framework reports no correlation rather than IDs that correlate to nothing.
19
19
 
@@ -31,14 +31,17 @@ OTel is **off by default**. `OTEL_ENABLED=true` alone does nothing — you also
31
31
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | — | OTLP/HTTP base URL (e.g. `http://localhost:4318`). Traces go to `<base>/v1/traces`, metrics to `<base>/v1/metrics`; a path prefix is kept. |
32
32
  | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | — | OTLP/HTTP traces endpoint (e.g. `http://localhost:4318/v1/traces`). Overrides the base for traces; used as-is. |
33
33
  | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | — | OTLP/HTTP metrics endpoint (e.g. `http://localhost:4318/v1/metrics`). Overrides the base for metrics; used as-is. |
34
+ | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | — | OTLP/HTTP logs endpoint (e.g. `http://localhost:4318/v1/logs`). Opt-in log export; used as-is and never derived from the base. |
34
35
  | `OTEL_SERVICE_NAME` | `createApp` `name` → `package.json` `name` | `service.name` resource attribute. Seeded from `createApp({ name })` when unset; an env value wins. |
35
36
  | `OTEL_SERVICE_VERSION` | `package.json` `version` | `service.version` resource attribute. |
36
37
  | `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Trace sampling ratio (0–1) for `TraceIdRatioBasedSampler`. |
37
- | `OTEL_LOG_LEVEL` | `INFO` | OTel diagnostic logger level (`NONE`/`ERROR`/`WARN`/`INFO`/`DEBUG`/`VERBOSE`/`ALL`). |
38
+ | `OTEL_LOG_LEVEL` | `INFO` | OTel diagnostic logger level (`NONE`/`ERROR`/`WARN`/`INFO`/`DEBUG`/`VERBOSE`/`ALL`; `warning`/`err`/`information` accepted). Diag output goes to stderr at every level, never stdout. |
38
39
 
39
40
  Metrics push via `PeriodicExportingMetricReader` every **15 seconds**. Traces use `BatchSpanProcessor`.
40
41
 
41
- 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. Those two exporters are the only export path. A signal with no resolved endpoint exports nothing, OTel log records are never exported, and `NodeSDK`'s own `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` defaults are not consulted.
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
+
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`.
42
45
 
43
46
  ---
44
47
 
@@ -46,7 +49,7 @@ Endpoints resolve per the [OTLP exporter spec](https://opentelemetry.io/docs/spe
46
49
 
47
50
  | Runtime | Behavior |
48
51
  |:--------|:---------|
49
- | **Node.js / Bun** | Full `NodeSDK`. Auto-instrumentations: HTTP server (Node http hooks; skips `/healthz`), Pino logs (`trace_id`/`span_id` injection). On the HTTP transport, when OTel is enabled and `@hono/otel` is installed, `httpInstrumentationMiddleware` is also wired onto the MCP endpoint — fills the gap on Bun, where the Node http auto-instrumentation silently no-ops. Manual spans, custom metrics, and OTLP export work on Bun regardless. |
52
+ | **Node.js / Bun** | Full `NodeSDK`. Auto-instrumentations: HTTP server (Node http hooks; skips `/healthz`), and Pino, which patches only a `pino` loaded after the SDK starts — never the framework logger's, imported first. On the HTTP transport, when OTel is enabled and `@hono/otel` is installed, `httpInstrumentationMiddleware` is also wired onto the MCP endpoint — fills the gap on Bun, where the Node http auto-instrumentation silently no-ops. Manual spans, custom metrics, and OTLP export work on Bun regardless. |
50
53
  | **Cloudflare Workers / V8 isolates** | `NodeSDK` is unavailable. SDK init no-ops silently. `createCounter`/`createHistogram`/`withSpan` calls still work via the global OTel API but produce no output unless you wire a Worker-compatible exporter and `ctx.waitUntil()` for flush. |
51
54
 
52
55
  Cloud platform detection auto-populates resource attributes:
@@ -126,9 +129,10 @@ All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` wher
126
129
 
127
130
  | Metric | Type | Unit | Attributes |
128
131
  |:-------|:-----|:-----|:-----------|
129
- | `mcp.tool.calls` | counter | `{calls}` | `mcp.tool.name`, `mcp.tool.success` |
132
+ | `mcp.tool.calls` | counter | `{calls}` | `mcp.tool.name`, `mcp.tool.success`, `mcp.tool.outcome` (`ok`/`error`/`cancelled`) |
130
133
  | `mcp.tool.duration` | histogram | `ms` | `mcp.tool.name`, `mcp.tool.success` |
131
- | `mcp.tool.errors` | counter | `{errors}` | `mcp.tool.name`, `mcp.tool.error_category` (`upstream`/`server`/`client`) — see [Error category](#error-category) |
134
+ | `mcp.tool.errors` | counter | `{errors}` | `mcp.tool.name`, `mcp.tool.error_category` (`upstream`/`server`/`client`) — see [Error category](#error-category) — and `mcp.tool.outcome` (`error`/`cancelled`) |
135
+ | `mcp.tool.rejections` | counter | `{calls}` | `mcp.tool.name`, `mcp.tool.error_code`, `mcp.tool.error_category` — once per call rejected before the handler ran |
132
136
  | `mcp.tool.input_bytes` | histogram | `bytes` | `mcp.tool.name` |
133
137
  | `mcp.tool.output_bytes` | histogram | `bytes` | `mcp.tool.name` (success only; the handler's returned value) |
134
138
  | `mcp.tool.param.usage` | counter | `{uses}` | `mcp.tool.name`, `mcp.tool.param` (top-level keys supplied by caller) |
@@ -147,6 +151,8 @@ All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` wher
147
151
  | `mcp.prompt.message_count` | histogram | `{messages}` | `mcp.prompt.name` |
148
152
  | `mcp.requests.active` | up/down counter | `{requests}` | — (in-flight handler executions, all three types) |
149
153
 
154
+ **Rejections and cancellations.** A call refused before the handler runs — argument validation (`-32602`) or the inline `auth` check (`-32005` missing scope, `-32006` no auth context) — never reaches the measured region, so it is absent from `mcp.tool.calls`, `mcp.tool.duration`, and `mcp.tool.errors` and counts once on `mcp.tool.rejections` instead, labelled with the code and category the caller received. `mcp.tool.outcome` separates a caller hang-up from a failure: `cancelled` for a `RequestCancelled` (`-32011`, always paired with `error_category="client"`), `error` for any other failure, `ok` for a success or an `input_required` round. `mcp.tool.success` and `error_category` keep their meaning, so existing `sum()` queries are unchanged. An error rate that excludes hang-ups filters on `mcp.tool.outcome!="cancelled"`; the failure rate a caller sees is `(errors + rejections) / (calls + rejections)`. Resources and prompts carry neither split.
155
+
150
156
  The three `mcp.input.*` counters are the only trace of the pre-validation step a tool call leaves. Each marks a call the strict `input` schema would otherwise have rejected: a client-added root key dropped, a key rewritten to its canonical spelling, or a stringified array repaired after the parse failed (one increment per repaired call, not per repaired value). Nothing about any of them reaches the response, so a client artifact spreading across a fleet shows up here first. All three are lazy: a server whose callers never trip a stage emits no series at all.
151
157
 
152
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.
@@ -199,9 +205,9 @@ Read together: `queue_depth` rising while `wait` climbs means the configured rat
199
205
 
200
206
  ### Error category
201
207
 
202
- `mcp.tool.error_category` and `mcp.prompt.error_category` bucket a failure as `upstream` (an external dependency refused or timed out), `server` (a bug or this process's own infrastructure), or `client` (the request itself). The bucket comes from the classified JSON-RPC code, with one refinement: `RateLimited` (`-32003`) legitimately carries two sources, so the canvas tenant-cap refusal — which names itself with `data.reason: 'canvas_capacity_exhausted'` — files under `server`, and every other `-32003` stays `upstream`. Retry semantics and the HTTP 429 mapping are the same for both, which is why the code is shared and the stable `reason` discriminator does the separating.
208
+ `mcp.tool.error_category`, `mcp.prompt.error_category`, and `mcp.error.category` on `mcp.errors.classified` bucket a failure as `upstream` (an external dependency refused or timed out), `server` (a bug or this process's own infrastructure), or `client` (the request itself). The bucket comes from the JSON-RPC code the caller receives — for a thrown value that is not an `McpError`, the code the auto-classifier assigns, so `Error('Request timed out')` is `upstream` and a handler-thrown `ZodError` is `client` on every counter, and all three agree per failure. The span's and completion log's error code for such a value stays `UNHANDLED_ERROR` / `UNKNOWN_ERROR`. The one refinement: `RateLimited` (`-32003`) legitimately carries two sources, so the canvas tenant-cap refusal — which names itself with `data.reason: 'canvas_capacity_exhausted'` — files under `server`, and every other `-32003` stays `upstream`. Retry semantics and the HTTP 429 mapping are the same for both, which is why the code is shared and the stable `reason` discriminator does the separating.
203
209
 
204
- A dashboard reading `error_category` alone therefore no longer needs to special-case one server's capacity limit as an upstream outage. `reason` itself is not on the metric — it is unbounded across a fleet, so it lives on the span and in the log.
210
+ A dashboard reading `error_category` alone therefore no longer needs to special-case one server's capacity limit as an upstream outage, and one grouping `mcp.errors.classified` by origin reads `mcp.error.category` rather than decoding the code with its own copy of the table — the code cannot see `data.reason`. `reason` itself is not on the metric — it is unbounded across a fleet, so it lives on the span and in the log.
205
211
 
206
212
  ### Declared error severity
207
213
 
@@ -216,7 +222,7 @@ The call still failed: the execution span keeps `SpanStatusCode.ERROR` and its r
216
222
 
217
223
  | Metric | Type | Unit | Attributes |
218
224
  |:-------|:-----|:-----|:-----------|
219
- | `mcp.errors.classified` | counter | `{errors}` | `mcp.error.classified_code` (JSON-RPC code), `operation`, and `mcp.error.severity` when the failure's declared severity resolved |
225
+ | `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 the failure's declared severity resolved |
220
226
  | `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) |
221
227
  | `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) |
222
228
 
@@ -237,7 +243,7 @@ Auto-registered when `process.memoryUsage` / `process.uptime` / `perf_hooks` are
237
243
 
238
244
  ## Logs
239
245
 
240
- Pino logs are auto-instrumented by `@opentelemetry/instrumentation-pino`. When a span is active, `trace_id` and `span_id` are injected into the record. Combined with the framework logger's automatic `requestId`/`tenantId` correlation, every log line is searchable by trace.
246
+ 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).
241
247
 
242
248
  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 carries a `metrics` payload, with fields tuned to each surface:
243
249
 
@@ -247,6 +253,20 @@ For domain logging inside handlers, use `ctx.log` (`debug`/`info`/`notice`/`warn
247
253
  | Resource | `Resource read finished.` | `durationMs`, `isSuccess`, `errorCode`, `outputBytes`, `uri`, `mimeType` |
248
254
  | Prompt | `Prompt generation finished.` (or `failed.`) | `durationMs`, `isSuccess`, `errorCode`, `inputBytes`, `outputBytes`, `messageCount` |
249
255
 
256
+ ### Failed-call payloads
257
+
258
+ 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` included.
259
+
260
+ | Field | Content |
261
+ |:------|:--------|
262
+ | `toolInput` | The arguments as the caller sent them, before pre-validation drops or renames a key |
263
+ | `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 |
264
+ | `toolInputTruncated` / `toolResultTruncated` | Whether that payload was cut at `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` (default `16384`) |
265
+
266
+ 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.
267
+
268
+ 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.
269
+
250
270
  ---
251
271
 
252
272
  ## Custom instrumentation
@@ -4,7 +4,7 @@ description: >
4
4
  Testing patterns for MCP tool/resource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.11"
7
+ version: "1.12"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -99,7 +99,7 @@ try {
99
99
  }
100
100
  ```
101
101
 
102
- Routes match in registration order. `match` accepts an exact URL, `RegExp`, or request predicate; `respond` accepts a clonable `Response` or response factory. Set `once: true` for one-shot behavior. Unmatched requests throw unless `onUnhandled` is provided.
102
+ Routes match in registration order. `match` accepts an exact URL, `RegExp`, or request predicate; `respond` accepts a static `Response` or a response factory. A static response's body is read once, on the route's first match, and every call is served a fresh `Response` over those bytes with the same `status`, `statusText`, and headers — so a consumer that cancels the body, or an error-body reader like `httpErrorFromResponse` that stops past its cap, settles on Node as on Bun. Set `once: true` for one-shot behavior. Unmatched requests throw unless `onUnhandled` is provided.
103
103
 
104
104
  **A request predicate routes on the URL's origin, never a prefix.** `req.url.startsWith(BASE_URL)` also matches a lookalike host (`https://api.example.test.evil.com/...`), which CodeQL reports as high-severity incomplete URL substring sanitization — it scans test files as readily as `src/`, so a suite that is green locally still fails the security check on a pull request. Parse the URL and compare origins, matching the path separately:
105
105
 
@@ -133,7 +133,9 @@ toolContractSuite(searchTool, {
133
133
 
134
134
  Use `runToolContract(definition, input, { context })` from `/testing` when a custom test runner or an imperative assertion is a better fit. It intentionally skips transport auth and telemetry; those belong in transport/integration tests.
135
135
 
136
- Arguments that fail the `input` schema are rejected the way the production handler factory rejects them: `InvalidParams` (`-32602`), with a message naming the tool and every failing field. That is the code a client sees on the wire, so assert it — not `ValidationError` (`-32007`), which stays the classification for a `ZodError` a handler throws itself and for an output-schema rejection.
136
+ Arguments that fail the `input` schema are rejected the way the production handler factory rejects them: `InvalidParams` (`-32602`), with a message naming the tool and every failing field. That is the code a client sees on the wire, so assert it — not `ValidationError` (`-32007`), which stays the classification for a `ZodError` a handler throws itself. A result that breaks the tool's own `output` or `enrichment` schema is the definition's bug, so it returns `InternalError` (`-32603`) with a message naming that contract, exactly as in production.
137
+
138
+ Cancellation settles as it does in production. Pass `context: { signal }` and abort it: once the signal has fired, whatever the handler — or the output validation, `format()`, and enrichment after it — throws comes back as `RequestCancelled` (`-32011`), whether that is the signal's `AbortError`, its reason string, a `withRetry` backoff that stopped, or an `McpError` of the handler's own. A throw while the signal is still live keeps its own classification, and argument parsing stays outside the settle, so schema-invalid arguments on an aborted signal still return `InvalidParams`. A `toolContractSuite` error case with an aborted `context.signal` asserts `code: JsonRpcErrorCode.RequestCancelled` the same way.
137
139
 
138
140
  ---
139
141
 
@@ -4,7 +4,7 @@ description: >
4
4
  API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.12"
7
+ version: "2.13"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -31,11 +31,11 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
31
31
 
32
32
  | Export | API | Notes |
33
33
  |:-------|:----|:------|
34
- | `fetchWithTimeout` | `(url, timeoutMs, context, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `timeoutMs` bounds the **whole exchange**: on a 2xx carrying a body the returned `Response` is a passthrough wrapper that keeps the deadline armed until the body closes, errors, or is cancelled, so a stalled stream rejects the caller's `.text()`/`.json()` with the same `Timeout` error the header phase raises. `status`, `statusText`, `headers`, `url`, `redirected`, and `type` carry across the wrapper; the original body is locked by it, and bodyless/null-body responses (HEAD, 204/205/304) come back untouched. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean`, `expectedStatuses?: number[]` (listed non-2xx statuses logged at `debug` not `error`, still thrown), `errorBodyLimit?: number` (bytes of a non-2xx body kept, default `500`), `errorHeaders?: string[]` (response headers copied onto `error.data.headers` on a non-2xx — same selector as `httpErrorFromResponse` below; `location` is selectable under `redirect: 'manual'` but does **not** compose with `rejectPrivateIPs`, whose per-hop branch consumes the 3xx before the throw path sees it), and `signal?: AbortSignal` (external cancellation — an abort on it throws `RequestCancelled` (-32011), logged at `info` and outside `withRetry`'s transient set, since the caller is gone and no retry can reach them). On a non-2xx, `error.data` carries `status`/`body` plus the legacy `statusCode`/`responseBody` aliases (identical values; consolidating in a future major); a body over `errorBodyLimit` is captured from both ends — 40% head, 60% tail, joined by `…[N bytes elided]…` — so a diagnostic behind a boilerplate preamble survives the cap, while a body still streaming at the 16 KiB scan ceiling stays head-only with a trailing `…`. SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. **Both resolvers are queried** — `resolve4`/`resolve6` (c-ares) and `lookup` (the system resolver, which is what reads `/etc/hosts`, split DNS, and NSS modules) — and a non-global answer from either rejects. Runtimes differ in which resolver the connection uses (Bun 1.4 moved `net.connect()` on Linux to `getaddrinfo` while leaving `dns.resolve*()` on c-ares), so checking one alone leaves a name the other can see unguarded; each probe settles independently, so a resolver absent from the runtime is skipped rather than fatal. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. **Error/log redaction:** URLs written into thrown errors and log lines are reduced to `origin + pathname` — the query string (where API keys commonly ride: `?api-key=…`, `?api_key=…`) never reaches the client or the logs. The actual request still uses the full URL. |
34
+ | `fetchWithTimeout` | `(url, timeoutMs, context, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `timeoutMs` bounds the **whole exchange**: on a 2xx carrying a body the returned `Response` is a passthrough wrapper that keeps the deadline armed until the body closes, errors, or is cancelled, so a stalled stream rejects the caller's `.text()`/`.json()` with the same `Timeout` error the header phase raises. `status`, `statusText`, `headers`, `url`, `redirected`, and `type` carry across the wrapper; the original body is locked by it, and bodyless/null-body responses (HEAD, 204/205/304) come back untouched. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean`, `expectedStatuses?: number[]` (listed non-2xx statuses logged at `debug` not `error`, still thrown), `errorBodyLimit?: number` (bytes of a non-2xx body kept, default `500`), `errorHeaders?: string[]` (response headers copied onto `error.data.headers` on a non-2xx — same selector as `httpErrorFromResponse` below; `location` is selectable under `redirect: 'manual'` but does **not** compose with `rejectPrivateIPs`, whose per-hop branch consumes the 3xx before the throw path sees it), and `signal?: AbortSignal` (external cancellation — an abort on it throws `RequestCancelled` (-32011), logged at `info` and outside `withRetry`'s transient set, since the caller is gone and no retry can reach them; an abort whose reason is a `TimeoutError` — `AbortSignal.timeout()`, or an `AbortSignal.any` whose timeout member fired — is a deadline instead, and throws `Timeout` (-32004) with `data.errorSource: 'FetchSignalTimeout'`, distinct from the helper's own `timeoutMs` expiry, `'FetchTimeout'`, and likewise outside `withRetry`'s default transient set, since every retry would reuse the fired signal). Validation rejections carry `data.reason` and a `recovery.hint`: `invalid_url` (not an absolute `http:`/`https:` URL, including a redirect target), `private_address_blocked` (the SSRF guard refused the host by name, literal IP, or DNS answer), `too_many_redirects` (past the 5-hop cap, with `data.maxRedirects`). On a non-2xx, `error.data` carries `status`/`body` plus the legacy `statusCode`/`responseBody` aliases (identical values; consolidating in a future major); a body over `errorBodyLimit` is captured from both ends — 40% head, 60% tail, joined by `…[N bytes elided]…` — so a diagnostic behind a boilerplate preamble survives the cap, while a body still streaming at the 16 KiB scan ceiling stays head-only with a trailing `…`. SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. **Both resolvers are queried** — `resolve4`/`resolve6` (c-ares) and `lookup` (the system resolver, which is what reads `/etc/hosts`, split DNS, and NSS modules) — and a non-global answer from either rejects. Runtimes differ in which resolver the connection uses (Bun 1.4 moved `net.connect()` on Linux to `getaddrinfo` while leaving `dns.resolve*()` on c-ares), so checking one alone leaves a name the other can see unguarded; each probe settles independently, so a resolver absent from the runtime is skipped rather than fatal. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. **Error/log redaction:** URLs written into thrown errors and log lines are reduced to `origin + pathname` — the query string (where API keys commonly ride: `?api-key=…`, `?api_key=…`) never reaches the client or the logs. The actual request still uses the full URL. |
35
35
  | `withRetry` | `<T>(fn: (attempt: RetryAttempt) => Promise<T>, options?: RetryOptions) -> Promise<T>` | Executes `fn` with exponential backoff. Retries on transient errors (`ServiceUnavailable`, `Timeout`, `RateLimited`); non-transient errors fail immediately. Honors an upstream `Retry-After` on `data.retryAfter` (delta-seconds or HTTP-date) over exponential backoff, capped at `maxDelayMs`; a requested wait beyond the cap fails fast rather than sleeping. On exhaustion, enriches the final error with attempt count in message and `data.retryAttempts`. **Place the retry boundary around the full pipeline** (fetch + parse), not just the network call. `RetryOptions`: `maxRetries` (default `3`), `baseDelayMs` (default `1000`), `maxDelayMs` (default `30000`), `jitter` (default `0.25`), `operation` (log label), `context` (RequestContext), `signal` (AbortSignal), `isTransient` (custom predicate), `deadlineMs` (total wall-clock budget — see below). |
36
36
  | `RetryAttempt` | `{ readonly signal: AbortSignal; readonly remainingMs: number }` | What `fn` receives each attempt. `signal` is `AbortSignal.any` over the `deadlineMs` clock and `options.signal`; `remainingMs` is what is left of the total budget as the attempt starts, never negative and `Number.POSITIVE_INFINITY` when no deadline is set — so `Math.min(perAttemptMs, remainingMs)` is correct either way. A zero-argument `fn` stays assignable, so existing callers compile unchanged. |
37
- | `deadlineMs` | `RetryOptions` field | One wall-clock budget across every attempt, backoff, and honored `Retry-After` — the bound `maxRetries` plus a per-attempt timeout cannot express. Four 30s attempts outlast a client's 60s request timeout, so the caller gets a transport timeout instead of the server's classified error. **Thread `attempt.signal` into the attempt's I/O** (`fetchWithTimeout(url, Math.min(30_000, remainingMs), ctx, { signal })`) or the deadline overshoots by one in-flight request. Clock is `AbortController` + `setTimeout` (never `AbortSignal.timeout()`, per the Bun realm mismatch), cleared on return — no timer outlives the call. Expiry rejects with `Timeout` (-32004) carrying `data: { reason: 'retry_deadline_exceeded', deadlineMs, elapsedMs, retryAttempts }` and the last attempt's error as `cause`; **one shape for every expiry**, including the `RequestCancelled` that an external-signal abort raises inside `fetchWithTimeout` and the raw abort reason a mid-backoff expiry would otherwise surface. No `retryable` flag (a narrower call can still succeed) and no `attempt` index (`retryAttempts` carries it). A backoff that would outlast the remaining budget fails fast with the expiry instead of sleeping into a certain timeout; an honored `Retry-After` that would outlast it takes the `maxDelayMs` exit instead — the attempt's error unchanged, `data.retryAfter` intact, since "wait the window the upstream named" is still the caller's action. **Three clocks stay distinct:** a caller abort on `options.signal` keeps precedence and rethrows unchanged (stamped `RequestCancelled` by the handler factory), a single attempt's timeout is `Timeout` with `errorSource: 'FetchTimeout'` and no `reason`, and the expiry is `Timeout` with the `reason`. Unset, behavior is identical to before — attempt counts, delays, log lines, and the exhausted-error shape untouched. Bounds **one** ladder: a tool making three upstream calls threads its own remaining budget into each. |
38
- | `defaultIsTransient` | `(error: unknown) -> boolean` | The predicate `withRetry` uses when `isTransient` is omitted: an `McpError` with a transient code (`ServiceUnavailable`, `Timeout`, `RateLimited`) unless it carries `data.retryable === false` or `data.reason === 'pacer_shed'`; any non-`McpError` throw is assumed transient. Exported so `isTransient` — which **replaces** the default outright — can compose instead of mirroring the transient set, which drifts silently when the framework's classification changes: `isTransient: (error) => !isMyBudgetRefusal(error) && defaultIsTransient(error)`, or the inverse `defaultIsTransient(error) \|\| isMyRetryableShape(error)`. The transient code set itself stays private (a module-level `Set` an exported binding could be mutated into framework-wide retry behavior). |
37
+ | `deadlineMs` | `RetryOptions` field | One wall-clock budget across every attempt, backoff, and honored `Retry-After` — the bound `maxRetries` plus a per-attempt timeout cannot express. Four 30s attempts outlast a client's 60s request timeout, so the caller gets a transport timeout instead of the server's classified error. **Thread `attempt.signal` into the attempt's I/O** (`fetchWithTimeout(url, Math.min(30_000, remainingMs), ctx, { signal })`) or the deadline overshoots by one in-flight request. Clock is `AbortController` + `setTimeout` (never `AbortSignal.timeout()`, per the Bun realm mismatch), cleared on return — no timer outlives the call. Expiry rejects with `Timeout` (-32004) carrying `data: { reason: 'retry_deadline_exceeded', deadlineMs, elapsedMs, retryAttempts }` and the last attempt's error as `cause`; **one shape for every expiry**, including the per-attempt `Timeout` (`errorSource: 'FetchSignalTimeout'`) the clock's abort raises inside `fetchWithTimeout` and the raw abort reason a mid-backoff expiry would otherwise surface. No `retryable` flag (a narrower call can still succeed) and no `attempt` index (`retryAttempts` carries it). A backoff that would outlast the remaining budget fails fast with the expiry instead of sleeping into a certain timeout; an honored `Retry-After` that would outlast it takes the `maxDelayMs` exit instead — the attempt's error unchanged, `data.retryAfter` intact, since "wait the window the upstream named" is still the caller's action. **Three clocks stay distinct:** a caller abort on `options.signal` keeps precedence — mid-attempt it rethrows the attempt's error unchanged, mid-backoff it rejects with `signal.reason` itself (an `AbortError` `DOMException` for a reason-less `abort()`), and the handler factory reports either as `RequestCancelled` when the request signal is the one that fired — a single attempt's timeout is `Timeout` with `errorSource: 'FetchTimeout'` and no `reason`, and the expiry is `Timeout` with the `reason`. Unset, behavior is identical to before — attempt counts, delays, log lines, and the exhausted-error shape untouched. Bounds **one** ladder: a tool making three upstream calls threads its own remaining budget into each. |
38
+ | `defaultIsTransient` | `(error: unknown) -> boolean` | The predicate `withRetry` uses when `isTransient` is omitted: an `McpError` with a transient code (`ServiceUnavailable`, `Timeout`, `RateLimited`) unless it carries `data.retryable === false`, `data.reason === 'pacer_shed'`, or `data.errorSource === 'FetchSignalTimeout'` (a caller-side deadline that already fired); any non-`McpError` throw is assumed transient. Exported so `isTransient` — which **replaces** the default outright — can compose instead of mirroring the transient set, which drifts silently when the framework's classification changes: `isTransient: (error) => !isMyBudgetRefusal(error) && defaultIsTransient(error)`, or the inverse `defaultIsTransient(error) \|\| isMyRetryableShape(error)`. The transient code set itself stays private (a module-level `Set` an exported binding could be mutated into framework-wide retry behavior). |
39
39
  | `httpErrorFromResponse` | `(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>` | Maps an HTTP `Response` to a properly classified `McpError` — full status table including 401/403/408/422/429/5xx, body capture (truncated), `retry-after` header, optional `cause`. `error.data` carries `status`/`body` plus the legacy `statusCode`/`responseBody` aliases (identical values), so a consumer can classify either helper's error without knowing which raised it. Use this instead of hand-rolling `if (status === 429) ...` ladders. Reads the response body — `clone()` first if you need it elsewhere. **`error.data` is client-facing** — the framework forwards it verbatim as `structuredContent.error.data` — so the full upstream URL is **omitted by default**: a request URL routinely carries user input, internal identifiers, or an API key in its query string. `includeUrl: true` opts into `data.url` carrying the full `response.url`; with an empty `response.url` no key is added either way, and the message still names the host. Response headers are opt-in on the same footing: `errorHeaders: ['x-ratelimit-remaining-usd', 'x-request-id']` copies the named headers onto `data.headers` under **lowercase** keys — selection is case-insensitive and entries differing only in case collapse to one key, presence follows `Headers.has()` (an empty value is captured as `''`, an absent header adds no key), and a multi-valued field is captured comma-joined as `Headers.get()` returns it. Omitted, empty, or matching nothing, no `headers` key is emitted. `set-cookie` is **never** captured whatever the selector says: it is credential-bearing and `Headers.get()` joins its values into a string that is not a valid reconstruction. Every selected value reaches the client, so never name a header that carries a credential — and a selected `Location` can itself carry a sensitive path, query, or token. `HttpErrorFromResponseOptions`: `service?` (logical name in message, e.g. `'NCBI'`), `captureBody?` (default `true`), `bodyLimit?` (default `500`), `includeUrl?` (default `false`), `errorHeaders?` (default none), `data?` (extra fields merged into `error.data`, overriding defaults on key collision — a caller's own `url` or `headers` still reaches the wire), `cause?`, `codeOverride?` (per-status mapping override). Pairs naturally with `withRetry` — both classify codes the same way. A 501 also carries `data.retryable: false`, so retry fails it fast instead of re-asking for a method the upstream does not implement. |
40
40
  | `createPacer` | `(options: PacerOptions) -> Pacer` | FIFO queue in front of one rate-limited upstream — the outbound counterpart to `RateLimiter` (`utils/security`), which is inbound, per-caller, and reject-only, so it cannot queue work against an upstream budget. `pacer.run(task, { signal?, maxWaitMs? })` holds `task` until every `limits` window, `minStartGapMs`, `maxConcurrent`, and the cooldown gate allow it, then calls it with the caller's signal. `PacerOptions`: `name` (author-set telemetry label), `limits` (`{ requests, perMs }[]` — each a sliding window over recorded **start** times, so a slow response never widens the rate the upstream sees; all must allow a start), `minStartGapMs` (**not** expressible through `limits`: `{ requests: 10, perMs: 1000 }` permits ten starts in the same millisecond), `maxConcurrent`, `maxQueueDepth` (absolute backpressure for callers passing no `maxWaitMs`; rejects without arming a timer), `cooldown` (`{ baseMs, maxMs }`). **Shed:** `maxWaitMs` bounds queue time only, never the task. The projected wait is exact over the windows and the gap but a lower bound once `maxConcurrent` binds (a slot frees on an unknowable completion), so enqueue rejects only when that lower bound already exceeds `maxWaitMs` — no false sheds — and a still-queued entry rejects when `maxWaitMs` elapses. The shed error is `rateLimited` (-32003) with `data: { reason: 'pacer_shed', retryAfter, queueDepth }` and **no `retryable: false`** — to the calling agent a shed is an ordinary rate limit (wait `retryAfter`, call again) and that flag would say the opposite; `defaultIsTransient` reads the `reason` instead, so an enclosing `withRetry` fails fast rather than sleeping past the deadline the shed enforces. **Cooldown gate:** a `RateLimited` thrown by the task closes the gate for every queued caller until an absolute instant, `min(max(baseMs · 2^(consecutive−1), retryAfter), maxMs)` — `maxMs` caps both the doubling and an honored `Retry-After`, so a pathological upstream value cannot park the queue. Absent or unparseable `retryAfter` leaves the doubling; any other error leaves the gate open; the first success resets the count. **Composition:** `withRetry(({ signal }) => pacer.run(fn, { signal }), { signal, deadlineMs })` — retry outside, pacer inside, so each attempt re-queues and is re-paced. Because the gate is an absolute instant rather than a duration counted from dequeue, retry's `Retry-After` sleep and the gate overlap in wall-clock instead of summing: the window is waited once, not twice. **Lifecycle:** timers and `AbortSignal` only, process-local; the dispatch timer is `unref()`'d where supported; `dispose()` / `[Symbol.dispose]()` clears it and rejects queued waiters with `RequestCancelled` (in-flight tasks are left to finish) — wire it through `createApp({ teardown })`. On Workers state is per-isolate so the limits bind per isolate, OTel is off so the metrics are inert, and `createWorkerHandler` accepts no `teardown`. Metrics: `mcp.pacer.queue_depth`, `mcp.pacer.wait`, `mcp.pacer.sheds`, `mcp.pacer.cooldowns`, attributed by `mcp.pacer.name` only — see `api-telemetry`. |
41
41
  | `httpStatusToErrorCode` | `(status: number) -> JsonRpcErrorCode \| undefined` | Sync status → code lookup. Returns `undefined` for 1xx/2xx. A 3xx maps to `InvalidRequest` — it reaches error mapping under `redirect: 'manual'`, where the request as sent cannot be served at this URL, and that code is outside `withRetry`'s transient set since re-issuing returns the same redirect. Use when you need just the code without a `Response` object handy. No status maps to `InternalError` — that code means *this* server failed, which a remote status cannot establish; every 5xx is `ServiceUnavailable` (or `Timeout` for 504) and so picks up `withRetry`'s default transient policy. |
@@ -47,9 +47,9 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
47
47
  | Export | API | Notes |
48
48
  |:-------|:----|:------|
49
49
  | `extractCursor` | `(params?) -> string \| undefined` | Extracts opaque cursor string from MCP request params. Checks `params.cursor` then `params._meta.cursor`. Returns `undefined` when no cursor is present. Does not decode. |
50
- | `paginateArray` | `<T>(items, cursorStr, defaultPageSize, maxPageSize, context: RequestContext) -> PaginatedResult<T>` | Decodes cursor, slices array, returns `{ items, nextCursor?, totalCount }`. `nextCursor` omitted on last page. Throws `McpError(InvalidParams)` on invalid cursor. On a continued call the page size comes from the cursor, not `defaultPageSize` — a tool with a caller-facing `limit` input must slice on `decodeCursor(...).offset` itself to honor `limit` past page 1. |
50
+ | `paginateArray` | `<T>(items, cursorStr, defaultPageSize, maxPageSize, context: RequestContext) -> PaginatedResult<T>` | Decodes cursor, slices array, returns `{ items, nextCursor?, totalCount }`. `nextCursor` omitted on last page. Throws `McpError(InvalidParams)` on invalid cursor, inherited from `decodeCursor` (`data.reason: 'invalid_cursor'`). On a continued call the page size comes from the cursor, not `defaultPageSize` — a tool with a caller-facing `limit` input must slice on `decodeCursor(...).offset` itself to honor `limit` past page 1. |
51
51
  | `encodeCursor` | `(state: PaginationState) -> string` | Encodes `{ offset, limit, ...extra }` to opaque base64url string. |
52
- | `decodeCursor` | `(cursor, context: RequestContext) -> PaginationState` | Decodes opaque base64url cursor. Throws `McpError(InvalidParams)` if malformed. |
52
+ | `decodeCursor` | `(cursor, context: RequestContext) -> PaginationState` | Decodes opaque base64url cursor. Throws `McpError(InvalidParams)` if malformed, with `data: { cursor, reason: 'invalid_cursor', recovery: { hint } }` — the hint tells the caller to omit `cursor` or pass the previous `nextCursor` unchanged, so the rejection renders the `Recovery:` line and `(reason invalid_cursor)` trailer like any other classified failure. |
53
53
 
54
54
  ---
55
55
 
@@ -85,7 +85,7 @@ The `utils` export includes two type guards. The full set of guards lives in the
85
85
  | Export | API | Notes |
86
86
  |:-------|:----|:------|
87
87
  | `Logger` | Class | The `Logger` class itself. Use `Logger.getInstance()` if needed; most consumers use the `logger` singleton. |
88
- | `logger` | `Logger` instance (wraps Pino). `.debug(msg, ctx?)` `.info(msg, ctx?)` `.notice(msg, ctx?)` `.warning(msg, ctx?)` `.error(msg, errorOrCtx, ctx?)` `.crit(msg, errorOrCtx, ctx?)` `.alert(msg, errorOrCtx, ctx?)` `.emerg(msg, errorOrCtx, ctx?)` `.fatal(msg, errorOrCtx, ctx?)` | Global structured logger. Use `ctx.log` in handlers instead. `logger` is for lifecycle/background contexts (startup, shutdown, `setup()`). Auto-redacts sensitive fields. Records logged before the framework initializes the logger — anything in `setup()` — are held in a 250-record buffer and replayed once the sinks exist, filtered against the level the logger starts with. **Note:** `.error()` and higher accept `(msg, Error, ctx?)` or `(msg, ctx?)` — the second arg is overloaded. `.fatal()` is an alias for `.emerg()`. Full RFC 5424 severity set. |
88
+ | `logger` | `Logger` instance (wraps Pino). `.debug(msg, ctx?)` `.info(msg, ctx?)` `.notice(msg, ctx?)` `.warning(msg, ctx?)` `.error(msg, errorOrCtx, ctx?)` `.crit(msg, errorOrCtx, ctx?)` `.alert(msg, errorOrCtx, ctx?)` `.emerg(msg, errorOrCtx, ctx?)` `.fatal(msg, errorOrCtx, ctx?)` | Global structured logger. Use `ctx.log` in handlers instead. `logger` is for lifecycle/background contexts (startup, shutdown, `setup()`). Auto-redacts sensitive fields. The context's `extra` bag is flattened into the record, but a canonical field the context carries (`requestId`, `timestamp`, `traceId`, `spanId`, `sessionId`, `tenantId`, `operation`) always wins over an `extra` key of the same name. Records logged before the framework initializes the logger — anything in `setup()` — are held in a 250-record buffer and replayed once the sinks exist, filtered against the level the logger starts with. **Note:** `.error()` and higher accept `(msg, Error, ctx?)` or `(msg, ctx?)` — the second arg is overloaded. `.fatal()` is an alias for `.emerg()`. Full RFC 5424 severity set. |
89
89
  | `McpLogLevel` | Type | Log level union type for typing level variables. |
90
90
 
91
91
  ---
@@ -109,7 +109,7 @@ The `utils` export includes two type guards. The full set of guards lives in the
109
109
 
110
110
  | Export | API | Notes |
111
111
  |:-------|:----|:------|
112
- | `ErrorHandler` | `.tryCatch<T>(fn, opts) -> Promise<T>` `.handleError(error, opts) -> Error` `.classifyOnly(error) -> { code, message, data? }` `.determineErrorCode(error) -> JsonRpcErrorCode` `.mapError(error, mappings, defaultFactory?) -> T \| Error` `.formatError(error) -> Record<string, unknown>` | Service-level error handling. `tryCatch` wraps async or sync `fn`, logs via `handleError`, and always rethrows. No `.tryCatchSync()`. Use in services, NOT in tool handlers (those throw raw `McpError`). `tryCatch` accepts `Omit<ErrorHandlerOptions, 'rethrow'>` — required: `operation`. Optional: `context`, `errorCode`, `input`, `includeStack`, `critical`, `errorMapper`. `handleError` accepts the full `ErrorHandlerOptions` including `rethrow`. |
112
+ | `ErrorHandler` | `.tryCatch<T>(fn, opts) -> Promise<T>` `.handleError(error, opts) -> Error` `.classifyOnly(error) -> { code, message, data? }` `.determineErrorCode(error) -> JsonRpcErrorCode` `.mapError(error, mappings, defaultFactory?) -> T \| Error` `.formatError(error) -> Record<string, unknown>` | Service-level error handling. `tryCatch` wraps async or sync `fn`, logs via `handleError`, and always rethrows. No `.tryCatchSync()`. Use in services, NOT in tool handlers (those throw raw `McpError`). `tryCatch` accepts `Omit<ErrorHandlerOptions, 'rethrow'>` — required: `operation`. Optional: `context`, `errorCode`, `input`, `includeStack`, `critical`, `errorMapper`. `handleError` accepts the full `ErrorHandlerOptions` including `rethrow`. The returned error's `data` (client-visible once thrown toward a handler) keeps the caught `McpError`'s own `data` plus `originalErrorName`/`originalMessage`/`rootCause`; `context` goes to the log record only. |
113
113
 
114
114
  ---
115
115
 
@@ -148,7 +148,7 @@ Helper API only. For the catalog of what the framework auto-emits (span names, m
148
148
 
149
149
  | Export | Signature | Notes |
150
150
  |:-------|:----------|:------|
151
- | `initializeOpenTelemetry` | `() -> Promise<void>` | Idempotent. Initializes `NodeSDK` with OTLP trace + metrics exporters, `TraceIdRatioBasedSampler`, HTTP instrumentation, and Pino log injection. No-ops when `OTEL_ENABLED=false` or in Worker/Edge runtimes where `NodeSDK` is unavailable. Safe to call multiple times. |
151
+ | `initializeOpenTelemetry` | `() -> Promise<void>` | Idempotent. Initializes `NodeSDK` with OTLP trace + metrics exporters, `TraceIdRatioBasedSampler`, and HTTP instrumentation, and attaches the framework logger's OTLP log sink when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` is set. Framework log records carry `traceId`/`spanId` from the request context; `PinoInstrumentation` is registered but patches only a `pino` loaded after the SDK starts, never the framework logger's. No-ops when `OTEL_ENABLED=false` or in Worker/Edge runtimes where `NodeSDK` is unavailable. Safe to call multiple times. |
152
152
  | `shutdownOpenTelemetry` | `(timeoutMs?: number) -> Promise<void>` | Gracefully flushes and shuts down the SDK. `timeoutMs` defaults to `5000`. Resets internal state so the next `initializeOpenTelemetry()` call can reinitialize. No-op when SDK was never started. |
153
153
  | `sdk` | `NodeSDK \| null` | The live SDK instance, or `null` when telemetry is disabled, in a Worker runtime, or after shutdown. |
154
154
 
@@ -22,7 +22,7 @@ import { markdown, MarkdownBuilder, diffFormatter, tableFormatter, treeFormatter
22
22
  | `keyValuePlain` | `(key, value) -> this` | `key: value` (no bold) |
23
23
  | `list` | `(items, ordered?) -> this` | `ordered` defaults to `false`; empty arrays silently ignored |
24
24
  | `codeBlock` | `(content, language?) -> this` | Fenced block; `language` defaults to `''`. The fence outgrows the longest backtick run in `content`, so a payload the tool did not author (upstream text, a file excerpt) cannot break out of the block; content is emitted byte-for-byte |
25
- | `inlineCode` | `(code) -> this` | Backtick-wrapped; no trailing newline |
25
+ | `inlineCode` | `(code) -> this` | Code span; no trailing newline. The backtick delimiter outgrows the longest backtick run in `code`, space-padded when a backtick touches either end or the value both begins and ends with a space, so a value the tool did not author reads back byte-identical as one span and cannot break out into live markdown. A value with no backtick that does not both begin and end with a space renders as `` `code` `` |
26
26
  | `paragraph` | `(text) -> this` | Text + `\n\n` |
27
27
  | `blockquote` | `(text) -> this` | Each line prefixed with `>` + space |
28
28
  | `hr` | `() -> this` | `---` |
@@ -12,7 +12,7 @@ All parsers are **Tier 3** — lazy-load their peer dependency on first call. Al
12
12
  - `<think>...</think>` blocks at the start of input are automatically stripped and logged at `debug` level (except `dateParser` and `pdfParser`)
13
13
  - Every `context?` parameter is optional (synthetic context created if omitted) and accepts the handler `Context` as well as a `RequestContext` bag
14
14
  - Input budgets are opt-in: a parser is unbounded unless the caller passes `maxBytes`, which then rejects an over-budget input with `ValidationError` (`reason: 'parser_input_too_large'`). `DEFAULT_TEXT_PARSER_MAX_BYTES` (1 MiB) and `DEFAULT_BINARY_PARSER_MAX_BYTES` (25 MiB) are exported as starting points, not applied defaults
15
- - Errors throw `McpError` — never return error values. The message is `<summary>: <library message>`, so it carries the underlying parser's diagnostic; `data` carries only `{ reason }`, and the input sample and stack stay on `cause`
15
+ - Errors throw `McpError` — never return error values. The message is `<summary>: <library message>`, so it carries the underlying parser's diagnostic; `data` carries only `{ reason }` (`csvParser` adds Papa's `errors` list and a content sample), and the input sample and stack stay on `cause`. Input that is empty after `<think>` stripping and trimming rejects with `ValidationError` (`reason: 'parser_input_empty'`). The `context` you pass is for log correlation only — it never becomes error `data`, which reaches the client
16
16
 
17
17
  ---
18
18
 
@@ -56,7 +56,7 @@ const data = await xmlParser.parse<FeedResponse>(xmlString);
56
56
  |:-------|:----------|
57
57
  | `parse` | `<T = unknown>(csvString, options?, context?) -> Promise<Papa.ParseResult<T>>` |
58
58
 
59
- `options` is `Papa.ParseConfig` forwarded verbatim — key options: `header`, `delimiter`, `dynamicTyping`. Returns `{ data: T[], errors: ParseError[], meta: ParseMeta }`. Throws `ValidationError` if `result.errors` is non-empty.
59
+ `options` is `Papa.ParseConfig` forwarded verbatim — key options: `header`, `delimiter`, `dynamicTyping`. Returns `{ data: T[], errors: ParseError[], meta: ParseMeta }`. Throws `ValidationError` if `result.errors` is non-empty, with `data: { reason: 'csv_parse_failed', errors, originalContentSample }`.
60
60
 
61
61
  ```ts
62
62
  const result = await csvParser.parse<Row>(csvString, { header: true, dynamicTyping: true });