Skip to content

feat(issue-545): cache mcp tool lists - #702

Merged
Weegy merged 8 commits into
mainfrom
feat/issue-545-cache-mcp-tool-lists
Aug 15, 2026
Merged

feat(issue-545): cache mcp tool lists#702
Weegy merged 8 commits into
mainfrom
feat/issue-545-cache-mcp-tool-lists

Conversation

@AndreDerjagin

@AndreDerjagin AndreDerjagin commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

Client-side TTL cache for tools/list in McpManager plus ttlMs/cacheScope
emission from the loopback and public MCP servers (MCP 2026-07-28
CacheableResult). Closes #545.

Why

Fewer discovery round-trips and stable tool blocks for prompt caching. No SDK
bump needed: SDK 1.30.0 parses results leniently, the fields survive (same
pattern as resultType, #544). Servers without ttlMs get a 60s default —
deliberate spec deviation, recorded in ADR-0009 (spec-strict "absent ⇒ 0" would
make the cache a no-op against today's ecosystem; OMADIA_MCP_TOOLLIST_TTL_MS=0
opts back in). Discovery and the security rescan bypass the cache
(fresh: true); close() and notifications/tools/list_changed purge it.

Test plan

  • test/mcpToolListCache.test.ts — 22 tests: TTL expiry, missing/negative/zero
    ttlMs, 15-min clamp, unknown scope ⇒ private, fresh bypass, purge on
    close()/list_changed, token rotation ⇒ cache miss, no cross-auth-context
    sharing, caller mutation cannot poison the cache
  • Emission asserts in test/cliBridge/loopbackMcpServer.test.ts
    (300000/public) and test/publicMcp/publicMcpEndpoint.e2e.test.ts
    (60000/private) — 76/76 across the three suites
  • tsc --noEmit clean (middleware + harness-orchestrator)
  • eslint clean on changed files

Risk / blast radius

  • New env var OMADIA_MCP_TOOLLIST_TTL_MS — documented in .env.example and
    middleware-agent-handoff.md; no compose change needed (rides the existing
    env_file). Package-local pattern (no zod schema): malformed values fall back
    to the default silently.
  • listTools() signature gains an optional opts param; new pure-function
    exports from @omadia/orchestrator. Both additive, no breaking change.
  • Revocation on the public server is unaffected: the TTL only staleness-bounds
    the list, tools/call keeps checking bindings live.
  • Known follow-up (Port the MCP client + server surfaces to @modelcontextprotocol/*@2 — HTTP first (spike done) #562 phase 2): stdio/sse peers never send ttlMs under v2
    era negotiation — they permanently ride the ADR-0009 default, which is the
    designed behavior.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…eResult (#545)

Client (McpManager.listTools): TTL cache keyed like the connection pool —
public scope under the bare server id, private/unknown/absent under the
pool key (server id + token hash). Server TTLs clamped to 15 min; absent
ttlMs gets a 60s default (deliberate spec deviation, ADR-0009;
OMADIA_MCP_TOOLLIST_TTL_MS=0 restores spec-strict). Purged on close()/
closeAll() and immediately on notifications/tools/list_changed; lazy
expiry on read. Discovery and the security rescan bypass via fresh: true.

The bare-id probe only accepts entries filed as public (sharedPublic):
a token-less caller's private list shares that key and must never cross
auth contexts. Cached descriptors are deep-copied on write and read so
caller mutation cannot poison the cache.

Own servers now emit CacheableResult: loopback ttlMs 300000/public
(frozen per-turn list), public server ttlMs 60000/private (per-API-key
filtered list; tools/call keeps checking bindings live).
…light fetch (#545)

The SDK dispatches notifications via Promise.resolve().then(), so a
list_changed arriving in the same stdio chunk as the tools/list response
could purge the cache BEFORE listTools primed it — the invalidation was
lost and the stale-marked list stayed cached for its full TTL. Flaked in
CI where pipe buffering coalesces the frames.
Guard the cache write with a purge-generation counter: snapshot before
the fetch, skip the write when any purge advanced it (purgeToolLists,
closeAll). The stdio fixture now writes response + notification in one
stdout.write, so the existing test exercises the coalesced delivery
deterministically (red without the guard, green with it).
@AndreDerjagin
AndreDerjagin requested a review from Weegy August 14, 2026 08:47
AndreDerjagin and others added 3 commits August 14, 2026 11:12
#700 split the public endpoint's `tools/list` handler in two, one per protocol
era. The `ttlMs`/`cacheScope` this branch added landed on the 2025 leg alone —
which is the one era where the fields are not vocabulary and no client can read
them, while the 2026-07-28 leg (the only one that can) emitted none. Both legs
now carry the same hints, with the same reasoning: `private` is mandatory
because the list is filtered per API key, and the TTL is short because a revoked
binding lingers in a cached list for that long.

`McpListToolsResult` now declares `ttlMs`/`cacheScope` instead of the read site
casting to reach them. The cast made "the server sent nothing" and "this type
forgot the fields" indistinguishable, so a change that really did stop them
arriving would have compiled in silence and the cache would have fallen back to
its default TTL forever.

The comment justifying that read is corrected too. Loose passthrough is why the
fields survive on the v1 legs; on the v2 leg they are modelled 2026-07-28 fields
that arrive verbatim. Measured, because there was reason to doubt it — the v2
`listTools` runs with `cacheMode: 'bypass'`, and bypass turns out to skip only
the SDK's own response cache, not the hints. Not collapsed into "the SDK is
lenient": that is exactly the reasoning which proved false for `resultType` on a
legacy-era v2 decode (#562 phase 2).

ADR-0009 gains the structural half of its own premise: for `stdio` and `sse`
peers the ecosystem never catches up, because `server/discover` is answered at
the HTTP edge only. The default TTL is the permanent path there, not a
transitional one.

Mutation check: dropping the hints from the modern leg turns the new guard red,
and only that one.

Refs #545
@Weegy
Weegy merged commit 015005b into main Aug 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache MCP tool lists via ttlMs + rely on deterministic tool ordering (cost/latency win)

2 participants