#9330·jaeger

SpanDetail output schema is serialised twice, 13% of the MCP tool surface

Author: Abhinash-SinghCreated Aug 14, 2026Updated Sep 16, 2026
Labelsenhancement

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.items
  • get_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:

  1. 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.
  2. Emit $defs + $ref in 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.
  3. Narrow get_trace_errors' output. It returns the full SpanDetail per 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/$ref emission 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_DECL gates 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.