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:
parent
42babb7482
commit
3153f0305a
3 changed files with 534 additions and 32 deletions
|
|
@ -514,6 +514,22 @@ export interface ServerConfig {
|
|||
* ignored.
|
||||
*/
|
||||
duplicateToolNames?: string[];
|
||||
/**
|
||||
* URIs of registered resources to emit **twice** in `resources/list` (same
|
||||
* `uri`, the second's title marked "(duplicate)").
|
||||
*
|
||||
* The `resources/list` analogue of {@link duplicateToolNames}, and
|
||||
* unreachable the same way: `registerResource` keys on the URI, so no preset
|
||||
* can produce a repeat. A real server can — two resource sources
|
||||
* concatenated — and the Inspector has to render that faithfully rather than
|
||||
* collide on the React key (#2206).
|
||||
*
|
||||
* The copies go **after** the whole list rather than beside their twin, for
|
||||
* the reason spelled out on `duplicateToolNames`: a head-adjacent pair
|
||||
* happens to survive reconciliation, so only a separated pair exposes the
|
||||
* defect. A URI that isn't registered is ignored.
|
||||
*/
|
||||
duplicateResourceUris?: string[];
|
||||
/**
|
||||
* Replace a registered tool's `inputSchema` / `outputSchema` in `tools/list`
|
||||
* with a **raw** JSON Schema document (#1005).
|
||||
|
|
@ -1410,13 +1426,37 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
});
|
||||
}
|
||||
|
||||
// Resources pagination
|
||||
if (capabilities.resources && maxPageSize.resources !== undefined) {
|
||||
// Emit each named resource a second time, same `uri`, title marked so the two
|
||||
// rows are told apart on screen. See ServerConfig.duplicateResourceUris
|
||||
// (#2206) — and the note there on why the copies are appended.
|
||||
const duplicateResourceUris = new Set(config.duplicateResourceUris ?? []);
|
||||
const withDuplicateResources = (resources: Resource[]): Resource[] =>
|
||||
duplicateResourceUris.size === 0
|
||||
? resources
|
||||
: [
|
||||
...resources,
|
||||
...resources
|
||||
.filter((resource) => duplicateResourceUris.has(resource.uri))
|
||||
.map((resource) => ({
|
||||
...resource,
|
||||
title: `${resource.title ?? resource.name} (duplicate)`,
|
||||
})),
|
||||
];
|
||||
|
||||
// Resources pagination, and the duplicate-URI override, both need the same
|
||||
// hand-built list, so the handler is installed when either is configured.
|
||||
if (
|
||||
capabilities.resources &&
|
||||
(maxPageSize.resources !== undefined || duplicateResourceUris.size > 0)
|
||||
) {
|
||||
mcpServer.server.setRequestHandler(
|
||||
"resources/list",
|
||||
async (request, ctx) => {
|
||||
const cursor = request.params?.cursor;
|
||||
const pageSize = maxPageSize.resources!;
|
||||
// No pagination configured: one page holding everything, so the
|
||||
// duplicate override can share this handler without inventing a page
|
||||
// size — mirroring the `tools/list` handler above.
|
||||
const pageSize = maxPageSize.resources ?? Number.MAX_SAFE_INTEGER;
|
||||
const codec = cursorCodec(pageSize);
|
||||
|
||||
// Collect all resources (static + from templates)
|
||||
|
|
@ -1458,11 +1498,12 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
}
|
||||
}
|
||||
|
||||
const listed = withDuplicateResources(allResources);
|
||||
const startIndex = codec.decode(cursor);
|
||||
const endIndex = startIndex + pageSize;
|
||||
const page = allResources.slice(startIndex, endIndex);
|
||||
const page = listed.slice(startIndex, endIndex);
|
||||
const nextCursor =
|
||||
endIndex < allResources.length ? codec.encode(endIndex) : undefined;
|
||||
endIndex < listed.length ? codec.encode(endIndex) : undefined;
|
||||
|
||||
return {
|
||||
resources: page,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ export interface ConfigFile {
|
|||
* no preset can produce. See {@link ServerConfig.duplicateToolNames} (#1957).
|
||||
*/
|
||||
duplicateToolNames?: string[];
|
||||
/**
|
||||
* URIs of registered resources to emit **twice** in `resources/list` (same
|
||||
* `uri`, the second's title marked "(duplicate)"). See
|
||||
* {@link ServerConfig.duplicateResourceUris} (#2206).
|
||||
*/
|
||||
duplicateResourceUris?: string[];
|
||||
/**
|
||||
* Replace a registered tool's advertised `inputSchema`/`outputSchema` with a
|
||||
* raw JSON Schema document — the constructs a Zod-built preset cannot emit.
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export function resolveConfig(config: ConfigFile): ServerConfig {
|
|||
maxPageSize: config.maxPageSize,
|
||||
emptyStringCursor: config.emptyStringCursor,
|
||||
duplicateToolNames: config.duplicateToolNames,
|
||||
duplicateResourceUris: config.duplicateResourceUris,
|
||||
rawToolSchemas: config.rawToolSchemas,
|
||||
extensionGatedTools: config.extensionGatedTools,
|
||||
serverType: isHttp
|
||||
|
|
|
|||
Loading…
Reference in a new issue