Commit 1c48182
authored
fix(mcp): keep tool catalogs in sync with the server's live tool set (#1246)
* fix(mcp): keep tool catalogs in sync with the server's live tool set
A connection's tools were listed once at create time and only re-listed on
an executor-side config edit or a manual Refresh, so server-side catalog
changes (tools added, removed, renamed) silently broke invocations and
never surfaced new tools.
Spec-compliance and staleness fixes:
- Follow nextCursor pagination on tools/list: discovery previously kept
only the first page of a paginated catalog.
- Handle notifications/tools/list_changed received during a call window:
the connection's persisted catalog is marked stale (tools_synced_at
cleared) and re-listed on the next tools read.
- Detect unknown-tool rejections on tools/call, in both wire shapes (the
spec's -32602 protocol error and the reference TS SDK's isError
envelope), anchored to the exact tool name. The call answers a typed
mcp_tool_unknown failure telling the caller to re-list, and the catalog
heals on the next read.
- Re-list remote catalogs on read once older than a freshness TTL
(ExecutorConfig.toolsSyncTtlMs, default 15 minutes, null disables),
covering servers that change tools with no notification. Plugins opt in
via the new remoteToolCatalog flag; MCP sets it.
- Treat failed listings as non-authoritative (ResolveToolsResult
.incomplete): keep the previously persisted catalog instead of wiping
working tools over a transient outage, and stamp the sync time so a
down server isn't re-dialed on every read.
Core additions: ctx.connections.markToolsStale (deferred refresh for
mid-invocation signals) and a bounded stale-scan in toolsList that only
widens with the TTL when a loaded plugin actually declares a remote
catalog.
* test(e2e): prove MCP tool-catalog sync end-to-end
Four cross-target scenarios (cloud + selfhost) driving only public surfaces
(typed API + sandbox executions) against real MCP servers whose catalogs
mutate mid-scenario:
- list_changed: a tool call renames the server's catalog mid-call; the
notification rides the open connection and the very next tools read
re-lists — no refresh click, no failing retry.
- unknown-tool self-heal: the catalog mutates outside any call window (no
notification available); invoking the retired tool fails with the typed
mcp_tool_unknown error telling the agent to re-list, and that failure
alone heals the catalog for the next read.
- pagination: a server paging tools/list via nextCursor registers its whole
catalog, not just the first page.
- outage resilience: a refresh against a dead server keeps the working
catalog instead of wiping it, and converges to the server's new catalog
once it recovers.
The mutable-catalog test fixture now also sends list_changed through the
tool handler's request-scoped notifier: the SDK's RegisteredTool.update
emits it with no relatedRequestId, which the streamable-http transport
routes to the standalone GET SSE stream — a stream a request-scoped client
may never hold open. Stamping the request id routes it onto the in-flight
call's response stream, matching how a real server notifies mid-call.
* test(e2e): film the Tools tab following an MCP server-side rename
A browser scenario (cloud, video + per-step screenshots) walking the whole
user journey: add a live MCP server through the add flow, connect with no
auth, see the v1 tool in the Tools tab, run the tool that renames the
server's catalog mid-call (the server pushes list_changed on the open
connection), then revisit Tools and watch the renamed catalog appear — no
Refresh click anywhere. The old behavior freezes this UI on the stale tool
forever.
The mutable-catalog fixture takes a per-run server name so the derived
integration namespace can't collide on shared-tenant targets.1 parent 5225f85 commit 1c48182
13 files changed
Lines changed: 1330 additions & 63 deletions
File tree
- .changeset
- e2e/scenarios
- packages
- core/sdk/src
- plugins/mcp/src
- sdk
- testing
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
0 commit comments