SpanDetail output schema is serialised twice, 13% of the MCP tool surface
Requirement
The MCP tool surface is the fixed cost every agent session pays before fetching any telemetry. Operators running smaller-context models need that surface to be as small as it can be without losing information, so the budget goes to spans rather than to schema boilerplate.
Problem
SpanDetail is serialised in full in two separate output schemas. MCP has no
shared-definitions mechanism (no $defs/$ref in the emitted schemas), so a type
used by more than one tool is repeated verbatim.
Measured against jaeger all-in-one at b3076cc with memory storage and
ai.mcp: {}, reading tools/list off the wire (compact JSON):
- 9 tools, 15,048 bytes of tool definitions
- Output schemas: 9,763 bytes — 64% of that
- Input schemas: 3,254 bytes (21%); descriptions: 1,378 bytes (9%)
The repeated definition is 1,799 bytes, appearing at:
get_span_details.outputSchema.properties.spans.itemsget_trace_errors.outputSchema.properties.spans.items
with three further duplicated types nested inside it, already counted within that
1,799: events.items (317 B), links.items (306 B), status (250 B).
Separately, the 162-byte trace_id input schema is shared by get_critical_path
and get_trace_errors.
Recoverable total: 1,961 bytes, 13% of the tool surface.
Credit to @Animesh-Parashar, who first surfaced the SpanDetail duplication in
#9135. One refinement on the figure quoted there: 3,598 bytes is the combined size
of both copies, but one definition has to exist regardless, so deduplication
recovers one copy — 1,799 bytes, not 3,598. Worth pinning down before it becomes
the justification for a change, since it halves the payoff.
A methodology note for anyone re-measuring: comparing whole schemas for
byte-identity finds almost none of this. SpanDetail is nested inside two
different output schemas rather than repeated at top level, so the detection has to
recurse into subschemas.
Proposal
Options, roughly in increasing order of effort:
- Do nothing, but document it. 13% is real but not dramatic, and it is a fixed cost rather than a per-call one. Response payloads are unbounded and currently ship twice (~2.2× overhead, see #9135), so they dominate the total budget. This may simply be the wrong thing to optimise first.
- Emit
$defs+$refin the output schemas if the Go SDK's schema generator can be persuaded to, and if the models consuming these reliably resolve$ref. That second condition is the risk: a model that does not resolve refs sees a less useful schema, which would trade context bytes for accuracy — the wrong direction. - Narrow
get_trace_errors' output. It returns the fullSpanDetailper span; an error-focused tool may not need every field, in which case the duplication partly disappears as a side effect of a tighter contract.
I lean toward (1) or (3). (2) is the obvious fix but its benefit depends on model behaviour we have not measured, and this is exactly the kind of change the evaluation work in #9135 exists to decide empirically rather than by intuition.
Open questions
- Does the Go SDK's schema generation support
$defs/$refemission today? - Is there a preference for keeping tool schemas fully self-contained, deliberately, so that no client needs ref resolution?
- Is output-schema size worth optimising at all given
JSON_SCHEMA_FOR_FUNC_DECLgates whether these reach the model in the first place?
Happy to send a PR for whichever direction maintainers prefer, though I would rather see the direction settled here first than guess.
Source: jaegertracing/jaeger