test(metrics): #403 review — lock the registerTool monkeypatch + reword overhead note (#402)

DO-1: add an integration test (test/mock/tool-timing-server.test.mjs) that
constructs a real createDocmostMcpServer with a spy onMetric, links a Client
over InMemoryTransport, invokes get_workspace (no input schema, so the wrapped
handler always runs) and asserts onMetric fired with
("mcp_tool_duration_seconds", <number>, { tool: "get_workspace" }). This locks
that the monkeypatch wraps every tool AND labels with the registration name —
which the isolated timeToolHandler unit test does not. Mutation-verified
(mislabel -> test fails). The tool's expected backend failure (ECONNREFUSED)
is tolerated: the wrapper times in a finally on throw too, so the metric fires.

DO-2: reword the mcp.service.ts "zero overhead when disabled" comment to
"negligible overhead" — the registerTool wrapper still runs performance.now()
+ an async try/finally per tool call when onMetric is undefined (the
`onMetric?.()` short-circuits the label build; cost is immaterial at
tool-call rate), so "zero" was literally inaccurate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 02:38:11 +03:00
parent 7a9d719877
commit 8f5f5877b3
2 changed files with 93 additions and 3 deletions
@@ -338,9 +338,12 @@ export class McpService implements OnModuleDestroy {
// mapping) is owned by SandboxStore.asSink().
// Route the package's dependency-neutral metric samples onto the
// prom-client registry. When metrics are disabled, onMetric is
// undefined → the package's tool-timer/timeout hooks no-op with
// zero overhead. labels?.tool is guarded defensively (the tool
// wrapper always sets it).
// undefined → the package's tool-timer/timeout hooks are a
// negligible-overhead no-op: the registerTool wrapper still runs a
// performance.now() + async try/finally per tool call, but the
// `onMetric?.()` short-circuits so no label/object is built. (Cost
// is immaterial at LLM tool-call rate.) labels?.tool is guarded
// defensively (the tool wrapper always sets it).
return {
...resolved.config,
sandbox: this.sandboxStore.asSink(),