@cyanheads/mcp-ts-core 0.12.7 → 0.12.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 (215) hide show
  1. package/AGENTS.md +8 -3
  2. package/CLAUDE.md +8 -3
  3. package/README.md +11 -4
  4. package/changelog/0.12.x/0.12.8.md +55 -0
  5. package/dist/config/index.d.ts +3 -34
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/index.js +4 -26
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/core/app.d.ts +0 -8
  10. package/dist/core/app.d.ts.map +1 -1
  11. package/dist/core/app.js +0 -7
  12. package/dist/core/app.js.map +1 -1
  13. package/dist/core/serverManifest.d.ts +0 -7
  14. package/dist/core/serverManifest.d.ts.map +1 -1
  15. package/dist/core/serverManifest.js +1 -13
  16. package/dist/core/serverManifest.js.map +1 -1
  17. package/dist/linter/rules/enrichment-rules.js +2 -2
  18. package/dist/linter/rules/enrichment-rules.js.map +1 -1
  19. package/dist/linter/rules/format-parity-rules.d.ts.map +1 -1
  20. package/dist/linter/rules/format-parity-rules.js +14 -36
  21. package/dist/linter/rules/format-parity-rules.js.map +1 -1
  22. package/dist/linter/rules/prompt-rules.d.ts +1 -1
  23. package/dist/linter/rules/prompt-rules.d.ts.map +1 -1
  24. package/dist/linter/rules/prompt-rules.js +2 -19
  25. package/dist/linter/rules/prompt-rules.js.map +1 -1
  26. package/dist/linter/rules/resource-rules.d.ts +1 -1
  27. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  28. package/dist/linter/rules/resource-rules.js +9 -39
  29. package/dist/linter/rules/resource-rules.js.map +1 -1
  30. package/dist/linter/rules/schema-rules.d.ts +22 -2
  31. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  32. package/dist/linter/rules/schema-rules.js +28 -5
  33. package/dist/linter/rules/schema-rules.js.map +1 -1
  34. package/dist/linter/rules/tool-rules.d.ts +1 -1
  35. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  36. package/dist/linter/rules/tool-rules.js +13 -41
  37. package/dist/linter/rules/tool-rules.js.map +1 -1
  38. package/dist/linter/validate.d.ts.map +1 -1
  39. package/dist/linter/validate.js +22 -42
  40. package/dist/linter/validate.js.map +1 -1
  41. package/dist/mcp-server/apps/appBuilders.d.ts.map +1 -1
  42. package/dist/mcp-server/apps/appBuilders.js +2 -16
  43. package/dist/mcp-server/apps/appBuilders.js.map +1 -1
  44. package/dist/mcp-server/handlerContext.d.ts +66 -0
  45. package/dist/mcp-server/handlerContext.d.ts.map +1 -0
  46. package/dist/mcp-server/handlerContext.js +71 -0
  47. package/dist/mcp-server/handlerContext.js.map +1 -0
  48. package/dist/mcp-server/inputRequired.d.ts +7 -1
  49. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  50. package/dist/mcp-server/inputRequired.js +10 -3
  51. package/dist/mcp-server/inputRequired.js.map +1 -1
  52. package/dist/mcp-server/resources/resource-registration.d.ts +2 -2
  53. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  54. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  55. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +14 -43
  56. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  57. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +11 -50
  58. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  59. package/dist/mcp-server/tools/tool-registration.d.ts +5 -9
  60. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  61. package/dist/mcp-server/tools/tool-registration.js +9 -11
  62. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  63. package/dist/mcp-server/tools/utils/schemaShape.d.ts +21 -0
  64. package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -1
  65. package/dist/mcp-server/tools/utils/schemaShape.js +8 -6
  66. package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -1
  67. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +15 -43
  68. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  69. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +31 -72
  70. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  71. package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
  72. package/dist/mcp-server/transports/http/httpErrorHandler.js +2 -1
  73. package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
  74. package/dist/mcp-server/transports/http/landing-page/handler.d.ts.map +1 -1
  75. package/dist/mcp-server/transports/http/landing-page/handler.js +2 -1
  76. package/dist/mcp-server/transports/http/landing-page/handler.js.map +1 -1
  77. package/dist/mcp-server/transports/http/protectedResourceMetadata.d.ts.map +1 -1
  78. package/dist/mcp-server/transports/http/protectedResourceMetadata.js +2 -1
  79. package/dist/mcp-server/transports/http/protectedResourceMetadata.js.map +1 -1
  80. package/dist/mcp-server/transports/http/publicOrigin.d.ts +11 -0
  81. package/dist/mcp-server/transports/http/publicOrigin.d.ts.map +1 -0
  82. package/dist/mcp-server/transports/http/publicOrigin.js +13 -0
  83. package/dist/mcp-server/transports/http/publicOrigin.js.map +1 -0
  84. package/dist/mcp-server/transports/http/serverCard.d.ts.map +1 -1
  85. package/dist/mcp-server/transports/http/serverCard.js +2 -1
  86. package/dist/mcp-server/transports/http/serverCard.js.map +1 -1
  87. package/dist/mcp-server/transports/http/sessionIdUtils.d.ts +4 -0
  88. package/dist/mcp-server/transports/http/sessionIdUtils.d.ts.map +1 -1
  89. package/dist/mcp-server/transports/http/sessionIdUtils.js +3 -13
  90. package/dist/mcp-server/transports/http/sessionIdUtils.js.map +1 -1
  91. package/dist/mcp-server/transports/manager.d.ts +0 -3
  92. package/dist/mcp-server/transports/manager.d.ts.map +1 -1
  93. package/dist/mcp-server/transports/manager.js +0 -7
  94. package/dist/mcp-server/transports/manager.js.map +1 -1
  95. package/dist/services/canvas/core/CanvasRegistry.d.ts +14 -0
  96. package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
  97. package/dist/services/canvas/core/CanvasRegistry.js +3 -2
  98. package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
  99. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +16 -0
  100. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  101. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +78 -103
  102. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  103. package/dist/services/graph/core/GraphService.d.ts +3 -3
  104. package/dist/services/graph/core/GraphService.js +3 -3
  105. package/dist/services/graph/types.d.ts +2 -79
  106. package/dist/services/graph/types.d.ts.map +1 -1
  107. package/dist/services/graph/types.js +2 -2
  108. package/dist/services/index.d.ts +1 -2
  109. package/dist/services/index.d.ts.map +1 -1
  110. package/dist/services/index.js +0 -1
  111. package/dist/services/index.js.map +1 -1
  112. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
  113. package/dist/services/mirror/sqlite/handle.js +22 -36
  114. package/dist/services/mirror/sqlite/handle.js.map +1 -1
  115. package/dist/services/speech/core/ISpeechProvider.d.ts +0 -24
  116. package/dist/services/speech/core/ISpeechProvider.d.ts.map +1 -1
  117. package/dist/services/speech/core/ISpeechProvider.js +1 -28
  118. package/dist/services/speech/core/ISpeechProvider.js.map +1 -1
  119. package/dist/services/speech/core/SpeechService.d.ts.map +1 -1
  120. package/dist/services/speech/core/SpeechService.js +5 -8
  121. package/dist/services/speech/core/SpeechService.js.map +1 -1
  122. package/dist/services/speech/providers/elevenlabs.provider.d.ts.map +1 -1
  123. package/dist/services/speech/providers/elevenlabs.provider.js +1 -0
  124. package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
  125. package/dist/services/speech/types.d.ts +2 -19
  126. package/dist/services/speech/types.d.ts.map +1 -1
  127. package/dist/storage/core/providerHelpers.d.ts +52 -0
  128. package/dist/storage/core/providerHelpers.d.ts.map +1 -0
  129. package/dist/storage/core/providerHelpers.js +96 -0
  130. package/dist/storage/core/providerHelpers.js.map +1 -0
  131. package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
  132. package/dist/storage/providers/cloudflare/d1Provider.js +1 -4
  133. package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
  134. package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
  135. package/dist/storage/providers/cloudflare/kvProvider.js +4 -31
  136. package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
  137. package/dist/storage/providers/cloudflare/r2Provider.d.ts +1 -1
  138. package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
  139. package/dist/storage/providers/cloudflare/r2Provider.js +8 -48
  140. package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
  141. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -1
  142. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
  143. package/dist/storage/providers/fileSystem/fileSystemProvider.js +17 -86
  144. package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
  145. package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
  146. package/dist/storage/providers/inMemory/inMemoryProvider.js +5 -38
  147. package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
  148. package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
  149. package/dist/storage/providers/supabase/supabaseProvider.js +1 -4
  150. package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
  151. package/dist/testing/fuzz.d.ts.map +1 -1
  152. package/dist/testing/fuzz.js +17 -31
  153. package/dist/testing/fuzz.js.map +1 -1
  154. package/dist/testing/index.d.ts.map +1 -1
  155. package/dist/testing/index.js +4 -26
  156. package/dist/testing/index.js.map +1 -1
  157. package/dist/utils/internal/error-handler/types.d.ts +0 -4
  158. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  159. package/dist/utils/internal/logger.d.ts.map +1 -1
  160. package/dist/utils/internal/logger.js +2 -16
  161. package/dist/utils/internal/logger.js.map +1 -1
  162. package/dist/utils/internal/performance.d.ts +8 -31
  163. package/dist/utils/internal/performance.d.ts.map +1 -1
  164. package/dist/utils/internal/performance.js +173 -295
  165. package/dist/utils/internal/performance.js.map +1 -1
  166. package/dist/utils/security/idGenerator.d.ts.map +1 -1
  167. package/dist/utils/security/idGenerator.js +24 -43
  168. package/dist/utils/security/idGenerator.js.map +1 -1
  169. package/dist/utils/security/sanitization.d.ts +0 -7
  170. package/dist/utils/security/sanitization.d.ts.map +1 -1
  171. package/dist/utils/security/sanitization.js +4 -31
  172. package/dist/utils/security/sanitization.js.map +1 -1
  173. package/dist/utils/security/sensitiveFields.d.ts +14 -0
  174. package/dist/utils/security/sensitiveFields.d.ts.map +1 -0
  175. package/dist/utils/security/sensitiveFields.js +31 -0
  176. package/dist/utils/security/sensitiveFields.js.map +1 -0
  177. package/dist/utils/telemetry/trace.d.ts +8 -10
  178. package/dist/utils/telemetry/trace.d.ts.map +1 -1
  179. package/dist/utils/telemetry/trace.js +19 -18
  180. package/dist/utils/telemetry/trace.js.map +1 -1
  181. package/dist/utils/types/guards.d.ts +0 -102
  182. package/dist/utils/types/guards.d.ts.map +1 -1
  183. package/dist/utils/types/guards.js +0 -114
  184. package/dist/utils/types/guards.js.map +1 -1
  185. package/package.json +6 -6
  186. package/skills/add-provider/SKILL.md +18 -4
  187. package/skills/api-config/SKILL.md +4 -18
  188. package/skills/api-services/SKILL.md +1 -1
  189. package/skills/api-services/references/speech.md +1 -2
  190. package/skills/api-telemetry/SKILL.md +2 -2
  191. package/skills/api-utils/SKILL.md +2 -2
  192. package/skills/code-simplifier/SKILL.md +47 -20
  193. package/skills/field-test/SKILL.md +93 -14
  194. package/skills/git-wrapup/SKILL.md +64 -27
  195. package/skills/orchestrations/SKILL.md +17 -6
  196. package/skills/orchestrations/workflows/field-test-fix.md +6 -4
  197. package/skills/orchestrations/workflows/fix-wrapup-release.md +6 -4
  198. package/skills/orchestrations/workflows/greenfield-build.md +2 -2
  199. package/skills/orchestrations/workflows/maintenance-release.md +4 -2
  200. package/skills/release-and-publish/SKILL.md +101 -23
  201. package/skills/release-pr-review/SKILL.md +147 -0
  202. package/templates/AGENTS.md +4 -2
  203. package/templates/CLAUDE.md +4 -2
  204. package/dist/mcp-server/transports/ITransport.d.ts +0 -15
  205. package/dist/mcp-server/transports/ITransport.d.ts.map +0 -1
  206. package/dist/mcp-server/transports/ITransport.js +0 -2
  207. package/dist/mcp-server/transports/ITransport.js.map +0 -1
  208. package/dist/services/llm/types.d.ts +0 -16
  209. package/dist/services/llm/types.d.ts.map +0 -1
  210. package/dist/services/llm/types.js +0 -9
  211. package/dist/services/llm/types.js.map +0 -1
  212. package/dist/utils/internal/health.d.ts +0 -60
  213. package/dist/utils/internal/health.d.ts.map +0 -1
  214. package/dist/utils/internal/health.js +0 -46
  215. package/dist/utils/internal/health.js.map +0 -1
@@ -50,28 +50,6 @@ function getActiveRequestsGauge() {
50
50
  activeRequests ??= createUpDownCounter('mcp.requests.active', 'Number of in-flight tool, resource, and prompt handler executions', '{requests}');
51
51
  return activeRequests;
52
52
  }
53
- /**
54
- * @deprecated No longer needed. `globalThis.performance.now` is universally
55
- * available on Node ≥24 and Bun ≥1.3. Kept as a no-op export to avoid a
56
- * breaking change for consumers that call it during startup.
57
- */
58
- export function loadPerfHooks() {
59
- // performance is an ambient global declared by @types/node (perf_hooks.d.ts)
60
- // and available in all supported environments (Node ≥24, Bun ≥1.3, workerd).
61
- return Promise.resolve({ performance });
62
- }
63
- /**
64
- * @deprecated No longer needed. `nowMs` now delegates directly to
65
- * `globalThis.performance.now()`, which is universally available on Node ≥24
66
- * and Bun ≥1.3. This function is a no-op and will be removed in a future
67
- * major release.
68
- *
69
- * @returns A promise that resolves immediately.
70
- */
71
- export function initHighResTimer(_perfLoader) {
72
- // No-op: globalThis.performance.now is guaranteed on all supported floors.
73
- return Promise.resolve();
74
- }
75
53
  /**
76
54
  * Returns the current time in milliseconds using `globalThis.performance.now()`.
77
55
  *
@@ -169,89 +147,40 @@ const toBytes = (payload) => {
169
147
  }
170
148
  }
171
149
  };
172
- // ==========================================================================
173
- // Tool execution measurement
174
- // ==========================================================================
175
- /*
176
- * Measured-region semantics, settled in #346 and shared by
177
- * {@link measureToolExecution} and {@link measureResourceExecution}.
178
- *
179
- * The measured region spans everything that decides the client-visible
180
- * outcome: the handler **and** the response pipeline that follows it
181
- * (output-schema validation, `format()`, the enrichment merge and its trailer
182
- * render). Telemetry that closed when the handler returned reported a
183
- * successful call while the client was handed `isError: true`.
184
- *
185
- * Two consequences of that width:
186
- *
187
- * 1. `mcp.*.duration` covers validation, formatting, and the merge, not the
188
- * handler alone. It is time-to-produce-the-result, which is what the
189
- * caller waited for.
190
- * 2. The callback's return value is the assembled client result, so it is no
191
- * longer what `mcp.*.output_bytes` should measure — `content[]` re-renders
192
- * the same data the structured payload carries, and counting both would
193
- * silently redefine an existing series. Callers therefore designate the
194
- * payload explicitly via the `recordOutput` argument, which also feeds
195
- * partial-success detection. `output_bytes` keeps measuring the handler's
196
- * returned domain value, unchanged from 0.12.2. When nothing is
197
- * designated, the callback's return value is measured — the single-value
198
- * form for callers with no assembly step.
199
- */
200
150
  /**
201
- * Wraps a tool's logic function with observability: an OpenTelemetry span,
202
- * OTel metric counters/histogram, payload size capture, and structured log.
203
- *
204
- * The caller supplies the raw tool logic as `toolLogicFn`; this function handles
205
- * all instrumentation so tool handlers stay free of telemetry boilerplate.
206
- *
207
- * On success the resolved value is passed through transparently.
208
- * On failure the error is re-thrown after being recorded on the span and metrics;
209
- * `McpError` instances surface their numeric `code` as the error code attribute.
210
- * A failure anywhere in the callback — handler or response pipeline — is
211
- * recorded, and no output-size histogram is emitted for it.
212
- *
213
- * @template T - The resolved type of the tool's return value.
214
- * @param toolLogicFn - Async function containing the tool's business logic and
215
- * the response assembly that follows it. Receives `context` re-bound to the
216
- * execution span this function opens, so a handler context built from it
217
- * correlates to the span the handler actually runs in rather than to the
218
- * enclosing request span, plus `recordOutput` — call it with the handler's
219
- * domain value to designate what `mcp.tool.output_bytes` and partial-success
220
- * detection measure. Without a call, the callback's return value is measured.
221
- * @param context - Request context extended with `toolName`; used for span/log correlation.
222
- * @param inputPayload - The raw input object passed to the tool, serialized to compute byte size.
223
- * @param successAttributes - Optional thunk evaluated after a successful run; its
224
- * returned key/value map is set on the span as extra attributes. Lets callers
225
- * attach post-hoc signals (e.g. `mcp.tool.enriched`) without coupling this
226
- * function to their domain. Not called on the error path.
227
- * @returns A promise that resolves with the tool's return value or rejects with the original error.
151
+ * Opens `<spanPrefix>:<name>`, runs `logic` inside it with `context` re-bound
152
+ * to that span, and settles the outcome the same way for every kind: an
153
+ * `input_required` signal is protocol control flow and counts as success
154
+ * (recording it as a failure would mark the span ERROR, count an error, and
155
+ * log `isSuccess: false` for every legitimate multi-round-trip request); an
156
+ * `McpError` surfaces its numeric code; anything else is `UNHANDLED_ERROR` or
157
+ * `UNKNOWN_ERROR`. The in-flight gauge and duration bracket the whole run.
228
158
  */
229
- export async function measureToolExecution(toolLogicFn, context, inputPayload, successAttributes) {
159
+ async function measure(kind, name, context, startAttributes, logic, hooks) {
230
160
  const tracer = trace.getTracer(config.openTelemetry.serviceName, config.openTelemetry.serviceVersion);
231
- const { toolName } = context;
232
- return await tracer.startActiveSpan(`tool_execution:${toolName}`, async (span) => {
161
+ return await tracer.startActiveSpan(`${kind.spanPrefix}:${name}`, async (span) => {
233
162
  // The span is active from here down, so everything below — the handler
234
- // context the logic function builds and this function's own completion log
235
- // — correlates to `tool_execution:*` rather than to whatever was active
236
- // when the caller built `context`.
163
+ // context the logic function builds and the completion log — correlates
164
+ // to this span rather than to whatever was active when the caller built
165
+ // `context`.
237
166
  const spanContext = withActiveSpan(context);
238
167
  const activeGauge = getActiveRequestsGauge();
239
168
  activeGauge.add(1);
240
169
  const t0 = nowMs();
241
- const inputBytes = toBytes(inputPayload);
242
170
  span.setAttributes({
243
- [ATTR_CODE_FUNCTION_NAME]: toolName,
244
- [ATTR_CODE_NAMESPACE]: 'mcp-tools',
245
- [ATTR_MCP_TOOL_INPUT_BYTES]: inputBytes,
171
+ [ATTR_CODE_FUNCTION_NAME]: name,
172
+ [ATTR_CODE_NAMESPACE]: kind.namespace,
173
+ ...startAttributes,
246
174
  });
247
- let ok = false;
248
- let inputRequired = false;
249
- let errorCode;
250
- let errorCategory;
251
- let outputBytes = 0;
252
- let partialSuccess = false;
253
- let batchSucceeded;
254
- let batchFailed;
175
+ const outcome = {
176
+ durationMs: 0,
177
+ errorCategory: undefined,
178
+ errorCode: undefined,
179
+ inputRequired: false,
180
+ measuredOutput: undefined,
181
+ ok: false,
182
+ outputBytes: 0,
183
+ };
255
184
  let designatedOutput;
256
185
  let outputDesignated = false;
257
186
  const recordOutput = (payload) => {
@@ -259,59 +188,29 @@ export async function measureToolExecution(toolLogicFn, context, inputPayload, s
259
188
  outputDesignated = true;
260
189
  };
261
190
  try {
262
- const result = await toolLogicFn(spanContext, recordOutput);
263
- ok = true;
264
- const measuredOutput = outputDesignated ? designatedOutput : result;
265
- outputBytes = toBytes(measuredOutput);
266
- // Detect partial success: the measured payload contains a non-empty `failed` array.
267
- // Convention-based — matches the batch response pattern recommended by the design skill.
268
- if (measuredOutput != null &&
269
- typeof measuredOutput === 'object' &&
270
- !Array.isArray(measuredOutput)) {
271
- const obj = measuredOutput;
272
- if (Array.isArray(obj.failed) && obj.failed.length > 0) {
273
- partialSuccess = true;
274
- batchFailed = obj.failed.length;
275
- if (Array.isArray(obj.succeeded))
276
- batchSucceeded = obj.succeeded.length;
277
- }
278
- }
191
+ const result = await logic(spanContext, recordOutput);
192
+ outcome.ok = true;
193
+ outcome.measuredOutput = outputDesignated ? designatedOutput : result;
194
+ outcome.outputBytes = toBytes(outcome.measuredOutput);
279
195
  span.setStatus({ code: SpanStatusCode.OK });
280
- span.setAttribute(ATTR_MCP_TOOL_OUTPUT_BYTES, outputBytes);
281
- if (partialSuccess) {
282
- span.setAttribute(ATTR_MCP_TOOL_PARTIAL_SUCCESS, true);
283
- if (batchFailed !== undefined)
284
- span.setAttribute(ATTR_MCP_TOOL_BATCH_FAILED, batchFailed);
285
- if (batchSucceeded !== undefined)
286
- span.setAttribute(ATTR_MCP_TOOL_BATCH_SUCCEEDED, batchSucceeded);
287
- }
288
- if (successAttributes) {
289
- for (const [key, value] of Object.entries(successAttributes())) {
290
- span.setAttribute(key, value);
291
- }
292
- }
196
+ hooks.onSuccess(span, outcome);
293
197
  return result;
294
- // `ctx.requestInput(...)` unwinds the handler as a thrown signal, but the
295
- // round ended in `input_required` — protocol control flow, not a failure.
296
- // Recording it as one would mark the span ERROR, increment the error
297
- // counter, and log `isSuccess: false` for every legitimate
298
- // multi-round-trip request.
299
198
  }
300
199
  catch (err) {
301
200
  if (isInputRequiredSignal(err)) {
302
- ok = true;
303
- inputRequired = true;
201
+ outcome.ok = true;
202
+ outcome.inputRequired = true;
304
203
  span.setStatus({ code: SpanStatusCode.OK });
305
- span.setAttribute(ATTR_MCP_TOOL_INPUT_REQUIRED, true);
204
+ span.setAttribute(kind.attrs.inputRequired, true);
306
205
  throw err;
307
206
  }
308
207
  if (err instanceof McpError) {
309
- errorCode = String(err.code);
310
- errorCategory = getErrorCategory(err.code);
208
+ outcome.errorCode = String(err.code);
209
+ outcome.errorCategory = getErrorCategory(err.code);
311
210
  }
312
211
  else {
313
- errorCode = err instanceof Error ? 'UNHANDLED_ERROR' : 'UNKNOWN_ERROR';
314
- errorCategory = 'server';
212
+ outcome.errorCode = err instanceof Error ? 'UNHANDLED_ERROR' : 'UNKNOWN_ERROR';
213
+ outcome.errorCategory = 'server';
315
214
  }
316
215
  if (err instanceof Error)
317
216
  span.recordException(err);
@@ -323,16 +222,97 @@ export async function measureToolExecution(toolLogicFn, context, inputPayload, s
323
222
  }
324
223
  finally {
325
224
  activeGauge.add(-1);
326
- const t1 = nowMs();
327
- const durationMs = Math.round((t1 - t0) * 100) / 100;
225
+ outcome.durationMs = Math.round((nowMs() - t0) * 100) / 100;
328
226
  span.setAttributes({
329
- [ATTR_MCP_TOOL_DURATION_MS]: durationMs,
330
- [ATTR_MCP_TOOL_SUCCESS]: ok,
227
+ [kind.attrs.durationMs]: outcome.durationMs,
228
+ [kind.attrs.success]: outcome.ok,
331
229
  });
332
- if (errorCode)
333
- span.setAttribute(ATTR_MCP_TOOL_ERROR_CODE, errorCode);
230
+ if (outcome.errorCode)
231
+ span.setAttribute(kind.attrs.errorCode, outcome.errorCode);
334
232
  span.end();
335
- // Record to OTel metric instruments (durable across restarts)
233
+ hooks.onSettled(spanContext, outcome);
234
+ }
235
+ });
236
+ }
237
+ // ==========================================================================
238
+ // Tool execution measurement
239
+ // ==========================================================================
240
+ const TOOL_KIND = {
241
+ attrs: {
242
+ durationMs: ATTR_MCP_TOOL_DURATION_MS,
243
+ errorCode: ATTR_MCP_TOOL_ERROR_CODE,
244
+ inputRequired: ATTR_MCP_TOOL_INPUT_REQUIRED,
245
+ success: ATTR_MCP_TOOL_SUCCESS,
246
+ },
247
+ namespace: 'mcp-tools',
248
+ spanPrefix: 'tool_execution',
249
+ };
250
+ /**
251
+ * Convention-based partial-success detection: a measured payload carrying a
252
+ * non-empty `failed` array — the batch response shape the design skill
253
+ * recommends. `batchSucceeded` is reported only when a `succeeded` array sits
254
+ * beside it.
255
+ */
256
+ function detectPartialSuccess(output) {
257
+ if (output == null || typeof output !== 'object' || Array.isArray(output))
258
+ return undefined;
259
+ const { failed, succeeded } = output;
260
+ if (!Array.isArray(failed) || failed.length === 0)
261
+ return undefined;
262
+ return {
263
+ batchFailed: failed.length,
264
+ batchSucceeded: Array.isArray(succeeded) ? succeeded.length : undefined,
265
+ };
266
+ }
267
+ /**
268
+ * Wraps a tool's logic function with observability: an OpenTelemetry span,
269
+ * OTel metric counters/histogram, payload size capture, and structured log.
270
+ *
271
+ * On success the resolved value is passed through transparently. On failure
272
+ * the error is re-thrown after being recorded on the span and metrics;
273
+ * `McpError` instances surface their numeric `code` as the error code
274
+ * attribute. A failure anywhere in the callback — handler or response
275
+ * pipeline — is recorded, and no output-size histogram is emitted for it.
276
+ *
277
+ * @template T - The resolved type of the tool's return value.
278
+ * @param toolLogicFn - Async function containing the tool's business logic and
279
+ * the response assembly that follows it. Receives `context` re-bound to the
280
+ * execution span this function opens, so a handler context built from it
281
+ * correlates to the span the handler actually runs in rather than to the
282
+ * enclosing request span, plus `recordOutput` — call it with the handler's
283
+ * domain value to designate what `mcp.tool.output_bytes` and partial-success
284
+ * detection measure. Without a call, the callback's return value is measured.
285
+ * @param context - Request context extended with `toolName`; used for span/log correlation.
286
+ * @param inputPayload - The raw input object passed to the tool, serialized to compute byte size.
287
+ * @param successAttributes - Optional thunk evaluated after a successful run; its
288
+ * returned key/value map is set on the span as extra attributes. Lets callers
289
+ * attach post-hoc signals (e.g. `mcp.tool.enriched`) without coupling this
290
+ * function to their domain. Not called on the error path.
291
+ * @returns A promise that resolves with the tool's return value or rejects with the original error.
292
+ */
293
+ export async function measureToolExecution(toolLogicFn, context, inputPayload, successAttributes) {
294
+ const { toolName } = context;
295
+ const inputBytes = toBytes(inputPayload);
296
+ let partial;
297
+ return await measure(TOOL_KIND, toolName, context, { [ATTR_MCP_TOOL_INPUT_BYTES]: inputBytes }, toolLogicFn, {
298
+ onSuccess: (span, outcome) => {
299
+ span.setAttribute(ATTR_MCP_TOOL_OUTPUT_BYTES, outcome.outputBytes);
300
+ partial = detectPartialSuccess(outcome.measuredOutput);
301
+ if (partial) {
302
+ span.setAttribute(ATTR_MCP_TOOL_PARTIAL_SUCCESS, true);
303
+ span.setAttribute(ATTR_MCP_TOOL_BATCH_FAILED, partial.batchFailed);
304
+ if (partial.batchSucceeded !== undefined) {
305
+ span.setAttribute(ATTR_MCP_TOOL_BATCH_SUCCEEDED, partial.batchSucceeded);
306
+ }
307
+ }
308
+ if (successAttributes) {
309
+ for (const [key, value] of Object.entries(successAttributes())) {
310
+ span.setAttribute(key, value);
311
+ }
312
+ }
313
+ },
314
+ onSettled: (spanContext, outcome) => {
315
+ const { durationMs, errorCategory, errorCode, inputRequired, ok, outputBytes } = outcome;
336
316
  const m = getToolMetrics();
337
317
  const metricAttrs = { [ATTR_MCP_TOOL_NAME]: toolName, [ATTR_MCP_TOOL_SUCCESS]: ok };
338
318
  const toolAttrs = { [ATTR_MCP_TOOL_NAME]: toolName };
@@ -362,10 +342,14 @@ export async function measureToolExecution(toolLogicFn, context, inputPayload, s
362
342
  inputBytes,
363
343
  outputBytes,
364
344
  ...(inputRequired && { inputRequired }),
365
- ...(partialSuccess && { partialSuccess, batchSucceeded, batchFailed }),
345
+ ...(partial && {
346
+ partialSuccess: true,
347
+ batchSucceeded: partial.batchSucceeded,
348
+ batchFailed: partial.batchFailed,
349
+ }),
366
350
  },
367
351
  }));
368
- }
352
+ },
369
353
  });
370
354
  }
371
355
  // ==========================================================================
@@ -382,10 +366,20 @@ function getResourceMetrics() {
382
366
  resourceOutputBytes ??= createHistogram('mcp.resource.output_bytes', 'Resource output payload size', 'bytes');
383
367
  return { resourceReadCounter, resourceReadDuration, resourceReadErrors, resourceOutputBytes };
384
368
  }
369
+ const RESOURCE_KIND = {
370
+ attrs: {
371
+ durationMs: ATTR_MCP_RESOURCE_DURATION_MS,
372
+ errorCode: ATTR_MCP_RESOURCE_ERROR_CODE,
373
+ inputRequired: ATTR_MCP_RESOURCE_INPUT_REQUIRED,
374
+ success: ATTR_MCP_RESOURCE_SUCCESS,
375
+ },
376
+ namespace: 'mcp-resources',
377
+ spanPrefix: 'resource_read',
378
+ };
385
379
  /**
386
380
  * Wraps a resource handler with observability: OTel span, metric counters/histogram,
387
381
  * and a structured log. Mirrors {@link measureToolExecution} but tuned for resource reads,
388
- * including the measured-region semantics documented there.
382
+ * including the measured-region semantics documented above.
389
383
  *
390
384
  * @template T - The resolved type of the resource handler's return value.
391
385
  * @param resourceLogicFn - Async function containing the resource handler and the
@@ -398,76 +392,13 @@ function getResourceMetrics() {
398
392
  * @returns A promise that resolves with the handler's return value or rejects with the original error.
399
393
  */
400
394
  export async function measureResourceExecution(resourceLogicFn, context, meta) {
401
- const tracer = trace.getTracer(config.openTelemetry.serviceName, config.openTelemetry.serviceVersion);
402
395
  const { resourceName } = context;
403
- return await tracer.startActiveSpan(`resource_read:${resourceName}`, async (span) => {
404
- // Active from here down — see the note in `measureToolExecution`.
405
- const spanContext = withActiveSpan(context);
406
- const activeGauge = getActiveRequestsGauge();
407
- activeGauge.add(1);
408
- const t0 = nowMs();
409
- span.setAttributes({
410
- [ATTR_CODE_FUNCTION_NAME]: resourceName,
411
- [ATTR_CODE_NAMESPACE]: 'mcp-resources',
412
- [ATTR_MCP_RESOURCE_URI]: meta.uri,
413
- [ATTR_MCP_RESOURCE_MIME_TYPE]: meta.mimeType,
414
- });
415
- let ok = false;
416
- let inputRequired = false;
417
- let errorCode;
418
- let outputBytes = 0;
419
- let designatedOutput;
420
- let outputDesignated = false;
421
- const recordOutput = (payload) => {
422
- designatedOutput = payload;
423
- outputDesignated = true;
424
- };
425
- try {
426
- const result = await resourceLogicFn(spanContext, recordOutput);
427
- ok = true;
428
- outputBytes = toBytes(outputDesignated ? designatedOutput : result);
429
- span.setStatus({ code: SpanStatusCode.OK });
430
- span.setAttribute(ATTR_MCP_RESOURCE_SIZE_BYTES, outputBytes);
431
- return result;
432
- // `ctx.requestInput(...)` unwinds the handler as a thrown signal, but the
433
- // round ended in `input_required` — protocol control flow, not a failure.
434
- // Recording it as one would mark the span ERROR, increment the error
435
- // counter, and log `isSuccess: false` for every legitimate
436
- // multi-round-trip request.
437
- }
438
- catch (err) {
439
- if (isInputRequiredSignal(err)) {
440
- ok = true;
441
- inputRequired = true;
442
- span.setStatus({ code: SpanStatusCode.OK });
443
- span.setAttribute(ATTR_MCP_RESOURCE_INPUT_REQUIRED, true);
444
- throw err;
445
- }
446
- if (err instanceof McpError)
447
- errorCode = String(err.code);
448
- else if (err instanceof Error)
449
- errorCode = 'UNHANDLED_ERROR';
450
- else
451
- errorCode = 'UNKNOWN_ERROR';
452
- if (err instanceof Error)
453
- span.recordException(err);
454
- span.setStatus({
455
- code: SpanStatusCode.ERROR,
456
- message: err instanceof Error ? err.message : String(err),
457
- });
458
- throw err;
459
- }
460
- finally {
461
- activeGauge.add(-1);
462
- const t1 = nowMs();
463
- const durationMs = Math.round((t1 - t0) * 100) / 100;
464
- span.setAttributes({
465
- [ATTR_MCP_RESOURCE_DURATION_MS]: durationMs,
466
- [ATTR_MCP_RESOURCE_SUCCESS]: ok,
467
- });
468
- if (errorCode)
469
- span.setAttribute(ATTR_MCP_RESOURCE_ERROR_CODE, errorCode);
470
- span.end();
396
+ return await measure(RESOURCE_KIND, resourceName, context, { [ATTR_MCP_RESOURCE_URI]: meta.uri, [ATTR_MCP_RESOURCE_MIME_TYPE]: meta.mimeType }, resourceLogicFn, {
397
+ onSuccess: (span, outcome) => {
398
+ span.setAttribute(ATTR_MCP_RESOURCE_SIZE_BYTES, outcome.outputBytes);
399
+ },
400
+ onSettled: (spanContext, outcome) => {
401
+ const { durationMs, errorCode, inputRequired, ok, outputBytes } = outcome;
471
402
  const m = getResourceMetrics();
472
403
  const metricAttrs = {
473
404
  [ATTR_MCP_RESOURCE_NAME]: resourceName,
@@ -492,7 +423,7 @@ export async function measureResourceExecution(resourceLogicFn, context, meta) {
492
423
  mimeType: meta.mimeType,
493
424
  },
494
425
  }));
495
- }
426
+ },
496
427
  });
497
428
  }
498
429
  // ==========================================================================
@@ -520,14 +451,23 @@ function getPromptMetrics() {
520
451
  promptMessageCount,
521
452
  };
522
453
  }
454
+ const PROMPT_KIND = {
455
+ attrs: {
456
+ durationMs: ATTR_MCP_PROMPT_DURATION_MS,
457
+ errorCode: ATTR_MCP_PROMPT_ERROR_CODE,
458
+ inputRequired: ATTR_MCP_PROMPT_INPUT_REQUIRED,
459
+ success: ATTR_MCP_PROMPT_SUCCESS,
460
+ },
461
+ namespace: 'mcp-prompts',
462
+ spanPrefix: 'prompt_generation',
463
+ };
523
464
  /**
524
465
  * Wraps a prompt generate function with observability: an OpenTelemetry span,
525
466
  * OTel metric counters/histograms, payload size capture, and structured log.
526
467
  *
527
- * Prompts can now perform meaningful work (conditional logic, async data fetches,
468
+ * Prompts can perform meaningful work (conditional logic, async data fetches,
528
469
  * multi-message assembly), so they get the same instrumentation depth as tools
529
- * and resources. Kept symmetric to {@link measureToolExecution} and
530
- * {@link measureResourceExecution}.
470
+ * and resources.
531
471
  *
532
472
  * @template T - The resolved type of the prompt generate function's return value.
533
473
  * @param promptLogicFn - Zero-argument async function containing the prompt's generate logic.
@@ -536,79 +476,18 @@ function getPromptMetrics() {
536
476
  * @returns A promise that resolves with the generate result or rejects with the original error.
537
477
  */
538
478
  export async function measurePromptGeneration(promptLogicFn, context, inputPayload) {
539
- const tracer = trace.getTracer(config.openTelemetry.serviceName, config.openTelemetry.serviceVersion);
540
479
  const { promptName } = context;
541
- return await tracer.startActiveSpan(`prompt_generation:${promptName}`, async (span) => {
542
- // Active from here down — see the note in `measureToolExecution`. Prompt
543
- // generators take no handler context, so this only re-binds the completion
544
- // log below.
545
- const spanContext = withActiveSpan(context);
546
- const activeGauge = getActiveRequestsGauge();
547
- activeGauge.add(1);
548
- const t0 = nowMs();
549
- const inputBytes = toBytes(inputPayload);
550
- span.setAttributes({
551
- [ATTR_CODE_FUNCTION_NAME]: promptName,
552
- [ATTR_CODE_NAMESPACE]: 'mcp-prompts',
553
- [ATTR_MCP_PROMPT_INPUT_BYTES]: inputBytes,
554
- });
555
- let ok = false;
556
- let inputRequired = false;
557
- let errorCode;
558
- let errorCategory;
559
- let outputBytes = 0;
560
- let messageCount = 0;
561
- try {
562
- const result = await promptLogicFn();
563
- ok = true;
564
- outputBytes = toBytes(result);
565
- if (Array.isArray(result))
566
- messageCount = result.length;
567
- span.setStatus({ code: SpanStatusCode.OK });
568
- span.setAttribute(ATTR_MCP_PROMPT_OUTPUT_BYTES, outputBytes);
480
+ const inputBytes = toBytes(inputPayload);
481
+ let messageCount = 0;
482
+ return await measure(PROMPT_KIND, promptName, context, { [ATTR_MCP_PROMPT_INPUT_BYTES]: inputBytes }, promptLogicFn, {
483
+ onSuccess: (span, outcome) => {
484
+ if (Array.isArray(outcome.measuredOutput))
485
+ messageCount = outcome.measuredOutput.length;
486
+ span.setAttribute(ATTR_MCP_PROMPT_OUTPUT_BYTES, outcome.outputBytes);
569
487
  span.setAttribute(ATTR_MCP_PROMPT_MESSAGE_COUNT, messageCount);
570
- return result;
571
- // `ctx.requestInput(...)` unwinds the handler as a thrown signal, but the
572
- // round ended in `input_required` — protocol control flow, not a failure.
573
- // Recording it as one would mark the span ERROR, increment the error
574
- // counter, and log `isSuccess: false` for every legitimate
575
- // multi-round-trip request.
576
- }
577
- catch (err) {
578
- if (isInputRequiredSignal(err)) {
579
- ok = true;
580
- inputRequired = true;
581
- span.setStatus({ code: SpanStatusCode.OK });
582
- span.setAttribute(ATTR_MCP_PROMPT_INPUT_REQUIRED, true);
583
- throw err;
584
- }
585
- if (err instanceof McpError) {
586
- errorCode = String(err.code);
587
- errorCategory = getErrorCategory(err.code);
588
- }
589
- else {
590
- errorCode = err instanceof Error ? 'UNHANDLED_ERROR' : 'UNKNOWN_ERROR';
591
- errorCategory = 'server';
592
- }
593
- if (err instanceof Error)
594
- span.recordException(err);
595
- span.setStatus({
596
- code: SpanStatusCode.ERROR,
597
- message: err instanceof Error ? err.message : String(err),
598
- });
599
- throw err;
600
- }
601
- finally {
602
- activeGauge.add(-1);
603
- const t1 = nowMs();
604
- const durationMs = Math.round((t1 - t0) * 100) / 100;
605
- span.setAttributes({
606
- [ATTR_MCP_PROMPT_DURATION_MS]: durationMs,
607
- [ATTR_MCP_PROMPT_SUCCESS]: ok,
608
- });
609
- if (errorCode)
610
- span.setAttribute(ATTR_MCP_PROMPT_ERROR_CODE, errorCode);
611
- span.end();
488
+ },
489
+ onSettled: (spanContext, outcome) => {
490
+ const { durationMs, errorCategory, errorCode, inputRequired, ok, outputBytes } = outcome;
612
491
  const m = getPromptMetrics();
613
492
  const metricAttrs = { [ATTR_MCP_PROMPT_NAME]: promptName, [ATTR_MCP_PROMPT_SUCCESS]: ok };
614
493
  const promptAttrs = { [ATTR_MCP_PROMPT_NAME]: promptName };
@@ -626,10 +505,9 @@ export async function measurePromptGeneration(promptLogicFn, context, inputPaylo
626
505
  });
627
506
  }
628
507
  const logFn = ok ? logger.info : logger.error;
629
- const promptMessage = ok
508
+ logFn.call(logger, ok
630
509
  ? TELEMETRY_LOG_MESSAGES.promptGenerationFinished
631
- : TELEMETRY_LOG_MESSAGES.promptGenerationFailed;
632
- logFn.call(logger, promptMessage, withExtra(spanContext, {
510
+ : TELEMETRY_LOG_MESSAGES.promptGenerationFailed, withExtra(spanContext, {
633
511
  promptName,
634
512
  metrics: {
635
513
  durationMs,
@@ -641,7 +519,7 @@ export async function measurePromptGeneration(promptLogicFn, context, inputPaylo
641
519
  ...(inputRequired && { inputRequired }),
642
520
  },
643
521
  }));
644
- }
522
+ },
645
523
  });
646
524
  }
647
525
  //# sourceMappingURL=performance.js.map