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>
66 lines
2.8 KiB
TypeScript
66 lines
2.8 KiB
TypeScript
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
|
|
* shape (Tool, Prompt, Resource): the optional `title` if provided, else the
|
|
* machine `name`. Centralized so list items, detail panels, and screens stay
|
|
* consistent.
|
|
*/
|
|
export function resolveDisplayLabel(name: string, title?: string): string {
|
|
return title ?? name;
|
|
}
|
|
|
|
/**
|
|
* True when the tool's input schema declares at least one property — used by
|
|
* App-flow callers to decide whether to render a form or auto-launch. Kept in
|
|
* one place so the definition of "has fields" stays consistent if it ever
|
|
* grows to consider `additionalProperties` etc.
|
|
*
|
|
* Root composition counts, since a schema declaring its fields on a root
|
|
* `allOf`/`oneOf`/`anyOf` has none of its own (#2123) — an App tool with such a
|
|
* schema would otherwise launch with empty arguments rather than asking for
|
|
* them. Counted from the composition members directly rather than from a
|
|
* resolved union, so a schema whose composition the form declines to flatten
|
|
* still reports the fields it has: it renders fewer controls, not none, and
|
|
* auto-invoking it would be wrong either way.
|
|
*/
|
|
export function hasInputFields(tool: Tool): boolean {
|
|
return declaresAnyFields(tool.inputSchema);
|
|
}
|
|
|
|
/**
|
|
* A tool's stable per-row identity in the Tools sidebar.
|
|
*
|
|
* A server may return the same tool name more than once, so the name alone
|
|
* identifies neither a React child nor a selection: colliding keys let a
|
|
* filtered-out row survive reconciliation (#1957), and a name-keyed selection
|
|
* highlights both copies at once while the detail panel can only ever resolve
|
|
* the first (#2001). The tool's position in the *unfiltered* list disambiguates
|
|
* duplicates and stays stable while the search narrows, since it is captured
|
|
* before filtering.
|
|
*
|
|
* 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 listRowKey(name, sourceIndex);
|
|
}
|
|
|
|
/**
|
|
* Resolves the tool a {@link toolRowKey} refers to, or `undefined` when the key
|
|
* names no row in the current list (e.g. the list changed under a stale
|
|
* selection). Compares computed keys rather than parsing the key, so the two
|
|
* stay in lockstep if the format ever changes.
|
|
*/
|
|
export function findToolByRowKey(
|
|
tools: Tool[],
|
|
key: string | undefined,
|
|
): Tool | undefined {
|
|
if (key === undefined) return undefined;
|
|
return tools.find((tool, index) => toolRowKey(tool.name, index) === key);
|
|
}
|