Repository navigation
Description
Reframed from #1893, which reported it as "replay validates more strictly than the initial call". It doesn't — the two are validated identically. What differs is that one failure is announced and the other is swallowed.
The bug
When a connect-time list fetch (tools/list, resources/list, prompts/list, resources/templates/list) fails client-side validation, the Inspector reports nothing:
ManagedListState's connect handler callsvoid this.refresh()(core/mcp/state/managedListState.ts:113). The rejection is unhandled — it reaches the browser console and nowhere else.- The connection header shows Connected. The Tools/Resources/Prompts panel is simply empty, indistinguishable from a server that legitimately has none.
- The failed exchange renders in the Protocol panel as a successful one —
client → server,101ms, and acompletebadge (that badge is theresultType, not a status). Nothing attributes the failure to the entry.
The same failure on the Replay path is caught at clients/web/src/App.tsx:3759 and shown as a red "Replay failed" toast carrying the full validation detail. Identical error, opposite treatment — which is what made #1893 look like a strictness asymmetry.
This is the Inspector reporting something false about the server under test: a server that returned an invalid result frame is presented as having answered successfully with an empty list.
Reproduction
- Run the modern test server:
node test-servers/build/server-composable.js --config test-servers/configs/modern-http.json - Put a proxy in front of it that deletes
ttlMsandcacheScopefrom any result carrying atoolsarray (any non-conforming modern server will do). - Connect the web client to the proxy with
protocolEra: "modern".
Observed: "Connected", empty Tools tab, Protocol entry showing a clean complete round-trip. The only trace is a console SdkError: Invalid result for tools/list: … ttlMs … cacheScope.
Expected: the failure is visible in the UI without opening devtools.
The CLI already behaves correctly here — --method tools/list against the same proxy returns the validation error as its result.
Suggested fix
- Don't swallow the connect-time rejection.
ManagedListStateshould capture the error into state rather thanvoid-ing the promise, and expose it so the panel can render "couldn't load tools: " instead of an empty list. - Mark the Protocol entry as errored. A response that failed client-side decoding should not render as a successful exchange; surface the validation error on the entry the same way a server-returned JSON-RPC error is surfaced.
- Audit for sibling swallows —
refreshExcludedTools()is wrapped in.catch(() => {})atcore/mcp/inspectorClient.ts:3084and has the same shape.
Notes / out of scope
- The strictness itself is correct. The 2026-07-28 normative schema declares
ttlMs: numberandcacheScope: "public" | "private"as required onListToolsResult(and the prompts/resources/templates equivalents), so the SDK is conformant and nothing should be loosened here. - There is a real prose-vs-schema contradiction in the spec —
server/utilities/cachingsays clients SHOULD default an absentttlMsto0— but resolving that belongs upstream inmodelcontextprotocol/modelcontextprotocol, not here. resources/templates/listbeing rejected even with the fields present (MCP inspector checks on replay results are stricter than the specification (or the specification is incorrect) #1893 item 4) is a separate report.
Activity
fix: mark a rejected standalone response entry as an error too
fix: only attribute a decode rejection to a Protocol entry
fix: read the list error via useSyncExternalStore; clear correlation …
fix: surface list-fetch failures instead of swallowing them