@cyanheads/mcp-ts-core 0.13.11 → 0.13.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENTS.md +9 -8
  2. package/CLAUDE.md +9 -8
  3. package/README.md +1 -1
  4. package/changelog/0.13.x/0.13.12.md +50 -0
  5. package/changelog/0.13.x/0.13.13.md +93 -0
  6. package/dist/core/context.d.ts +12 -0
  7. package/dist/core/context.d.ts.map +1 -1
  8. package/dist/core/context.js +59 -14
  9. package/dist/core/context.js.map +1 -1
  10. package/dist/core/worker.d.ts.map +1 -1
  11. package/dist/core/worker.js +21 -8
  12. package/dist/core/worker.js.map +1 -1
  13. package/dist/mcp-server/handlerContext.d.ts +14 -8
  14. package/dist/mcp-server/handlerContext.d.ts.map +1 -1
  15. package/dist/mcp-server/handlerContext.js +16 -9
  16. package/dist/mcp-server/handlerContext.js.map +1 -1
  17. package/dist/mcp-server/inputRequired.d.ts +18 -9
  18. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  19. package/dist/mcp-server/inputRequired.js +29 -15
  20. package/dist/mcp-server/inputRequired.js.map +1 -1
  21. package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
  22. package/dist/mcp-server/prompts/prompt-registration.js +10 -7
  23. package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
  24. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  25. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +29 -19
  26. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  27. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +8 -2
  28. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/utils/inputPrevalidation.js +18 -7
  30. package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
  31. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +7 -1
  32. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +107 -21
  34. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  35. package/dist/mcp-server/transports/auth/lib/authUtils.d.ts +22 -0
  36. package/dist/mcp-server/transports/auth/lib/authUtils.d.ts.map +1 -1
  37. package/dist/mcp-server/transports/auth/lib/authUtils.js +29 -1
  38. package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
  39. package/dist/mcp-server/transports/auth/lib/checkScopes.d.ts.map +1 -1
  40. package/dist/mcp-server/transports/auth/lib/checkScopes.js +2 -1
  41. package/dist/mcp-server/transports/auth/lib/checkScopes.js.map +1 -1
  42. package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
  43. package/dist/mcp-server/transports/http/httpErrorHandler.js +4 -2
  44. package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
  45. package/dist/services/mirror/sqlite/handle.d.ts +13 -2
  46. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
  47. package/dist/services/mirror/sqlite/handle.js +17 -6
  48. package/dist/services/mirror/sqlite/handle.js.map +1 -1
  49. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
  50. package/dist/services/mirror/sqlite/sqliteMirrorStore.js +3 -2
  51. package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
  52. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
  53. package/dist/storage/providers/fileSystem/fileSystemProvider.js +48 -21
  54. package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
  55. package/dist/types-global/errors.d.ts.map +1 -1
  56. package/dist/types-global/errors.js +31 -16
  57. package/dist/types-global/errors.js.map +1 -1
  58. package/dist/utils/internal/error-handler/errorHandler.d.ts +29 -11
  59. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  60. package/dist/utils/internal/error-handler/errorHandler.js +204 -95
  61. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  62. package/dist/utils/internal/error-handler/helpers.d.ts +91 -9
  63. package/dist/utils/internal/error-handler/helpers.d.ts.map +1 -1
  64. package/dist/utils/internal/error-handler/helpers.js +243 -39
  65. package/dist/utils/internal/error-handler/helpers.js.map +1 -1
  66. package/dist/utils/internal/error-handler/types.d.ts +21 -4
  67. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  68. package/dist/utils/internal/logValue.d.ts +33 -0
  69. package/dist/utils/internal/logValue.d.ts.map +1 -0
  70. package/dist/utils/internal/logValue.js +539 -0
  71. package/dist/utils/internal/logValue.js.map +1 -0
  72. package/dist/utils/internal/logger.d.ts +38 -9
  73. package/dist/utils/internal/logger.d.ts.map +1 -1
  74. package/dist/utils/internal/logger.js +228 -118
  75. package/dist/utils/internal/logger.js.map +1 -1
  76. package/dist/utils/internal/observabilityCap.d.ts +35 -0
  77. package/dist/utils/internal/observabilityCap.d.ts.map +1 -0
  78. package/dist/utils/internal/observabilityCap.js +43 -0
  79. package/dist/utils/internal/observabilityCap.js.map +1 -0
  80. package/dist/utils/internal/performance.d.ts.map +1 -1
  81. package/dist/utils/internal/performance.js +9 -11
  82. package/dist/utils/internal/performance.js.map +1 -1
  83. package/dist/utils/internal/requestContext.d.ts +3 -3
  84. package/dist/utils/internal/requestContext.js +1 -1
  85. package/dist/utils/network/fetchWithTimeout.d.ts +20 -10
  86. package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
  87. package/dist/utils/network/fetchWithTimeout.js +112 -30
  88. package/dist/utils/network/fetchWithTimeout.js.map +1 -1
  89. package/dist/utils/network/httpError.d.ts +8 -6
  90. package/dist/utils/network/httpError.d.ts.map +1 -1
  91. package/dist/utils/network/httpError.js +23 -7
  92. package/dist/utils/network/httpError.js.map +1 -1
  93. package/dist/utils/network/retry.d.ts +25 -2
  94. package/dist/utils/network/retry.d.ts.map +1 -1
  95. package/dist/utils/network/retry.js +47 -5
  96. package/dist/utils/network/retry.js.map +1 -1
  97. package/dist/utils/security/sanitization.d.ts +24 -28
  98. package/dist/utils/security/sanitization.d.ts.map +1 -1
  99. package/dist/utils/security/sanitization.js +25 -84
  100. package/dist/utils/security/sanitization.js.map +1 -1
  101. package/dist/utils/security/sensitiveFields.d.ts +32 -4
  102. package/dist/utils/security/sensitiveFields.d.ts.map +1 -1
  103. package/dist/utils/security/sensitiveFields.js +85 -4
  104. package/dist/utils/security/sensitiveFields.js.map +1 -1
  105. package/dist/utils/telemetry/trace.d.ts +3 -1
  106. package/dist/utils/telemetry/trace.d.ts.map +1 -1
  107. package/dist/utils/telemetry/trace.js +6 -6
  108. package/dist/utils/telemetry/trace.js.map +1 -1
  109. package/framework-skills/api-auth/SKILL.md +3 -1
  110. package/framework-skills/api-canvas/SKILL.md +2 -2
  111. package/framework-skills/api-config/SKILL.md +2 -1
  112. package/framework-skills/api-context/SKILL.md +6 -6
  113. package/framework-skills/api-errors/SKILL.md +19 -17
  114. package/framework-skills/api-linter/SKILL.md +11 -10
  115. package/framework-skills/api-mirror/SKILL.md +3 -1
  116. package/framework-skills/api-telemetry/SKILL.md +14 -8
  117. package/framework-skills/api-utils/SKILL.md +6 -6
  118. package/framework-skills/api-utils/references/security.md +4 -2
  119. package/framework-skills/field-test/SKILL.md +2 -2
  120. package/framework-skills/git-wrapup/SKILL.md +3 -3
  121. package/framework-skills/tool-defs-analysis/SKILL.md +2 -2
  122. package/package.json +10 -9
  123. package/scripts/check-framework-antipatterns.ts +3 -2
  124. package/templates/.env.example +2 -0
  125. package/templates/tests/tools/echo.tool.test.ts +19 -1
@@ -41,11 +41,19 @@ interface FileSinkProbe {
41
41
  */
42
42
  export declare function probeFileSinks(dir: string): Promise<FileSinkProbe>;
43
43
  /**
44
- * Pino `formatters.log` hook. Strips values that break pino-redact's wildcard
45
- * traversal on Node 25+ — notably `AbortSignal` (via `ctx.signal`) and the
46
- * method-bearing handles on the framework `Context` (`log`, `state`, `content`,
47
- * `enrich`, `inputs`, `requestInput`, `notifyResource*`). Acts as a safety net
48
- * for every log call regardless of how callers shaped their bindings.
44
+ * Pino `formatters.log` hook, and the bindings of every exported OTel record:
45
+ * the log-data walk ({@link toLogValue}) over the whole record. pino
46
+ * therefore never receives an `Error` or a class instance — the method-bearing
47
+ * handles on the framework `Context` and `ctx.signal` included — whatever
48
+ * shape a caller gave its bindings. The walk is the only redaction: it is the
49
+ * one path caller data takes into a written line, since neither pino logger
50
+ * sets a `mixin`, binds a child, or carries anything in `base` but constants.
51
+ * Exempt from it are the root correlation fields ({@link CORRELATION_KEYS})
52
+ * that the record's context supplied, and a root key named after a field pino
53
+ * writes on the line itself ({@link LINE_FIELDS}) is written as `data_<name>` —
54
+ * prefixed again while that name is taken. `err` is such a field on a line
55
+ * carrying an error argument, which {@link Logger} moves the same way before
56
+ * attaching the error.
49
57
  *
50
58
  * @internal Exported only for unit testing. Not part of the public API.
51
59
  */
@@ -95,8 +103,25 @@ export declare function setOtelLogSink(sink: OtelLogSink | undefined): void;
95
103
  * with one `warning` naming it; the process and the remaining sinks carry on.
96
104
  * - Optional OTel log export: once `initializeOpenTelemetry` attaches a Logs API
97
105
  * sink, every record that passes the level filter and rate limit is also emitted
98
- * there, redacted like the pino output.
99
- * - Sensitive field redaction via Pino's `redact` option.
106
+ * there, with the same fields as the pino output — except the error argument,
107
+ * which is exported as the `exception.*` attributes instead of an `err` field.
108
+ * - One walk over every record's data ({@link sanitizeLogBindings}), and the only
109
+ * redaction: each `Error`, under any key, written as `type`/`message`/`stack`
110
+ * (plus a string or `McpError` `code`, an `McpError`'s `data`, and `cause`/`errors`
111
+ * in the same shape, a stack shared with the parent error written once); objects
112
+ * kept through 15 levels below the record root and one 16 levels down written as
113
+ * `'[MaxDepth]'`; at most 400,000 reads — one per object and per other field or
114
+ * array element, ten per object at the depth bound — so data a getter, a Proxy, or a
115
+ * `toJSON()` builds on every read stops at `'[Truncated]'`; at most 16 MiB
116
+ * (16,777,216) characters of strings, field names, and primitives written, repeated
117
+ * or not, then `'[Truncated]'` and the walk stops, so a 10 MB string is written whole
118
+ * and a 20 MB one is not; repeated content — an object reached again through a shared
119
+ * reference, or a string or field name of 1,024 characters or more written again —
120
+ * also charged about the characters it writes against 1,000,000, and written as
121
+ * `'[Truncated]'` where that runs out; cycles written as `'[Circular]'`; a value
122
+ * whose read throws written as `'[Unreadable]'`; and every key the shared matcher
123
+ * (`isSensitiveKey`) matches redacted at every depth kept, except the correlation
124
+ * fields the record's context supplied, at its root.
100
125
  * - Rate limiting per level + message to suppress log storms, configurable via
101
126
  * `MCP_LOG_RATE_LIMIT_THRESHOLD` (0 disables) and `MCP_LOG_RATE_LIMIT_WINDOW_MS`.
102
127
  * Suppressed counts are flushed at `warning` once a window has elapsed, on
@@ -282,6 +307,9 @@ export declare class Logger {
282
307
  * For startup paths that end the process before {@link initialize} runs: the
283
308
  * records describe what the failing boot was doing, so they are worth more on
284
309
  * stderr than discarded. Never writes to stdout, which stdio transport owns.
310
+ * An error's message is read through the walk, so one whose read throws is
311
+ * written as `'[Unreadable]'`, one the walk writes no message for is left
312
+ * off, and neither replaces the error that ended the boot.
285
313
  *
286
314
  * @example
287
315
  * ```ts
@@ -350,8 +378,9 @@ export declare class Logger {
350
378
  * Logs an error-level message at `error` severity (RFC 5424 level 3).
351
379
  *
352
380
  * Use when an operation fails but the server can continue. The `errorOrContext`
353
- * parameter accepts either an `Error` (serialized via `pino.stdSerializers.err`) or a
354
- * `RequestContext` when no error object is available. Maps to pino `error` level.
381
+ * parameter accepts either an `Error` (written under `err` as its `type`, `message`,
382
+ * `stack`, and the other fields every logged Error carries — see {@link sanitizeLogBindings})
383
+ * or a `RequestContext` when no error object is available. Maps to pino `error` level.
355
384
  *
356
385
  * @param msg - Human-readable description of the failure.
357
386
  * @param errorOrContext - The `Error` to serialize, or a `RequestContext` if no error object exists.
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/utils/internal/logger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/utils/internal/logger.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import pino from 'pino';
2
2
  import { config } from '../../config/index.js';
3
+ import { toLogValue } from './logValue.js';
3
4
  import { requestContextService, toCanonicalContext, } from './requestContext.js';
4
5
  import { UNTHROTTLED_MESSAGES } from './telemetryMessages.js';
5
- import { DEFAULT_SENSITIVE_FIELDS, toPinoRedactPaths } from '../security/sensitiveFields.js';
6
6
  const mcpToPinoLevel = {
7
7
  emerg: 'fatal',
8
8
  alert: 'fatal',
@@ -100,13 +100,13 @@ export async function probeFileSinks(dir) {
100
100
  function isServerless() {
101
101
  return typeof process === 'undefined' || process.env.IS_SERVERLESS === 'true';
102
102
  }
103
- /** Pino redact paths for sensitive fields (top-level, one-deep, two-deep). */
104
- const SENSITIVE_PINO_FIELDS = toPinoRedactPaths(DEFAULT_SENSITIVE_FIELDS);
105
103
  /**
106
- * Depth cap for {@link sanitizeLogBindings}. Matches the deepest path pino-redact
107
- * currently traverses (`*.*.field`) plus headroom for nested ad-hoc context.
104
+ * pino serializers for the process and `interactions.log` loggers. The
105
+ * log-data walk has already written every `Error` as plain data, so `err`
106
+ * passes through: pino's default would retype that object as `Object` and
107
+ * copy any other field it carries.
108
108
  */
109
- const MAX_SANITIZE_DEPTH = 4;
109
+ const PINO_SERIALIZERS = { err: (value) => value };
110
110
  /**
111
111
  * Hard ceiling on distinct rate-limit keys retained between sweeps.
112
112
  *
@@ -133,104 +133,192 @@ const FLUSH_DRAIN_TIMEOUT_MS = 2_000;
133
133
  */
134
134
  const PRE_INIT_BUFFER_LIMIT = 250;
135
135
  /**
136
- * Recursively sanitizes a value for pino consumption. Returns a JSON-safe
137
- * replacement, or `undefined` when the value is unsafe and should be dropped.
138
- *
139
- * Preserves primitives, plain objects (recursively), and arrays. Converts
140
- * `Date` → ISO string, `URL` → string. Keeps `Error` instances (pino's err
141
- * serializer handles them). Drops functions and non-plain class instances
142
- * such as `AbortSignal`, `Map`, `Set`, `Promise`, storage handles, and any
143
- * other object whose prototype exposes getters that enforce receiver checks
144
- * — on Node 25, `for..in` traversal inside `@pinojs/redact` triggers those
145
- * getters with a non-branded receiver and throws.
136
+ * The fields a record carries from its request context to correlate it. At the
137
+ * record's root, each one the context supplied is never redacted, so a name
138
+ * added through `setSensitiveFields` (`session_id`, `id`) cannot cut every
139
+ * record off from its request. A caller's key of the same name — in `extra`
140
+ * when the context lacks that field, anywhere in caller data, and every key of
141
+ * an `interactions.log` record, which has no context — is matched like any other.
146
142
  */
147
- function sanitizeValue(value, depth) {
148
- if (value === null)
149
- return null;
150
- const t = typeof value;
151
- if (t === 'undefined' || t === 'function')
152
- return;
153
- if (t !== 'object')
154
- return value;
155
- if (depth >= MAX_SANITIZE_DEPTH)
156
- return;
157
- if (value instanceof Error)
158
- return value;
159
- if (value instanceof Date)
160
- return value.toISOString();
161
- if (value instanceof URL)
162
- return value.toString();
163
- if (Array.isArray(value)) {
164
- return value.map((v) => sanitizeValue(v, depth + 1));
165
- }
166
- const proto = Object.getPrototypeOf(value);
167
- if (proto !== Object.prototype && proto !== null) {
168
- return;
169
- }
170
- const out = {};
171
- for (const key of Object.keys(value)) {
172
- const sanitized = sanitizeValue(value[key], depth + 1);
173
- if (sanitized !== undefined)
174
- out[key] = sanitized;
175
- }
176
- return out;
143
+ const CORRELATION_KEYS = new Set([
144
+ 'operation',
145
+ 'requestId',
146
+ 'sessionId',
147
+ 'spanId',
148
+ 'tenantId',
149
+ 'timestamp',
150
+ 'traceId',
151
+ ]);
152
+ /** The `base` the process logger writes on every line: constants only. */
153
+ function processLogBase() {
154
+ return {
155
+ env: config.environment,
156
+ version: config.mcpServerVersion,
157
+ pid: !isServerless() ? process.pid : undefined,
158
+ };
177
159
  }
178
160
  /**
179
- * Pino `formatters.log` hook. Strips values that break pino-redact's wildcard
180
- * traversal on Node 25+ — notably `AbortSignal` (via `ctx.signal`) and the
181
- * method-bearing handles on the framework `Context` (`log`, `state`, `content`,
182
- * `enrich`, `inputs`, `requestInput`, `notifyResource*`). Acts as a safety net
183
- * for every log call regardless of how callers shaped their bindings.
161
+ * The fields the logger's pino instances write on a line themselves: `level`
162
+ * and `time` before the record and `msg` after it, the process logger's
163
+ * `base` ({@link processLogBase}), and pino's default `base` — `pid` and
164
+ * `hostname` — on `interactions.log`. A record key with one of these names
165
+ * would be a second copy in the line: a transport routes a line by the last
166
+ * `level` it parses, so a caller's `level: 'high'` would drop the record from
167
+ * stderr and `combined.log`, and a parser keeps the last `version`, so a
168
+ * caller's would replace the server's.
169
+ */
170
+ const LINE_FIELDS = new Set([
171
+ 'level',
172
+ 'time',
173
+ 'msg',
174
+ ...Object.keys(processLogBase()),
175
+ 'pid',
176
+ 'hostname',
177
+ ]);
178
+ /**
179
+ * The context a record's bindings were projected from, under a key only this
180
+ * module holds. It rides the spread into pino's record and the OTLP bindings,
181
+ * where {@link sanitizeLogBindings} reads which correlation fields the context
182
+ * supplied; the walk never writes a symbol-keyed field.
183
+ */
184
+ const RECORD_CONTEXT = Symbol('recordContext');
185
+ /** Moves `record[key]` to `data_<key>`, prefixed again while that name is taken. */
186
+ function moveLineField(record, key) {
187
+ let name = `data_${key}`;
188
+ while (Object.hasOwn(record, name))
189
+ name = `data_${name}`;
190
+ record[name] = record[key];
191
+ delete record[key];
192
+ }
193
+ /**
194
+ * Pino `formatters.log` hook, and the bindings of every exported OTel record:
195
+ * the log-data walk ({@link toLogValue}) over the whole record. pino
196
+ * therefore never receives an `Error` or a class instance — the method-bearing
197
+ * handles on the framework `Context` and `ctx.signal` included — whatever
198
+ * shape a caller gave its bindings. The walk is the only redaction: it is the
199
+ * one path caller data takes into a written line, since neither pino logger
200
+ * sets a `mixin`, binds a child, or carries anything in `base` but constants.
201
+ * Exempt from it are the root correlation fields ({@link CORRELATION_KEYS})
202
+ * that the record's context supplied, and a root key named after a field pino
203
+ * writes on the line itself ({@link LINE_FIELDS}) is written as `data_<name>` —
204
+ * prefixed again while that name is taken. `err` is such a field on a line
205
+ * carrying an error argument, which {@link Logger} moves the same way before
206
+ * attaching the error.
184
207
  *
185
208
  * @internal Exported only for unit testing. Not part of the public API.
186
209
  */
187
210
  export function sanitizeLogBindings(obj) {
188
- return sanitizeValue(obj, 0);
211
+ const supplied = obj[RECORD_CONTEXT];
212
+ const written = toLogValue(obj, {
213
+ exemptRootKey: supplied && ((key) => CORRELATION_KEYS.has(key) && Object.hasOwn(supplied, key)),
214
+ });
215
+ // The walk's copy is a fresh object, so renaming in place touches no caller data.
216
+ for (const key of LINE_FIELDS) {
217
+ if (Object.hasOwn(written, key))
218
+ moveLineField(written, key);
219
+ }
220
+ return written;
221
+ }
222
+ /**
223
+ * The walk's copy of `fields` a spread could not read (a getter, a revoked
224
+ * Proxy): `'[Unreadable]'` where a read failed, and under `key` when nothing
225
+ * could be read. A log call never throws on data it cannot read.
226
+ */
227
+ function walkedFields(fields, key) {
228
+ const walked = toLogValue(fields);
229
+ return walked !== null && typeof walked === 'object' && !Array.isArray(walked)
230
+ ? walked
231
+ : { [key]: walked };
232
+ }
233
+ /**
234
+ * The bindings a record writes for `context`. `extra` is flattened rather than
235
+ * nested so the line keeps the shape callers had when `RequestContext` was an
236
+ * open bag. The projection runs first: `logger.info(msg, ctx)` with a handler
237
+ * context is a documented call, and that object carries live request machinery
238
+ * and the user-entered content in `inputs.responses` — none of which belongs in
239
+ * a log line. The canonical fields are spread again after `extra`, so a caller's
240
+ * key reusing a canonical name (`requestId`, `traceId`, …) never replaces the
241
+ * context's value — the first spread only keeps them leading the line. The
242
+ * canonical fields also ride along under {@link RECORD_CONTEXT}, so the walk
243
+ * exempts only the correlation fields the context supplied. A context the
244
+ * projection cannot read is projected from the walk's copy, and one with
245
+ * nothing readable is written as `context: '[Unreadable]'`.
246
+ */
247
+ function toBindings(context) {
248
+ let projected;
249
+ try {
250
+ projected = toCanonicalContext((context ?? {}));
251
+ }
252
+ catch {
253
+ const walked = toLogValue(context);
254
+ if (walked === null || typeof walked !== 'object')
255
+ return { context: walked };
256
+ projected = toCanonicalContext(walked);
257
+ }
258
+ const { extra, ...canonical } = projected;
259
+ try {
260
+ return { ...canonical, ...extra, ...canonical, [RECORD_CONTEXT]: canonical };
261
+ }
262
+ catch {
263
+ return {
264
+ ...canonical,
265
+ ...walkedFields(extra, 'extra'),
266
+ ...canonical,
267
+ [RECORD_CONTEXT]: canonical,
268
+ };
269
+ }
189
270
  }
190
- const SENSITIVE_FIELD_NAMES = new Set(DEFAULT_SENSITIVE_FIELDS);
191
271
  /**
192
- * Converts one sanitized binding into an attribute value: an `Error` becomes
193
- * the `type`/`message`/`stack` object pino's serializer would have written, and
194
- * a primitive JSON has no form for becomes its string.
272
+ * The walk's copy of a log call's error argument, as fields: `{}` when the walk
273
+ * writes nothing for it, as for a function given `Error.prototype`, which passes
274
+ * `instanceof Error` and is dropped like any other function.
275
+ */
276
+ function errorFields(error) {
277
+ const walked = toLogValue(error);
278
+ return walked !== null && typeof walked === 'object' ? walked : {};
279
+ }
280
+ /** Whether `value` is an `Error`; a value `instanceof` cannot inspect (a revoked Proxy) is not. */
281
+ function isError(value) {
282
+ try {
283
+ return value instanceof Error;
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ /**
290
+ * Converts one walked binding into an attribute value. The walk has already
291
+ * written every `Error` as plain data and redacted every sensitive field, so
292
+ * all that is left is a primitive JSON has no form for, which becomes its string.
195
293
  */
196
294
  function toExportValue(value) {
197
- if (value instanceof Error)
198
- return { message: value.message, stack: value.stack, type: value.name };
199
295
  if (Array.isArray(value))
200
296
  return value.map(toExportValue);
201
297
  if (value !== null && typeof value === 'object') {
202
- return toExportAttributes(value);
298
+ return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, toExportValue(field)]));
203
299
  }
204
300
  if (typeof value === 'bigint' || typeof value === 'symbol')
205
301
  return String(value);
206
302
  return value;
207
303
  }
208
- /**
209
- * Builds exported attributes from sanitized bindings, replacing every field
210
- * named in {@link DEFAULT_SENSITIVE_FIELDS} at any depth. OTel records bypass
211
- * pino's `redact`, so without this a value kept out of every file and stream
212
- * would leave the process in the clear.
213
- */
214
- function toExportAttributes(fields) {
215
- const out = {};
216
- for (const [key, field] of Object.entries(fields)) {
217
- out[key] = SENSITIVE_FIELD_NAMES.has(key) ? '[REDACTED]' : toExportValue(field);
218
- }
219
- return out;
220
- }
221
304
  /**
222
305
  * Builds the OTel record for one framework log line: the message as the body,
223
- * the sanitized and redacted bindings as attributes, and an `Error` as the
224
- * `exception.*` semantic-convention attributes. Trace context is left to the
225
- * Logs API, which takes it from the active context at emit time.
306
+ * the bindings the process log writes as attributes, and an `Error` argument as
307
+ * the `exception.*` semantic-convention attributes, read through the walk, so a
308
+ * field whose read throws is exported as `'[Unreadable]'` and one the walk does
309
+ * not write is left out, rather than failing the log call. Trace context is
310
+ * left to the Logs API, which takes it from the active context at emit time.
226
311
  */
227
312
  function toOtelLogRecord(level, msg, bindings, error) {
228
- const attributes = toExportAttributes(sanitizeLogBindings(bindings));
313
+ const attributes = toExportValue(sanitizeLogBindings(bindings));
229
314
  if (error) {
230
- attributes['exception.type'] = error.name;
231
- attributes['exception.message'] = error.message;
232
- if (error.stack)
233
- attributes['exception.stacktrace'] = error.stack;
315
+ const { type, message, stack } = errorFields(error);
316
+ if (type !== undefined)
317
+ attributes['exception.type'] = toExportValue(type);
318
+ if (message !== undefined)
319
+ attributes['exception.message'] = toExportValue(message);
320
+ if (stack)
321
+ attributes['exception.stacktrace'] = toExportValue(stack);
234
322
  }
235
323
  return {
236
324
  attributes,
@@ -267,8 +355,25 @@ export function setOtelLogSink(sink) {
267
355
  * with one `warning` naming it; the process and the remaining sinks carry on.
268
356
  * - Optional OTel log export: once `initializeOpenTelemetry` attaches a Logs API
269
357
  * sink, every record that passes the level filter and rate limit is also emitted
270
- * there, redacted like the pino output.
271
- * - Sensitive field redaction via Pino's `redact` option.
358
+ * there, with the same fields as the pino output — except the error argument,
359
+ * which is exported as the `exception.*` attributes instead of an `err` field.
360
+ * - One walk over every record's data ({@link sanitizeLogBindings}), and the only
361
+ * redaction: each `Error`, under any key, written as `type`/`message`/`stack`
362
+ * (plus a string or `McpError` `code`, an `McpError`'s `data`, and `cause`/`errors`
363
+ * in the same shape, a stack shared with the parent error written once); objects
364
+ * kept through 15 levels below the record root and one 16 levels down written as
365
+ * `'[MaxDepth]'`; at most 400,000 reads — one per object and per other field or
366
+ * array element, ten per object at the depth bound — so data a getter, a Proxy, or a
367
+ * `toJSON()` builds on every read stops at `'[Truncated]'`; at most 16 MiB
368
+ * (16,777,216) characters of strings, field names, and primitives written, repeated
369
+ * or not, then `'[Truncated]'` and the walk stops, so a 10 MB string is written whole
370
+ * and a 20 MB one is not; repeated content — an object reached again through a shared
371
+ * reference, or a string or field name of 1,024 characters or more written again —
372
+ * also charged about the characters it writes against 1,000,000, and written as
373
+ * `'[Truncated]'` where that runs out; cycles written as `'[Circular]'`; a value
374
+ * whose read throws written as `'[Unreadable]'`; and every key the shared matcher
375
+ * (`isSensitiveKey`) matches redacted at every depth kept, except the correlation
376
+ * fields the record's context supplied, at its root.
272
377
  * - Rate limiting per level + message to suppress log storms, configurable via
273
378
  * `MCP_LOG_RATE_LIMIT_THRESHOLD` (0 disables) and `MCP_LOG_RATE_LIMIT_WINDOW_MS`.
274
379
  * Suppressed counts are flushed at `warning` once a window has elapsed, on
@@ -324,18 +429,11 @@ export class Logger {
324
429
  const pinoLevel = mcpToPinoLevel[level] ?? 'info';
325
430
  const pinoOptions = {
326
431
  level: pinoLevel,
327
- base: {
328
- env: config.environment,
329
- version: config.mcpServerVersion,
330
- pid: !isServerless() ? process.pid : undefined,
331
- },
332
- redact: {
333
- paths: SENSITIVE_PINO_FIELDS,
334
- censor: '[REDACTED]',
335
- },
432
+ base: processLogBase(),
336
433
  formatters: {
337
434
  log: sanitizeLogBindings,
338
435
  },
436
+ serializers: PINO_SERIALIZERS,
339
437
  };
340
438
  if (isServerless()) {
341
439
  return pino(pinoOptions);
@@ -386,13 +484,10 @@ export class Logger {
386
484
  if (!destination)
387
485
  return;
388
486
  return pino({
389
- redact: {
390
- paths: SENSITIVE_PINO_FIELDS,
391
- censor: '[REDACTED]',
392
- },
393
487
  formatters: {
394
488
  log: sanitizeLogBindings,
395
489
  },
490
+ serializers: PINO_SERIALIZERS,
396
491
  transport: {
397
492
  target: 'pino/file',
398
493
  options: { destination },
@@ -723,6 +818,9 @@ export class Logger {
723
818
  * For startup paths that end the process before {@link initialize} runs: the
724
819
  * records describe what the failing boot was doing, so they are worth more on
725
820
  * stderr than discarded. Never writes to stdout, which stdio transport owns.
821
+ * An error's message is read through the walk, so one whose read throws is
822
+ * written as `'[Unreadable]'`, one the walk writes no message for is left
823
+ * off, and neither replaces the error that ended the boot.
726
824
  *
727
825
  * @example
728
826
  * ```ts
@@ -735,7 +833,12 @@ export class Logger {
735
833
  // Checked before the records are cleared: a runtime with no stderr keeps them.
736
834
  if (typeof process === 'undefined' || typeof process.stderr?.write !== 'function')
737
835
  return;
738
- const lines = this.pendingRecords.map((record) => `[pre-init ${record.level}] ${record.msg}${record.error ? ` — ${record.error.message}` : ''}`);
836
+ const lines = this.pendingRecords.map(({ error, level, msg }) => {
837
+ const message = error && errorFields(error).message;
838
+ return message === undefined
839
+ ? `[pre-init ${level}] ${msg}`
840
+ : `[pre-init ${level}] ${msg} — ${message}`;
841
+ });
739
842
  if (this.droppedPendingRecords > 0) {
740
843
  lines.push(`[pre-init] ${this.droppedPendingRecords} further record(s) dropped — buffer holds ${PRE_INIT_BUFFER_LIMIT}.`);
741
844
  }
@@ -751,26 +854,24 @@ export class Logger {
751
854
  if (!this.isLevelEnabled(level) || this.isRateLimited(level, msg))
752
855
  return;
753
856
  const pinoLevel = mcpToPinoLevel[level] ?? 'info';
754
- // `extra` is flattened rather than nested so the emitted line keeps the
755
- // shape callers had when `RequestContext` was an open bag. The projection
756
- // runs first: `logger.info(msg, ctx)` with a handler context is a documented
757
- // call, and that object carries live request machinery and the user-entered
758
- // content in `inputs.responses` — none of which belongs in a log line.
759
- // The canonical fields are spread again after `extra`, so a caller's key
760
- // reusing a canonical name (`requestId`, `traceId`, …) never replaces the
761
- // context's value — the first spread only keeps them leading the line.
762
- const { extra, ...canonical } = toCanonicalContext((context ?? {}));
763
- const bindings = { ...canonical, ...extra, ...canonical };
764
- // Pass the raw Error so pino's `err` serializer (default: `pino.stdSerializers.err`)
765
- // runs *after* our `formatters.log` sanitizer. Pre-serializing here would produce
766
- // an object whose prototype (`pinoErrProto`) trips the sanitizer's plain-object check.
767
- this.pinoLogger[pinoLevel](error ? { ...bindings, err: error } : bindings, msg);
857
+ const bindings = toBindings(context);
858
+ // The error argument rides the `err` key; pino's `formatters.log` walk writes
859
+ // it like any other Error in the record, and the `err` serializer passes the result through.
860
+ // It leads the record, so the walk, which works in key order, writes it before caller data
861
+ // can spend the walk's bound. On that line `err` is the logger's own field, so a caller's
862
+ // `err` moves aside like any line field.
863
+ if (error && Object.hasOwn(bindings, 'err'))
864
+ moveLineField(bindings, 'err');
865
+ this.pinoLogger[pinoLevel](error ? { err: error, ...bindings } : bindings, msg);
768
866
  otelLogSink?.emit(toOtelLogRecord(level, msg, bindings, error));
769
867
  }
770
868
  logWithError(level, msg, errorOrContext, context) {
771
- const errorObj = errorOrContext instanceof Error ? errorOrContext : undefined;
772
- const actualContext = errorOrContext instanceof Error ? context : errorOrContext;
773
- this.log(level, msg, actualContext, errorObj);
869
+ if (isError(errorOrContext)) {
870
+ this.log(level, msg, context, errorOrContext);
871
+ }
872
+ else {
873
+ this.log(level, msg, errorOrContext);
874
+ }
774
875
  }
775
876
  /**
776
877
  * Logs a diagnostic message at `debug` severity (RFC 5424 level 7).
@@ -839,8 +940,9 @@ export class Logger {
839
940
  * Logs an error-level message at `error` severity (RFC 5424 level 3).
840
941
  *
841
942
  * Use when an operation fails but the server can continue. The `errorOrContext`
842
- * parameter accepts either an `Error` (serialized via `pino.stdSerializers.err`) or a
843
- * `RequestContext` when no error object is available. Maps to pino `error` level.
943
+ * parameter accepts either an `Error` (written under `err` as its `type`, `message`,
944
+ * `stack`, and the other fields every logged Error carries — see {@link sanitizeLogBindings})
945
+ * or a `RequestContext` when no error object is available. Maps to pino `error` level.
844
946
  *
845
947
  * @param msg - Human-readable description of the failure.
846
948
  * @param errorOrContext - The `Error` to serialize, or a `RequestContext` if no error object exists.
@@ -937,12 +1039,20 @@ export class Logger {
937
1039
  * ```
938
1040
  */
939
1041
  logInteraction(interactionName, data) {
1042
+ let record;
1043
+ try {
1044
+ record = { interactionName, ...data };
1045
+ }
1046
+ catch {
1047
+ record = { interactionName, ...walkedFields(data, 'data') };
1048
+ }
940
1049
  if (!this.interactionLogger) {
941
- if (!isServerless() && !this.interactionSinkDropped)
942
- this.warning('Interaction logger not available.', (data.context || {}));
1050
+ if (!isServerless() && !this.interactionSinkDropped) {
1051
+ this.warning('Interaction logger not available.', record.context);
1052
+ }
943
1053
  return;
944
1054
  }
945
- this.interactionLogger.info({ interactionName, ...data });
1055
+ this.interactionLogger.info(record);
946
1056
  }
947
1057
  }
948
1058
  /**