Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-ligers-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/server": patch
---

fix(server): track renamed registered item keys
47 changes: 32 additions & 15 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ export class McpServer {
metadata: ResourceMetadata | undefined,
readCallback: ReadResourceCallback
): RegisteredResource {
let currentUri = uri;
const registeredResource: RegisteredResource = {
name,
title,
Expand All @@ -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;
Expand All @@ -663,6 +667,7 @@ export class McpServer {
metadata: ResourceMetadata | undefined,
readCallback: ReadResourceTemplateCallback
): RegisteredResourceTemplate {
let currentName = name;
const registeredResourceTemplate: RegisteredResourceTemplate = {
resourceTemplate: template,
title,
Expand All @@ -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;
Expand Down Expand Up @@ -706,6 +714,7 @@ export class McpServer {
_meta: Record<string, unknown> | undefined
): RegisteredPrompt {
// Track current schema and callback for handler regeneration
let currentName = name;
let currentArgsSchema = argsSchema;
let currentCallback = callback;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -781,6 +794,7 @@ export class McpServer {
validateAndWarnToolName(name);

// Track current handler for executor regeneration
let currentName = name;
let currentHandler = handler;

const registeredTool: RegisteredTool = {
Expand All @@ -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;
Expand Down
68 changes: 67 additions & 1 deletion packages/server/test/server/mcp.compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -127,3 +127,69 @@ describe('InferRawShape', () => {
expectTypeOf<S>().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<string, unknown> })._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<string, unknown> })._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<string, unknown> })._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<string, unknown> })._registeredResourceTemplates;
expect(Object.keys(templates)).toEqual(['third']);

template.remove();
expect(Object.keys(templates)).toEqual([]);
});
});
Loading