fix(everything): allow re-registration of session resources

When a tool like `gzip-file-as-resource` is called multiple times with the
same output name (especially the default `README.md.gz`), the server would
throw "Resource already registered" because the SDK doesn't allow
registering duplicate URIs.

This fix:
- Tracks registered resources by URI in a module-level Map
- Before registering a new resource, checks if the URI already exists
- If it does, removes the old resource using the SDK's `remove()` method
- Then registers the new resource with fresh content

This allows tools to be called repeatedly with the same parameters without
errors, which is important for LLM agents that may retry tool calls.

Found using Bellwether (https://bellwether.sh), an MCP server validation tool.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Greg King
2026-01-26 09:10:58 -05:00
parent 549dd025e2
commit 3e1be88c3b

View File

@@ -1,6 +1,13 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer, RegisteredResource } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Resource, ResourceLink } from "@modelcontextprotocol/sdk/types.js";
/**
* Tracks registered session resources by URI to allow updating/removing on re-registration.
* This prevents "Resource already registered" errors when a tool creates a resource
* with the same URI multiple times during a session.
*/
const registeredResources = new Map<string, RegisteredResource>();
/**
* Generates a session-scoped resource URI string based on the provided resource name.
*
@@ -47,17 +54,27 @@ export const registerSessionResource = (
blob: payload,
};
// Check if a resource with this URI is already registered and remove it
const existingResource = registeredResources.get(uri);
if (existingResource) {
existingResource.remove();
registeredResources.delete(uri);
}
// Register file resource
server.registerResource(
const registeredResource = server.registerResource(
name,
uri,
{ mimeType, description, title, annotations, icons, _meta },
async (uri) => {
async () => {
return {
contents: [resourceContent],
};
}
);
// Track the registered resource for potential future removal
registeredResources.set(uri, registeredResource);
return { type: "resource_link", ...resource };
};