Telemetry: argument-schema rejections emit nothing, so bad tool calls are invisible
The ask
Please record a tool call that fails input-schema validation. Today it produces no
span, no metric, and no log record, so it is not counted as a failure and not counted as
a call at all.
The category already exists in the vocabulary you shipped in v1.7.4 (invalid_argument).
This is only about the events reaching it.
Current behavior (v1.7.4)
docs/observability.md says, twice:
Instrumentation is automatic — every tool call emits a span, metrics, and a log record.
Each tool call produces a span, three metric updates, and one log record.
That holds for every call that reaches a handler. It does not hold for a call the MCP SDK
rejects first.
Every tool declares a z.strictObject input schema (src/server.ts:113 and the
equivalent in each registerTool block). The SDK validates against it and returns an
error result itself, without invoking the registered handler. withTelemetry
(src/telemetry/local.ts:149) wraps the handler, so on a rejected argument nothing in
src/telemetry/ runs:
- no
tool/<name> span
- no
tool.calls increment, so the denominator is wrong too
- no
tool.errors increment
- no log record
- no local JSONL
ToolEvent
The repository's own test suite documents that this path exists and is reachable.
src/server.test.ts:104-110 defines:
const refusalFor = async (name: string, args: Record<string, unknown>): Promise<string> => {
const result = (await client.callTool({ name, arguments: args })) as ToolResult;
expect(result.isError, `${name} accepted ${JSON.stringify(args)}`).toBe(true);
return result.content?.[0]?.text ?? "";
};
used by the case named "refuses a misspelled optional argument instead of running the
default". isError cannot have come from a handler: formatResult (src/server.ts:51-53)
is the only result builder in the file and it never sets isError. So the refusal is
produced outside the wrapped handler, which is exactly why telemetry does not see it.
Reproducing
With any OTLP endpoint configured, call a tool with a misspelled optional argument, for
example list_designs with searchPath instead of path (the case the test above
covers). The client receives isError: true. The backend receives nothing.
Why it matters
"The agent is calling this tool with the wrong arguments" is one of the most actionable
things per-tool telemetry can tell a server author, because the fix is usually a tool
description or a schema change rather than a code change. Right now that class of
failure reports a clean sheet.
It also skews the numbers that are reported. Since tool.calls never increments,
a tool whose arguments are frequently rejected looks like a tool with low usage and a
perfect success rate. Success rate is computed over the calls that got through, so the
worse the schema mismatch, the healthier the tool looks.
This is not specific to any one backend or deployment. Any server built on
registerTool plus a zod schema has the same blind spot.
Acceptance criteria
Deliberately left to you
- Where to hook it. Wrapping at the server or transport level, a
registerTool
wrapper that validates before delegating, or moving validation inside the instrumented
handler are all reasonable and have different trade-offs. I have no view on which.
- Whether a rejected call belongs in
tool.duration. There is a real argument that a
validation failure has no meaningful duration and would distort the histogram.
- Whether to distinguish it from a handler-raised
invalid_argument, for instance
with a separate error.class value. Grouping them is defensible; so is separating them.
- Whether the local JSONL
ToolEvent should also record it, or whether that file is
deliberately handler-scoped.
Not asking for
To be explicit, since this follows two earlier telemetry issues: no new vocabulary, no
change to the eight categories from v1.7.4, and nothing shaped around a particular
backend. The v1.7.4 taxonomy is a good fit as it stands and we have adapted our side to
it rather than the other way round.
Happy to open a PR if that's easier than specifying it.
Telemetry: argument-schema rejections emit nothing, so bad tool calls are invisible
The ask
Please record a tool call that fails input-schema validation. Today it produces no
span, no metric, and no log record, so it is not counted as a failure and not counted as
a call at all.
The category already exists in the vocabulary you shipped in v1.7.4 (
invalid_argument).This is only about the events reaching it.
Current behavior (v1.7.4)
docs/observability.mdsays, twice:That holds for every call that reaches a handler. It does not hold for a call the MCP SDK
rejects first.
Every tool declares a
z.strictObjectinput schema (src/server.ts:113and theequivalent in each
registerToolblock). The SDK validates against it and returns anerror result itself, without invoking the registered handler.
withTelemetry(
src/telemetry/local.ts:149) wraps the handler, so on a rejected argument nothing insrc/telemetry/runs:tool/<name>spantool.callsincrement, so the denominator is wrong tootool.errorsincrementToolEventThe repository's own test suite documents that this path exists and is reachable.
src/server.test.ts:104-110defines:used by the case named "refuses a misspelled optional argument instead of running the
default".
isErrorcannot have come from a handler:formatResult(src/server.ts:51-53)is the only result builder in the file and it never sets
isError. So the refusal isproduced outside the wrapped handler, which is exactly why telemetry does not see it.
Reproducing
With any OTLP endpoint configured, call a tool with a misspelled optional argument, for
example
list_designswithsearchPathinstead ofpath(the case the test abovecovers). The client receives
isError: true. The backend receives nothing.Why it matters
"The agent is calling this tool with the wrong arguments" is one of the most actionable
things per-tool telemetry can tell a server author, because the fix is usually a tool
description or a schema change rather than a code change. Right now that class of
failure reports a clean sheet.
It also skews the numbers that are reported. Since
tool.callsnever increments,a tool whose arguments are frequently rejected looks like a tool with low usage and a
perfect success rate. Success rate is computed over the calls that got through, so the
worse the schema mismatch, the healthier the tool looks.
This is not specific to any one backend or deployment. Any server built on
registerToolplus a zod schema has the same blind spot.Acceptance criteria
other failed call: span,
tool.calls,tool.errors, and a log recordtool.outcomeiserroranderror.typeisinvalid_argumenterror.messagecarries the validation failure, so the offending argument isidentifiable
tool.callscounts it, so success rate is computed over all attemptsdocs/observability.mdstates which calls are instrumented, so the "every toolcall" claim stays true
Deliberately left to you
registerToolwrapper that validates before delegating, or moving validation inside the instrumented
handler are all reasonable and have different trade-offs. I have no view on which.
tool.duration. There is a real argument that avalidation failure has no meaningful duration and would distort the histogram.
invalid_argument, for instancewith a separate
error.classvalue. Grouping them is defensible; so is separating them.ToolEventshould also record it, or whether that file isdeliberately handler-scoped.
Not asking for
To be explicit, since this follows two earlier telemetry issues: no new vocabulary, no
change to the eight categories from v1.7.4, and nothing shaped around a particular
backend. The v1.7.4 taxonomy is a good fit as it stands and we have adapted our side to
it rather than the other way round.
Happy to open a PR if that's easier than specifying it.