Skip to content

fix(core): cache MCP resource catalogs per connection - #59375

Open
Messier81 wants to merge 1 commit into
anomalyco:devfrom
Messier81:mcp-resource-cache
Open

Messier81 wants to merge 1 commit into
anomalyco:devfrom
Messier81:mcp-resource-cache

Conversation

@Messier81

@Messier81 Messier81 commented Oct 26, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #52852

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Every resource catalog read sent new resources/list and resources/templates/list requests to every connected server. Clients re-read the catalog on each mcp.resources.changed, and every connect or disconnect emits that event, so reconnecting a few servers multiplied traffic. Rate-limited remote servers answered 429 and ended up stuck as failed.

This caches the catalog per connection, the same way tools and prompts are already cached, as suggested on #52852:

  • Concurrent readers share one load.
  • The cache is cleared on notifications/resources/list_changed and when the connection stops (reconnect and session recovery both go through stopServer).
  • Failed loads are not cached.
  • Only MethodNotFound from template listing means "no templates". If template listing fails any other way, the resources are still returned but not cached.
  • Like tools and prompts, a server that doesn't advertise resources.listChanged keeps its catalog until it reconnects. Before this change, every read picked up changes.

How did you verify your code works?

New tests in packages/core/test/mcp.test.ts:

  • lists MCP resources once per connection until the server reports a change: 9 catalog reads send 1 list request of each kind; a change notification triggers one reload, and so does a reconnect. Before the fix, the 9 reads sent 9 of each.
  • lists MCP resources again after a failed resource list
  • keeps MCP resources when template listing fails, without caching them
  • caches MCP resources from a server that does not implement template listing

Two existing tests changed resources on the server without telling the client. They now send list_changed first.

Ran bun run script/test.ts on all test/mcp*.test.ts files (134 pass), the resource tests with --rerun-each 10, packages/server/test/client-mcp.test.ts, and bun typecheck in packages/core.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #52854, #51645

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks good. I ran the two new tests against the base: both fail there (9 catalog reads sent 9 list requests of each kind), and they pass with this change. The rest of the MCP tests in packages/core, packages/server/test/client-mcp.test.ts and the core type check (typecheck:tsc) also pass. One unrelated test, "terminates MCP descendants after the wrapper exits successfully", fails on the base too.

I checked the concurrency: a load that finishes after list_changed or stopServer only resolves into a handle that later readers no longer use, so it can't bring back an old catalog. Session recovery goes through stopServer, and a reader that stops waiting doesn't cancel the shared load.

Public API: unchanged.

One small point is noted inline.

Related: #52854, #51645

Comment thread packages/core/src/mcp/index.ts Resolved

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit addresses my earlier point: template listing now returns an empty list only on MethodNotFound, and any other template failure fails the load, so it isn't cached and the next read tries again. packages/core/test/mcp.test.ts passes except "terminates MCP descendants after the wrapper exits successfully", which also fails on the base, and the core type check (typecheck:tsc) passes.

Public API: unchanged.

A side effect to be aware of: a server that reports an unimplemented resources/templates/list with some other error code (e.g. -32603) used to show its resources with no templates. Now resourceCatalog() drops that server, and resources() fails for it on every read. If you want to keep those resources visible, you could fall back to an empty template list on other errors without caching that result. That's your call, not a blocker.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits address my last point: if template listing fails with an error other than MethodNotFound, the resources are still returned and that result isn't cached. Servers that don't advertise resources.listChanged are also listed again after 30 seconds, so they can't stay stale until a reconnect.

packages/core/test/mcp.test.ts passes except "terminates MCP descendants after the wrapper exits successfully", which also fails on the base. The resource tests passed on each of 3 runs, the other core MCP test files pass, and the core type check (typecheck:tsc) passes. The 30-second expiry test moves a clock forward instead of sleeping.

Public API: unchanged. resourceListChanged is added only to the internal MCP connection type in packages/core.

No further concerns.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit is sound. The cached catalog now belongs to the connection that loaded it, so a replacement connection never reads an old one's catalog. Failed loads, and loads where template listing failed, are kept for 5 seconds instead of being retried on every read, which stops a rate-limited server from being asked again immediately.

packages/core/test/mcp.test.ts passes except "terminates MCP descendants after the wrapper exits successfully", which also fails on the base. The resource tests passed on each of 3 runs, the other core MCP test files pass, and the core type check (typecheck:tsc) passes.

Public API: unchanged.

No further concerns.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expiry is now counted from when a load finishes, which covers the slow-failure case I mentioned last time. While a load is running the cache stays valid, so readers arriving at the same time still share one request.

packages/core/test/mcp.test.ts passes except "terminates MCP descendants after the wrapper exits successfully", which also fails on the base. The resource tests passed on each of 3 runs, the new slow-load tests use a fake clock instead of sleeping, and the core type check (typecheck:tsc) passes.

Public API: unchanged.

No further concerns.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The force-push removes the time limits from the last two rounds. The cache now matches tools and prompts: it belongs to the connection that filled it, it is cleared on list_changed and when the connection stops, and failed or incomplete loads aren't cached. Template listing still returns an empty list only on MethodNotFound.

packages/core/test/mcp.test.ts passes except "terminates MCP descendants after the wrapper exits successfully", which also fails on the base. The resource tests passed on each of 3 runs, the other core MCP test files pass, and the core type check (typecheck:tsc) passes. The tests no longer depend on timing.

Public API: unchanged.

One tradeoff is left for maintainers to decide, noted inline: servers that don't advertise resources.listChanged now keep their catalog until they reconnect.

Comment thread packages/core/src/mcp/index.ts Resolved

This branch has not been deployed

No deployments
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.

MCP: cache resource catalogs per connection like tools and prompts

1 participant