feat(client): minimal response-cache substrate (ResponseCacheStore + aggregate-then-write list*())

Merged
FE
4mo ago
+1193-337

Adds a minimal response-cache substrate to Client — the foundation that SEP-2243 header mirroring (#2327) and the future client-side cacheHints honoring both build on.

Motivation and Context

SEP-2243's client algorithm requires the client to know each tool's inputSchema (to find x-mcp-header declarations). #2327's first iteration built a SEP-2243-specific tool-definition cache with its own population/invalidation logic; review rounds repeatedly surfaced lifecycle edges (page-1 wipes, concurrent races, listChanged interactions). This PR moves to the right abstraction: a general response cache that listTools()/listPrompts()/listResources() write to, and _toolDefinition(name) reads as a stamp-memoized derived view. Lifecycle is solved once at the cache layer.

How Has This Been Tested?

Client suite (550), full e2e matrix (2545p/205xf), conformance client:all. New responseCache.test.ts covers store contract, partition keying, listMaxPages cap-throw, generation-guard, and the Object.prototype method-name guard.

Breaking Changes

  • listTools() / listPrompts() / listResources() / listResourceTemplates() called without a cursor now aggregate all pages internally and return the complete result. Callers that paginated manually should drop their loop. Explicit listTools({ cursor }) keeps the per-page contract.
  • A walk that exceeds ClientOptions.listMaxPages (default 64) now throws instead of returning a truncated result.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Public surface added: ResponseCacheStore (interface, MaybePromise<…> returns), InMemoryResponseCacheStore (default), CacheKey, CacheEntry, CacheScope, ClientOptions.responseCacheStore. The interface is shaped for the eventual cacheHints work to be additive: set(key, { value, expiresAt?, scope? }) carries the freshness slot now, even though nothing populates it yet.

Do not share a store across Client instances in v2.0.x — the store is keyed without a server-identity or principal partition, so cross-instance sharing causes server-identity confusion and clear()/evict() cross-talk. Per-principal partitioning that enables safe sharing is the follow-up work.

Lifecycle model: list_changed notifications evict the matching entry (they do not eagerly refetch). A per-method generation counter prevents an in-flight aggregation from caching a stale result after an eviction has fired. _resetConnectionState clears only the per-instance default store, never a user-supplied one.