fix: key resource list rows by position so duplicate URIs cannot collide

The Resources sidebar keyed every row on its `uri` (and templates on
`uriTemplate`), but nothing in MCP makes those unique — a server may return
the same URI twice, and `ManagedListState.applyItems` replaces the list
wholesale, so a repeat is what the server actually sent. React logged
`Encountered two children with the same key` on every render, and a
filtered-out row survived reconciliation instead of unmounting.

This is the resources counterpart of #1957. The row identity is now the same
`listRowKey(id, sourceIndex)` the Tools sidebar has used since then, extracted
so the two formats cannot drift; `toolRowKey` delegates to it. Applied to all
three Resources lists and to `RootsTable`, whose user-maintained list can
collect a duplicate URI the same way.

Selection stays keyed on the URI, deliberately: unlike a duplicated tool name
(#2001), duplicate rows here denote the same resource — a `resources/read`
takes the URI — so highlighting both is correct. Only the React key needs to
tell them apart.

Adds `ServerConfig.duplicateResourceUris` and the
`duplicate-resource-uris-http.json` showcase config, the `resources/list`
mirror of `duplicateToolNames`, so the defect has a fixture to reproduce it —
appended rather than adjacent, for the reason #1957 documented.

Closes #2206

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018yVgAJz4wpkS4mkGk7DQnp
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-06 19:43:47 -04:00
commit 3153f0305a

View file

@ -1,5 +1,6 @@
import type { Tool } from "@modelcontextprotocol/client";
import { declaresAnyFields } from "@inspector/core/json/rootUnion.js";
import { listRowKey } from "./listRowKey";
/**
* Returns the display label for an MCP entity that follows the BaseMetadata
@ -42,9 +43,12 @@ export function hasInputFields(tool: Tool): boolean {
*
* This is a UI identity only — the wire identity is still `tool.name`, which is
* what a `tools/call` must send.
*
* Shares {@link listRowKey} with the resource lists, which have the same defect
* with duplicate URIs (#2206), so the two formats cannot drift apart.
*/
export function toolRowKey(name: string, sourceIndex: number): string {
return `${sourceIndex}:${name}`;
return listRowKey(name, sourceIndex);
}
/**