LearnNewsExamplesServices
Frontmatter
id16588
titleCompaction reaches the tool description but not the schema beside it
stateClosed
labels
enhancementaiarchitectureperformance
assigneesneo-kimi-phoebe
createdAtAug 6, 2026, 12:10 PM
updatedAtAug 9, 2026, 11:14 PM
githubUrlhttps://github.com/neomjs/neo/issues/16588
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 9, 2026, 11:14 PM

Compaction reaches the tool description but not the schema beside it

Closed Backlog/active-chunk-13 enhancementaiarchitectureperformance
neo-opus-grace
neo-opus-grace commented on Aug 6, 2026, 12:10 PM

Context

Neo advertises 145+ MCP tools across six servers. To keep tools/list affordable, ToolService compacts each tool's description to 120 characters and defers the full text to get_mcp_tool_handbook, which an agent loads on demand before its first call. That design is real, switched on everywhere, and works.

It covers the tool description. It does not cover the schema shipped alongside it.

Found while reviewing PR #16583, where I drafted a description-budget Required Action on the premise that verbose property descriptions load into every agent's context. @tobiu corrected the premise — tools/list is compacted by design. Verifying that correction is what surfaced the narrower gap below, so the finding belongs to the correction, not to the PR. No live incident is attributed to this. It is a token-budget defect, filed against the design's own stated intent.

The Problem

ToolService.mjs:169-171 compacts the operation description:

const listDescription = me.compactToolDescriptions
    ? me.buildToolListDescription(operation, fullDescription)   // -> <= toolListDescriptionMaxLength
    : fullDescription;

Twenty lines later, :206-215 assembles what actually goes over the wire:

const toolForListing = {
    name       : tool.name,
    title      : tool.title,
    description: tool.description,     // compacted
    inputSchema: inputJsonSchema       // NOT compacted, NOT projected
};
if (outputJsonSchema !== null) {
    toolForListing.outputSchema = outputJsonSchema;   // same
}

Every description: inside those schemas rides along in full. The path is unbroken and each link is verified:

link evidence
OpenAPI description: → Zod .describe() openApiValidator.mjs:259-260
Zod .describe() → JSON Schema description measured, not assumed — z.toJSONSchema round-trip preserves the marker verbatim
JSON Schema → tools/list ToolService.mjs:210, emitted whole

Measured

Across five servers' openapi.yaml (file-system, github-workflow, knowledge-base, memory-core, neural-link):

server operations operation-description bytes schema-description bytes schema description fields
memory-core 42 16,525 22,065 271
github-workflow 24 15,954 11,250 133
knowledge-base 13 11,978 8,662 91
neural-link 59 13,385 7,560 164
file-system 7 555 235 7
total 145 58,397 49,772 666

All six servers set compactToolDescriptions: true with toolListDescriptionMaxLength: 120, so the operation half is capped at ≤ 17,400 bytes (145 × 120, an upper bound — most are shorter). The schema half stays at 49,772 bytes, uncompacted.

So after the compaction does its job, schema descriptions are ≥ 74% of the remaining description payload. The half that was engineered away is the smaller one.

Measurement bound: five of six servers. gitlab-workflow also sets compactToolDescriptions: true and is excluded from the table, so the real total is higher, not lower.

The Architectural Reality

The asymmetry is not arbitrary — there is a genuine constraint the fix must respect.

An agent needs the input schema's shape (types, required, enums) at list time, because MCP clients validate arguments against inputSchema before dispatch. You cannot defer the schema itself the way you defer prose.

But shape and prose are separable. {"type":"boolean"} is what validation needs; the sentence explaining when to pass false is what the handbook exists for. Today they travel together because nothing splits them.

buildToolHandbookEntry (ToolService.mjs:342-360) already owns the deferred-detail surface and already receives the operation. It is the natural home for the stripped prose — no new mechanism, no second cache.

The Fix

  1. Add a compactToolSchemas config beside compactToolDescriptions (same default-off, same per-server opt-in, so the change is observable before it is universal).
  2. When enabled, strip description from inputSchema / outputSchema nodes in toolForListing — recursively, preserving every shape-bearing key (type, required, enum, properties, items, additionalProperties, $ref targets, format, default).
  3. Carry the stripped prose into the existing handbook entry so get_mcp_tool_handbook returns the fully-described schema.
  4. Extend the advertised-surface digest test to cover the projected shape, so a future edit cannot silently reintroduce the prose.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
ToolService.compactToolSchemas (new config) this ticket Strips schema description from tools/list; shape untouched Default false — current behavior member JSDoc, beside compactToolDescriptions sibling precedent ToolService.mjs:33
tools/list inputSchema / outputSchema ToolService.mjs:206-215 Shape-identical, prose removed when enabled Unprojected when disabled measured 49,772 bytes across 666 fields
get_mcp_tool_handbook payload ToolService.mjs:342-360 Gains the fully-described schema Unchanged when disabled handbook entry JSDoc already the deferred-detail surface
advertised-surface digest ai/mcp/ToolService.mjs ADVERTISED_SURFACE_DIGEST_LABEL Covers the projected shape digest already includes input schemas (PR #16548)

Decision Record impact

none. This completes an existing projection policy rather than introducing authority; no runtime contract changes shape, and the behavior is opt-in per server.

Acceptance Criteria

(Ticked with receipts by @neo-kimi-phoebe at PR #16832 cycle-2, per reviewer RA prescription — edit-trail comment below; @neo-opus-grace holds explicit revert authority.)

  • With compactToolSchemas: true, no annotation-position description survives in a tools/list inputSchema or outputSchema served on the default listing route. (Restated from literal key-absence: an application property NAMED description under properties is data and survives — position-aware semantics. Receipt: schemaCompaction.spec.mjs real file-system contract test + the property-survival control.)
  • Shape is provably unchanged: a schema projected with the flag on validates exactly the same accept/reject set as with it off, asserted over a fixture carrying nested properties, items, enum, $ref and additionalProperties. (Receipt: the strip-twin equality + the Ajv accept/reject battery, 6 value shapes.)
  • get_mcp_tool_handbook returns the schema with descriptions for the same tool — the prose is relocated, not deleted. Proven by asserting a specific description string is absent from the list payload and present in the handbook payload. (Receipt: the real file-system contract AC-3 test. Named exception: exact-profile routes serve their constrained schema DESCRIBED by design — get_mcp_tool_handbook is policy-refused inside an exact projection, so the listing is the only surface a projected seat has for the profile's constraint prose; compaction targets the default listing only. Witness: the local-readonly-probe test.)
  • Default remains false; a server that does not opt in emits byte-identical tools/list output to today. (Receipt: the AC-4 default-off test, incl. the schema-free handbook entry shape.)
  • The advertised-surface digest test covers the projected shape, and fails if prose reappears in the list payload. (Receipt: the digest canary + the production-bound pins in schemaCompaction.spec.mjs; the identity-strip mutation reds 8 tests there. Digest-vs-listing policy distinction (cycle-3): with the flag on, the digest normalizes every route to the validation shape — exact-profile prose stays LISTED by the named exception but does not shift the capability digest: prose-only mutation stable, validation-shape mutation reds.)
  • Measured before/after tools/list byte counts recorded per server in the PR, using the same method as the table above. (Receipt: the PR body's six-server table, regenerated at the repair head: 187,651 → 119,141 bytes.)

Out of Scope

  • Editing any existing description to be shorter. The prose is not the defect; shipping it eagerly is. Individual descriptions stay as authored — several encode non-obvious safety properties (ingest_source_files's viaMcp / materializationAttempt are the current example) and shortening them would trade a measured token cost for an unmeasured comprehension cost.
  • Compacting the operation description further. 120 chars is working as designed.
  • Any change to x-neo-tool-summary, tool tiers, or the harness projection policy.

Avoided Traps

"Just drop the descriptions from the YAML." They are the input to the handbook, and openApiValidator also uses them for Zod introspection. The contract stays the SSOT; only the projection changes. Deleting at the source would fix the byte count and destroy the on-demand payload.

"Defer the whole schema like the description." Rejected: MCP clients validate arguments against inputSchema before dispatch, so the shape must be present at list time. Only prose is deferrable, which is why the fix is a recursive key-strip rather than a second lazy surface.

"Measure it later." The AC requires before/after byte counts by the stated method precisely because "this should reduce tokens" is the kind of claim that ships unverified.

Related

  • PR #16583 — where the premise correction that surfaced this occurred; that PR is not the cause and needs no change.
  • PR #16548 — established that a tool's input schema is part of the advertised-surface digest, which is why AC 5 is reachable.
  • #16585 — the other consumer of buildZodSchema. Independent: that one is about the contract being incomplete, this one about the projection being unfiltered.

Handoff Retrieval Hints

  • query_raw_memories: "tools/list compaction schema description projection handbook"
  • Source anchors: ai/mcp/ToolService.mjs:169-171 (compaction), :206-215 (emit), :342-360 (handbook entry), ai/mcp/validation/openApiValidator.mjs:259-260 (description → .describe()).
  • Live latest-open sweep: checked the latest 20 open issues 2026-08-06 plus a 12-message A2A claim scan; no equivalent found and no in-flight claim.

Origin Session ID: 8921d480-6087-4bfa-abe0-4f47873e06c4

Authored by @neo-opus-grace (Claude Opus 5).

tobiu referenced in commit 0488ab9 - "feat(ai): compactToolSchemas — schema prose leaves tools/list, relocates to the handbook (#16588) (#16832) on Aug 9, 2026, 11:14 PM
tobiu closed this issue on Aug 9, 2026, 11:14 PM