diff --git a/.changeset/cuddly-ligers-complain.md b/.changeset/cuddly-ligers-complain.md new file mode 100644 index 0000000000..01f742146b --- /dev/null +++ b/.changeset/cuddly-ligers-complain.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/server": patch +--- + +fix(server): track renamed registered item keys diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index fb45fd5db6..77f0bc30e0 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -630,6 +630,7 @@ export class McpServer { metadata: ResourceMetadata | undefined, readCallback: ReadResourceCallback ): RegisteredResource { + let currentUri = uri; const registeredResource: RegisteredResource = { name, title, @@ -640,9 +641,12 @@ export class McpServer { enable: () => registeredResource.update({ enabled: true }), remove: () => registeredResource.update({ uri: null }), update: updates => { - if (updates.uri !== undefined && updates.uri !== uri) { - delete this._registeredResources[uri]; - if (updates.uri) this._registeredResources[updates.uri] = registeredResource; + if (updates.uri !== undefined && updates.uri !== currentUri) { + delete this._registeredResources[currentUri]; + if (updates.uri) { + this._registeredResources[updates.uri] = registeredResource; + currentUri = updates.uri; + } } if (updates.name !== undefined) registeredResource.name = updates.name; if (updates.title !== undefined) registeredResource.title = updates.title; @@ -663,6 +667,7 @@ export class McpServer { metadata: ResourceMetadata | undefined, readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate { + let currentName = name; const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, title, @@ -673,9 +678,12 @@ export class McpServer { enable: () => registeredResourceTemplate.update({ enabled: true }), remove: () => registeredResourceTemplate.update({ name: null }), update: updates => { - if (updates.name !== undefined && updates.name !== name) { - delete this._registeredResourceTemplates[name]; - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; + if (updates.name !== undefined && updates.name !== currentName) { + delete this._registeredResourceTemplates[currentName]; + if (updates.name) { + this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; + currentName = updates.name; + } } if (updates.title !== undefined) registeredResourceTemplate.title = updates.title; if (updates.template !== undefined) registeredResourceTemplate.resourceTemplate = updates.template; @@ -706,6 +714,7 @@ export class McpServer { _meta: Record | undefined ): RegisteredPrompt { // Track current schema and callback for handler regeneration + let currentName = name; let currentArgsSchema = argsSchema; let currentCallback = callback; @@ -720,16 +729,20 @@ export class McpServer { enable: () => registeredPrompt.update({ enabled: true }), remove: () => registeredPrompt.update({ name: null }), update: updates => { - if (updates.name !== undefined && updates.name !== name) { - delete this._registeredPrompts[name]; - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt; + // Track if we need to regenerate the handler + let needsHandlerRegen = false; + if (updates.name !== undefined && updates.name !== currentName) { + delete this._registeredPrompts[currentName]; + if (updates.name) { + this._registeredPrompts[updates.name] = registeredPrompt; + currentName = updates.name; + } + needsHandlerRegen = true; } if (updates.title !== undefined) registeredPrompt.title = updates.title; if (updates.description !== undefined) registeredPrompt.description = updates.description; if (updates._meta !== undefined) registeredPrompt._meta = updates._meta; - // Track if we need to regenerate the handler - let needsHandlerRegen = false; if (updates.argsSchema !== undefined) { registeredPrompt.argsSchema = updates.argsSchema; currentArgsSchema = updates.argsSchema; @@ -740,7 +753,7 @@ export class McpServer { needsHandlerRegen = true; } if (needsHandlerRegen) { - registeredPrompt.handler = createPromptHandler(name, currentArgsSchema, currentCallback); + registeredPrompt.handler = createPromptHandler(currentName, currentArgsSchema, currentCallback); } if (updates.enabled !== undefined) registeredPrompt.enabled = updates.enabled; @@ -781,6 +794,7 @@ export class McpServer { validateAndWarnToolName(name); // Track current handler for executor regeneration + let currentName = name; let currentHandler = handler; const registeredTool: RegisteredTool = { @@ -798,12 +812,15 @@ export class McpServer { enable: () => registeredTool.update({ enabled: true }), remove: () => registeredTool.update({ name: null }), update: updates => { - if (updates.name !== undefined && updates.name !== name) { + if (updates.name !== undefined && updates.name !== currentName) { if (typeof updates.name === 'string') { validateAndWarnToolName(updates.name); } - delete this._registeredTools[name]; - if (updates.name) this._registeredTools[updates.name] = registeredTool; + delete this._registeredTools[currentName]; + if (updates.name) { + this._registeredTools[updates.name] = registeredTool; + currentName = updates.name; + } } if (updates.title !== undefined) registeredTool.title = updates.title; if (updates.description !== undefined) registeredTool.description = updates.description; diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 322b615353..dbdd4fd32e 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -2,7 +2,7 @@ import type { JSONRPCMessage } from '@modelcontextprotocol/core'; import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import * as z from 'zod/v4'; -import { McpServer } from '../../src/index.js'; +import { McpServer, ResourceTemplate } from '../../src/index.js'; import type { InferRawShape } from '../../src/server/mcp.js'; import { completable } from '../../src/server/completable.js'; @@ -127,3 +127,69 @@ describe('InferRawShape', () => { expectTypeOf().toEqualTypeOf<{ a: string; b?: string | undefined }>(); }); }); + +describe('registered item key updates', () => { + it('tracks the current tool name across repeated renames and removal', () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + const tool = server.registerTool('first', {}, async () => ({ + content: [{ type: 'text' as const, text: 'ok' }] + })); + + tool.update({ name: 'second' }); + tool.update({ name: 'third' }); + + const tools = (server as unknown as { _registeredTools: Record })._registeredTools; + expect(Object.keys(tools)).toEqual(['third']); + + tool.remove(); + expect(Object.keys(tools)).toEqual([]); + }); + + it('tracks the current prompt name across repeated renames and removal', () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + const prompt = server.registerPrompt('first', {}, async () => ({ + messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'ok' } }] + })); + + prompt.update({ name: 'second' }); + prompt.update({ name: 'third' }); + + const prompts = (server as unknown as { _registeredPrompts: Record })._registeredPrompts; + expect(Object.keys(prompts)).toEqual(['third']); + + prompt.remove(); + expect(Object.keys(prompts)).toEqual([]); + }); + + it('tracks the current static resource URI across repeated URI updates and removal', () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + const resource = server.registerResource('resource', 'file:///first', {}, async uri => ({ + contents: [{ uri: uri.href, text: 'ok' }] + })); + + resource.update({ uri: 'file:///second' }); + resource.update({ uri: 'file:///third' }); + + const resources = (server as unknown as { _registeredResources: Record })._registeredResources; + expect(Object.keys(resources)).toEqual(['file:///third']); + + resource.remove(); + expect(Object.keys(resources)).toEqual([]); + }); + + it('tracks the current resource template name across repeated renames and removal', () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + const template = server.registerResource('first', new ResourceTemplate('file:///{id}', { list: undefined }), {}, async uri => ({ + contents: [{ uri: uri.href, text: 'ok' }] + })); + + template.update({ name: 'second' }); + template.update({ name: 'third' }); + + const templates = (server as unknown as { _registeredResourceTemplates: Record })._registeredResourceTemplates; + expect(Object.keys(templates)).toEqual(['third']); + + template.remove(); + expect(Object.keys(templates)).toEqual([]); + }); +});