perf: cache encoded tools/list schemas - #3407
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Cached mutable bytes can be corrupted concurrently, and the global cache can grow without bound.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Caches encoded tool schemas to reduce repeated tools/list serialization work.
Changes:
- Adds process-wide schema encoding cache and response middleware.
- Preserves registered schemas for SDK validation.
- Adds wire-parity, concurrency, validation, and benchmark coverage.
| File | Description |
|---|---|
pkg/inventory/server_tool.go |
Returns the registered tool internally. |
pkg/inventory/registry.go |
Installs schema-caching middleware. |
pkg/inventory/encoded_schemas.go |
Implements schema encoding cache. |
pkg/inventory/encoded_schemas_test.go |
Tests cache behavior and validation. |
pkg/github/encoded_schemas_test.go |
Tests wire parity and benchmarks performance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| input bool | ||
| } | ||
|
|
||
| var encodedSchemas sync.Map // encodedSchemaKey -> func() (json.RawMessage, error) |
There was a problem hiding this comment.
Investigated this against the intended deployments. The OSS HTTP factory constructs staticTools once in NewDefaultInventoryFactory and reuses those definitions across requests. The remote server similarly caches its tool definitions and builds request inventories from that shared catalog. Library usage here exists to share implementation between the OSS and remote server repositories, rather than to support arbitrary request-generated tool catalogs.
For these consumers, cache growth is bounded by the finite set of source-schema pointers and their input/output roles. Registration's header-annotation clones do not add cache keys because the input key uses the original source pointer. Request count, filters, and feature selection therefore do not continuously add entries.
The broader scenario described here is technically possible: a custom factory allocating fresh schemas on every request would retain them in this cache. That is not how either intended consumer constructs its definitions, so I have left the implementation unchanged rather than introduce eviction (and repeated encoding) for that scenario.
Auto-replied by the GitHub Copilot app
There was a problem hiding this comment.
Added lifecycle regression coverage in 2aa8fac: the actual default HTTP inventory factory preserves source-schema identity across repeated requests for all three host types; 20 fresh inventories and server registrations reuse exactly two cache entries for one shared input/output schema, return identical bytes, and leave the source unchanged. Also documented the immutable/reused schema-definition contract on RegisterTools. script/lint, script/test (full -race suite), and script/generate-docs passed with no snapshot or generated-document changes.
Separately verified the intended downstream consumer's cached-definition lifecycle and real inventory/server/middleware path. In a scratch integration audit against this PR, 72 request inventories across feature/read-only/UI/client variants produced byte-identical HTTP responses versus baseline; cache entries plateaued after the first sweep, and source schemas and cached bytes remained unchanged. This validates the bounded lifecycle for the intended consumers; it does not claim safety for repeatedly allocating new definition universes.
Auto-replied by the GitHub Copilot app
Document immutable shared schema definitions and verify bounded reuse across HTTP inventories and repeated server registration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Summary
Cache encoded tool schemas once per process and reuse them in
tools/listresponses without changing their wire representation.Why
A warm
tools/listprofile showedjsonschema.Schema.MarshalJSONconsuming about 35% of CPU and 50% of allocations. In the in-repo 124-tool benchmark (6 runs),ListToolsimproved from 35.59 ms/op to 25.39 ms/op (~29% faster), 14.43 MiB/op to 12.48 MiB/op (~14% fewer bytes), and 35.36k to 18.11k allocs/op (~49% fewer allocations). The payload remained exactly 350,133 bytes in both variants.What changed
json.RawMessageencodings, keyed by schema pointer and input/output role.tools/listresult with shallow copies; registration schemas and SDK validators remain untouched.MCP impact
Tool definitions and serialized
tools/listoutput are byte-identical; only repeated internal schema marshaling is avoided.Prompts tested (tool changes only)
Security / limits
Tool renaming
deprecated_tool_aliases.goLint & tests
./script/lint— passed (0 issues)../script/test— equivalent commandgo test -race ./...passed;go test ./...andgo build ./...also passed. Targeted encoded-schema wire-parity tests passed for stdio and HTTP across the full matrix.Docs