Skip to content

feat(mcp): share the claude mcp list inventory cache across sessions - #3

Open
mfbx9da4 wants to merge 1 commit into
mainfrom
david/global-mcplist-cache
Open

feat(mcp): share the claude mcp list inventory cache across sessions#3
mfbx9da4 wants to merge 1 commit into
mainfrom
david/global-mcplist-cache

Conversation

@mfbx9da4

Copy link
Copy Markdown

Why

The claude mcp list inventory that resolves plugin- and claude.ai-connector MCP server transport (URL/command) was cached per session, keyed by session id. So every session paid the slow CLI probe (claude mcp list health-checks every configured server, seconds of wall time) once, on its first MCP tool call the config fast path couldn't attribute. Concurrent sessions didn't share the work, and nothing could warm the cache ahead of the first tool call, so the probe sat on the interactive path.

This is the upstream half of a Gram hooks change that moves the session-start MCP inventory collection off the critical path (DNO-622).

What changed (Before/After)

Before

resolveMCP miss (Claude, server not in config)
  -> claudeMCPListEntries(sessionID)
     cache file keyed by sha256(sessionID)   // per session
     run `claude mcp list` once per session, cache result (even empty)

Each session probes once. No sharing, no warming.

After

resolveMCP miss (Claude)
  -> mcpListEntries()                         // shared global cache
     <stateDir>/agenthooks-mcplist/inventory.json, keyed by server name
     seed by probing once if never populated
  -> on a still-miss: mcpListEntriesOnMiss()  // re-probe, throttled
     re-run at most once per 60s (picks up newly-installed servers), else fail open
  • Global, not per-session. One shared inventory file, so whichever session probed most recently keeps every other session's first MCP tool call instant.
  • Additive merge by server name. A probe that momentarily reports a smaller list (server mid-restart, health-check blip) can't evict another session's entry. Concurrent read-merge-write is serialized by a best-effort O_EXCL lock; the slow CLI runs outside the lock.
  • Throttled re-probe on miss. A tool call for a server absent from the cache re-probes at most once per mcpListRefreshInterval, so an unresolvable name can't stall every event on the CLI timeout. Empty/failed runs are negative-cached.
  • Warming API. New func (r *Runner) RefreshClaudeMCPList() []MCPListEntry and package-level func WarmClaudeMCPList() []MCPListEntry let a detached session-start worker populate the shared cache before the first tool call, removing the probe from the interactive path entirely. Both return the merged inventory so the caller can also relay it.

Behavior change to note

A different session no longer re-probes; it reuses the shared warm cache. An event with no session id now resolves against the shared cache too (previously it never probed, having no key). Both are the intended consequence of sharing. WithoutMCPListFallback / WithoutMCPResolution are unchanged.

Tests

  • Rewrote the once-per-session test to the shared-global semantics (one probe serves multiple sessions and processes).
  • Added: additive merge keeps a server dropped from a later probe; a stale miss re-probes and resolves; WarmClaudeMCPList populates the default cache and a default runner resolves from it without re-probing.

The `claude mcp list` inventory used to resolve plugin- and
claude.ai-connector MCP server transport was cached per session (keyed by
session id), so every session paid the slow CLI probe once on its first
unattributable MCP tool call.

Make the cache global and additive instead:

- One shared on-disk inventory (`<stateDir>/agenthooks-mcplist/inventory.json`),
  keyed by server name, merged additively so a session observing a
  momentarily smaller list can't clobber another session's entry.
- A resolve miss re-probes at most once per interval (stale servers get
  picked up) then fails open; empty/failed runs are negative-cached.
- Export RefreshClaudeMCPList (Runner) and WarmClaudeMCPList (package) so a
  detached session-start worker can warm the cache before the first tool
  call, removing the probe from the interactive path entirely.

Concurrent sessions now share one warm copy: whichever probed most recently
keeps every other session's first MCP tool call instant.
mfbx9da4 added a commit to speakeasy-api/gram that referenced this pull request Jul 23, 2026
…rt collector

Route the detached collector through agenthooks' new shared MCP inventory
cache instead of running its own `claude mcp list`. One CLI run now serves
both purposes: it warms the global cache that per-tool-call URL attribution
reads (so the first plugin/connector MCP tool call never stalls, not just
session start), and returns the full inventory the collector relays for
admin visibility.

- RunMCPInventory calls agenthooks.WarmClaudeMCPList and maps the result to
  the inventory upload, dropping the relay's duplicate collect/parse code.
- The upload is the shared additive inventory, so a later run unions with
  earlier ones rather than replacing them.

Requires agenthooks with the shared/additive mcp-list cache (bumped here to
the branch commit; see speakeasy-api/agenthooks#3).
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.

1 participant