refactor
This commit is contained in:
parent
e8088276b1
commit
d1c94a2c69
6 changed files with 348 additions and 154 deletions
26
README.md
26
README.md
|
|
@ -226,14 +226,36 @@ All resources, tools, and prompts support an optional `title` field for better U
|
|||
|
||||
**Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility.
|
||||
|
||||
#### Title Precedence for Tools
|
||||
|
||||
For tools specifically, there are two ways to specify a title:
|
||||
- `title` field in the tool configuration
|
||||
- `annotations.title` field (when using the older `tool()` method with annotations)
|
||||
|
||||
The precedence order is: `title` → `annotations.title` → `name`
|
||||
|
||||
```typescript
|
||||
// Using registerTool (recommended)
|
||||
server.registerTool("my_tool", {
|
||||
title: "My Tool", // This title takes precedence
|
||||
annotations: {
|
||||
title: "Annotation Title" // This is ignored if title is set
|
||||
}
|
||||
}, handler);
|
||||
|
||||
// Using tool with annotations (older API)
|
||||
server.tool("my_tool", "description", {
|
||||
title: "Annotation Title" // This is used as title
|
||||
}, handler);
|
||||
```
|
||||
|
||||
When building clients, use the provided utility to get the appropriate display name:
|
||||
|
||||
```typescript
|
||||
import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
|
||||
|
||||
// Falls back to 'name' if 'title' is not provided
|
||||
const displayName = getDisplayName(tool); // Returns title if available, otherwise name
|
||||
// Automatically handles the precedence: title → annotations.title → name
|
||||
const displayName = getDisplayName(tool);
|
||||
```
|
||||
|
||||
## Running Your Server
|
||||
|
|
|
|||
|
|
@ -318,7 +318,12 @@ async function listTools(): Promise<void> {
|
|||
console.log(' No tools available');
|
||||
} else {
|
||||
for (const tool of toolsResult.tools) {
|
||||
console.log(` - ${getDisplayName(tool)}: ${tool.description}`);
|
||||
const displayName = getDisplayName(tool);
|
||||
if (displayName !== tool.name) {
|
||||
console.log(` - ${tool.name} (${displayName}): ${tool.description}`);
|
||||
} else {
|
||||
console.log(` - ${tool.name}: ${tool.description}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { ResourceTemplate } from "./mcp.js";
|
||||
import { completable } from "./completable.js";
|
||||
import { UriTemplate } from "../shared/uriTemplate.js";
|
||||
import { getDisplayName } from "../shared/metadataUtils.js";
|
||||
|
||||
describe("McpServer", () => {
|
||||
/***
|
||||
|
|
@ -3598,3 +3599,140 @@ describe("prompt()", () => {
|
|||
expect(result.resources[0].mimeType).toBe("text/markdown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tool title precedence", () => {
|
||||
test("should follow correct title precedence: title → annotations.title → name", async () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
// Tool 1: Only name
|
||||
mcpServer.tool(
|
||||
"tool_name_only",
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
})
|
||||
);
|
||||
|
||||
// Tool 2: Name and annotations.title
|
||||
mcpServer.tool(
|
||||
"tool_with_annotations_title",
|
||||
"Tool with annotations title",
|
||||
{
|
||||
title: "Annotations Title"
|
||||
},
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
})
|
||||
);
|
||||
|
||||
// Tool 3: Name and title (using registerTool)
|
||||
mcpServer.registerTool(
|
||||
"tool_with_title",
|
||||
{
|
||||
title: "Regular Title",
|
||||
description: "Tool with regular title"
|
||||
},
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
})
|
||||
);
|
||||
|
||||
// Tool 4: All three - title should win
|
||||
mcpServer.registerTool(
|
||||
"tool_with_all_titles",
|
||||
{
|
||||
title: "Regular Title Wins",
|
||||
description: "Tool with all titles",
|
||||
annotations: {
|
||||
title: "Annotations Title Should Not Show"
|
||||
}
|
||||
},
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
})
|
||||
);
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const result = await client.request(
|
||||
{ method: "tools/list" },
|
||||
ListToolsResultSchema,
|
||||
);
|
||||
|
||||
|
||||
expect(result.tools).toHaveLength(4);
|
||||
|
||||
// Tool 1: Only name - should display name
|
||||
const tool1 = result.tools.find(t => t.name === "tool_name_only");
|
||||
expect(tool1).toBeDefined();
|
||||
expect(getDisplayName(tool1!)).toBe("tool_name_only");
|
||||
|
||||
// Tool 2: Name and annotations.title - should display annotations.title
|
||||
const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title");
|
||||
expect(tool2).toBeDefined();
|
||||
expect(tool2!.annotations?.title).toBe("Annotations Title");
|
||||
expect(getDisplayName(tool2!)).toBe("Annotations Title");
|
||||
|
||||
// Tool 3: Name and title - should display title
|
||||
const tool3 = result.tools.find(t => t.name === "tool_with_title");
|
||||
expect(tool3).toBeDefined();
|
||||
expect(tool3!.title).toBe("Regular Title");
|
||||
expect(getDisplayName(tool3!)).toBe("Regular Title");
|
||||
|
||||
// Tool 4: All three - title should take precedence
|
||||
const tool4 = result.tools.find(t => t.name === "tool_with_all_titles");
|
||||
expect(tool4).toBeDefined();
|
||||
expect(tool4!.title).toBe("Regular Title Wins");
|
||||
expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show");
|
||||
expect(getDisplayName(tool4!)).toBe("Regular Title Wins");
|
||||
});
|
||||
|
||||
test("getDisplayName unit tests for title precedence", () => {
|
||||
|
||||
// Test 1: Only name
|
||||
expect(getDisplayName({ name: "tool_name" })).toBe("tool_name");
|
||||
|
||||
// Test 2: Name and title - title wins
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "Tool Title"
|
||||
})).toBe("Tool Title");
|
||||
|
||||
// Test 3: Name and annotations.title - annotations.title wins
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
|
||||
// Test 4: All three - title wins (correct precedence)
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "Regular Title",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Regular Title");
|
||||
|
||||
// Test 5: Empty title should not be used
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
|
||||
// Test 6: Undefined vs null handling
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: undefined,
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
CompleteResult,
|
||||
PromptReference,
|
||||
ResourceTemplateReference,
|
||||
BaseMetadata,
|
||||
Resource,
|
||||
ListResourcesResult,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
|
|
@ -128,7 +129,7 @@ export class McpServer {
|
|||
|
||||
if (tool.outputSchema) {
|
||||
toolDefinition.outputSchema = zodToJsonSchema(
|
||||
tool.outputSchema,
|
||||
tool.outputSchema,
|
||||
{ strictUnions: true }
|
||||
) as Tool["outputSchema"];
|
||||
}
|
||||
|
|
@ -586,27 +587,13 @@ export class McpServer {
|
|||
throw new Error(`Resource ${uriOrTemplate} is already registered`);
|
||||
}
|
||||
|
||||
const registeredResource: RegisteredResource = {
|
||||
const registeredResource = this._createRegisteredResource(
|
||||
name,
|
||||
undefined,
|
||||
uriOrTemplate,
|
||||
metadata,
|
||||
readCallback: readCallback as ReadResourceCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResource.update({ enabled: false }),
|
||||
enable: () => registeredResource.update({ enabled: true }),
|
||||
remove: () => registeredResource.update({ uri: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.uri !== "undefined" && updates.uri !== uriOrTemplate) {
|
||||
delete this._registeredResources[uriOrTemplate]
|
||||
if (updates.uri) this._registeredResources[updates.uri] = registeredResource
|
||||
}
|
||||
if (typeof updates.name !== "undefined") registeredResource.name = updates.name
|
||||
if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResources[uriOrTemplate] = registeredResource;
|
||||
readCallback as ReadResourceCallback
|
||||
);
|
||||
|
||||
this.setResourceRequestHandlers();
|
||||
this.sendResourceListChanged();
|
||||
|
|
@ -616,27 +603,13 @@ export class McpServer {
|
|||
throw new Error(`Resource template ${name} is already registered`);
|
||||
}
|
||||
|
||||
const registeredResourceTemplate: RegisteredResourceTemplate = {
|
||||
resourceTemplate: uriOrTemplate,
|
||||
const registeredResourceTemplate = this._createRegisteredResourceTemplate(
|
||||
name,
|
||||
undefined,
|
||||
uriOrTemplate,
|
||||
metadata,
|
||||
readCallback: readCallback as ReadResourceTemplateCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResourceTemplate.update({ enabled: false }),
|
||||
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
||||
remove: () => registeredResourceTemplate.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredResourceTemplates[name]
|
||||
if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate
|
||||
}
|
||||
if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template
|
||||
if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResourceTemplates[name] = registeredResourceTemplate;
|
||||
readCallback as ReadResourceTemplateCallback
|
||||
);
|
||||
|
||||
this.setResourceRequestHandlers();
|
||||
this.sendResourceListChanged();
|
||||
|
|
@ -651,11 +624,7 @@ export class McpServer {
|
|||
registerResource(
|
||||
name: string,
|
||||
uriOrTemplate: string | ResourceTemplate,
|
||||
config: ResourceMetadata & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
},
|
||||
config: ResourceMetadata,
|
||||
readCallback: ReadResourceCallback | ReadResourceTemplateCallback
|
||||
): RegisteredResource | RegisteredResourceTemplate {
|
||||
if (typeof uriOrTemplate === "string") {
|
||||
|
|
@ -663,27 +632,13 @@ export class McpServer {
|
|||
throw new Error(`Resource ${uriOrTemplate} is already registered`);
|
||||
}
|
||||
|
||||
const registeredResource: RegisteredResource = {
|
||||
const registeredResource = this._createRegisteredResource(
|
||||
name,
|
||||
metadata: config,
|
||||
readCallback: readCallback as ReadResourceCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResource.update({ enabled: false }),
|
||||
enable: () => registeredResource.update({ enabled: true }),
|
||||
remove: () => registeredResource.update({ uri: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.uri !== "undefined" && updates.uri !== uriOrTemplate) {
|
||||
delete this._registeredResources[uriOrTemplate]
|
||||
if (updates.uri) this._registeredResources[updates.uri] = registeredResource
|
||||
}
|
||||
if (typeof updates.name !== "undefined") registeredResource.name = updates.name
|
||||
if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResources[uriOrTemplate] = registeredResource;
|
||||
(config as BaseMetadata).title,
|
||||
uriOrTemplate,
|
||||
config,
|
||||
readCallback as ReadResourceCallback
|
||||
);
|
||||
|
||||
this.setResourceRequestHandlers();
|
||||
this.sendResourceListChanged();
|
||||
|
|
@ -693,27 +648,13 @@ export class McpServer {
|
|||
throw new Error(`Resource template ${name} is already registered`);
|
||||
}
|
||||
|
||||
const registeredResourceTemplate: RegisteredResourceTemplate = {
|
||||
resourceTemplate: uriOrTemplate,
|
||||
metadata: config,
|
||||
readCallback: readCallback as ReadResourceTemplateCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResourceTemplate.update({ enabled: false }),
|
||||
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
||||
remove: () => registeredResourceTemplate.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredResourceTemplates[name]
|
||||
if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate
|
||||
}
|
||||
if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template
|
||||
if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResourceTemplates[name] = registeredResourceTemplate;
|
||||
const registeredResourceTemplate = this._createRegisteredResourceTemplate(
|
||||
name,
|
||||
(config as BaseMetadata).title,
|
||||
uriOrTemplate,
|
||||
config,
|
||||
readCallback as ReadResourceTemplateCallback
|
||||
);
|
||||
|
||||
this.setResourceRequestHandlers();
|
||||
this.sendResourceListChanged();
|
||||
|
|
@ -721,8 +662,108 @@ export class McpServer {
|
|||
}
|
||||
}
|
||||
|
||||
private _createRegisteredResource(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
uri: string,
|
||||
metadata: ResourceMetadata | undefined,
|
||||
readCallback: ReadResourceCallback
|
||||
): RegisteredResource {
|
||||
const registeredResource: RegisteredResource = {
|
||||
name,
|
||||
title,
|
||||
metadata,
|
||||
readCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResource.update({ enabled: false }),
|
||||
enable: () => registeredResource.update({ enabled: true }),
|
||||
remove: () => registeredResource.update({ uri: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.uri !== "undefined" && updates.uri !== uri) {
|
||||
delete this._registeredResources[uri]
|
||||
if (updates.uri) this._registeredResources[updates.uri] = registeredResource
|
||||
}
|
||||
if (typeof updates.name !== "undefined") registeredResource.name = updates.name
|
||||
if (typeof updates.title !== "undefined") registeredResource.title = updates.title
|
||||
if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResources[uri] = registeredResource;
|
||||
return registeredResource;
|
||||
}
|
||||
|
||||
private _createRegisteredResourceTemplate(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
template: ResourceTemplate,
|
||||
metadata: ResourceMetadata | undefined,
|
||||
readCallback: ReadResourceTemplateCallback
|
||||
): RegisteredResourceTemplate {
|
||||
const registeredResourceTemplate: RegisteredResourceTemplate = {
|
||||
resourceTemplate: template,
|
||||
title,
|
||||
metadata,
|
||||
readCallback,
|
||||
enabled: true,
|
||||
disable: () => registeredResourceTemplate.update({ enabled: false }),
|
||||
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
||||
remove: () => registeredResourceTemplate.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredResourceTemplates[name]
|
||||
if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate
|
||||
}
|
||||
if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title
|
||||
if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template
|
||||
if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata
|
||||
if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled
|
||||
this.sendResourceListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredResourceTemplates[name] = registeredResourceTemplate;
|
||||
return registeredResourceTemplate;
|
||||
}
|
||||
|
||||
private _createRegisteredPrompt(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
description: string | undefined,
|
||||
argsSchema: PromptArgsRawShape | undefined,
|
||||
callback: PromptCallback<PromptArgsRawShape | undefined>
|
||||
): RegisteredPrompt {
|
||||
const registeredPrompt: RegisteredPrompt = {
|
||||
title,
|
||||
description,
|
||||
argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema),
|
||||
callback,
|
||||
enabled: true,
|
||||
disable: () => registeredPrompt.update({ enabled: false }),
|
||||
enable: () => registeredPrompt.update({ enabled: true }),
|
||||
remove: () => registeredPrompt.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredPrompts[name]
|
||||
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
|
||||
}
|
||||
if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title
|
||||
if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
|
||||
if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
|
||||
if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
|
||||
this.sendPromptListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredPrompts[name] = registeredPrompt;
|
||||
return registeredPrompt;
|
||||
}
|
||||
|
||||
private _createRegisteredTool(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
description: string | undefined,
|
||||
inputSchema: ZodRawShape | undefined,
|
||||
outputSchema: ZodRawShape | undefined,
|
||||
|
|
@ -730,6 +771,7 @@ export class McpServer {
|
|||
callback: ToolCallback<ZodRawShape | undefined>
|
||||
): RegisteredTool {
|
||||
const registeredTool: RegisteredTool = {
|
||||
title,
|
||||
description,
|
||||
inputSchema:
|
||||
inputSchema === undefined ? undefined : z.object(inputSchema),
|
||||
|
|
@ -868,7 +910,7 @@ export class McpServer {
|
|||
}
|
||||
const callback = rest[0] as ToolCallback<ZodRawShape | undefined>;
|
||||
|
||||
return this._createRegisteredTool(name, description, inputSchema, outputSchema, annotations, callback)
|
||||
return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -891,21 +933,15 @@ export class McpServer {
|
|||
|
||||
const { title, description, inputSchema, outputSchema, annotations } = config;
|
||||
|
||||
const registeredTool = this._createRegisteredTool(
|
||||
return this._createRegisteredTool(
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
annotations,
|
||||
cb as ToolCallback<ZodRawShape | undefined>
|
||||
);
|
||||
|
||||
// Set title if provided
|
||||
if (title !== undefined) {
|
||||
registeredTool.title = title;
|
||||
}
|
||||
|
||||
return registeredTool;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -953,28 +989,13 @@ export class McpServer {
|
|||
}
|
||||
|
||||
const cb = rest[0] as PromptCallback<PromptArgsRawShape | undefined>;
|
||||
const registeredPrompt: RegisteredPrompt = {
|
||||
const registeredPrompt = this._createRegisteredPrompt(
|
||||
name,
|
||||
undefined,
|
||||
description,
|
||||
argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema),
|
||||
callback: cb,
|
||||
enabled: true,
|
||||
disable: () => registeredPrompt.update({ enabled: false }),
|
||||
enable: () => registeredPrompt.update({ enabled: true }),
|
||||
remove: () => registeredPrompt.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredPrompts[name]
|
||||
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
|
||||
}
|
||||
if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title
|
||||
if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
|
||||
if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
|
||||
if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
|
||||
this.sendPromptListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredPrompts[name] = registeredPrompt;
|
||||
argsSchema,
|
||||
cb
|
||||
);
|
||||
|
||||
this.setPromptRequestHandlers();
|
||||
this.sendPromptListChanged()
|
||||
|
|
@ -1000,29 +1021,13 @@ export class McpServer {
|
|||
|
||||
const { title, description, arguments: argsSchema } = config;
|
||||
|
||||
const registeredPrompt: RegisteredPrompt = {
|
||||
const registeredPrompt = this._createRegisteredPrompt(
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema),
|
||||
callback: cb as PromptCallback<PromptArgsRawShape | undefined>,
|
||||
enabled: true,
|
||||
disable: () => registeredPrompt.update({ enabled: false }),
|
||||
enable: () => registeredPrompt.update({ enabled: true }),
|
||||
remove: () => registeredPrompt.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredPrompts[name]
|
||||
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
|
||||
}
|
||||
if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title
|
||||
if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
|
||||
if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
|
||||
if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
|
||||
this.sendPromptListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredPrompts[name] = registeredPrompt;
|
||||
argsSchema,
|
||||
cb as PromptCallback<PromptArgsRawShape | undefined>
|
||||
);
|
||||
|
||||
this.setPromptRequestHandlers();
|
||||
this.sendPromptListChanged()
|
||||
|
|
@ -1155,16 +1160,16 @@ export type RegisteredTool = {
|
|||
enable(): void;
|
||||
disable(): void;
|
||||
update<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
|
||||
updates: {
|
||||
name?: string | null,
|
||||
updates: {
|
||||
name?: string | null,
|
||||
title?: string,
|
||||
description?: string,
|
||||
paramsSchema?: InputArgs,
|
||||
outputSchema?: OutputArgs,
|
||||
annotations?: ToolAnnotations,
|
||||
callback?: ToolCallback<InputArgs>,
|
||||
enabled?: boolean
|
||||
}): void
|
||||
description?: string,
|
||||
paramsSchema?: InputArgs,
|
||||
outputSchema?: OutputArgs,
|
||||
annotations?: ToolAnnotations,
|
||||
callback?: ToolCallback<InputArgs>,
|
||||
enabled?: boolean
|
||||
}): void
|
||||
remove(): void
|
||||
};
|
||||
|
||||
|
|
@ -1212,12 +1217,13 @@ export type ReadResourceCallback = (
|
|||
|
||||
export type RegisteredResource = {
|
||||
name: string;
|
||||
title?: string;
|
||||
metadata?: ResourceMetadata;
|
||||
readCallback: ReadResourceCallback;
|
||||
enabled: boolean;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
update(updates: { name?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void
|
||||
update(updates: { name?: string, title?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void
|
||||
remove(): void
|
||||
};
|
||||
|
||||
|
|
@ -1232,12 +1238,13 @@ export type ReadResourceTemplateCallback = (
|
|||
|
||||
export type RegisteredResourceTemplate = {
|
||||
resourceTemplate: ResourceTemplate;
|
||||
title?: string;
|
||||
metadata?: ResourceMetadata;
|
||||
readCallback: ReadResourceTemplateCallback;
|
||||
enabled: boolean;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
update(updates: { name?: string | null, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
|
||||
update(updates: { name?: string | null, title?: string, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
|
||||
remove(): void
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,26 @@ import { BaseMetadata } from "../types.js";
|
|||
|
||||
/**
|
||||
* Gets the display name for an object with BaseMetadata.
|
||||
* Returns the title if available, otherwise falls back to name.
|
||||
* For tools, the precedence is: title → annotations.title → name
|
||||
* For other objects: title → name
|
||||
* This implements the spec requirement: "if no title is provided, name should be used for display purposes"
|
||||
*/
|
||||
export function getDisplayName(metadata: BaseMetadata): string {
|
||||
return metadata.title ?? metadata.name;
|
||||
// First check for title (not undefined and not empty string)
|
||||
if (metadata.title !== undefined && metadata.title !== '') {
|
||||
return metadata.title;
|
||||
}
|
||||
|
||||
// Then check for annotations.title (only present in Tool objects)
|
||||
if ('annotations' in metadata) {
|
||||
const metadataWithAnnotations = metadata as BaseMetadata & { annotations?: { title?: string } };
|
||||
if (metadataWithAnnotations.annotations?.title) {
|
||||
return metadataWithAnnotations.annotations.title;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally fall back to name
|
||||
return metadata.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -203,7 +203,14 @@ export const BaseMetadataSchema = z
|
|||
.object({
|
||||
/** Intended for programmatic or logical use, but used as a display name in past specs or fallback */
|
||||
name: z.string(),
|
||||
/** Intended for UI and end-user contexts — optimized to be human-readable */
|
||||
/**
|
||||
* Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
|
||||
* even by those unfamiliar with domain-specific terminology.
|
||||
*
|
||||
* If not provided, the name should be used for display (except for Tool,
|
||||
* where `annotations.title` should be given precedence over using `name`,
|
||||
* if present).
|
||||
*/
|
||||
title: z.optional(z.string()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
|
|||
Loading…
Reference in a new issue