Skip to content

Commit d1c94a2

Browse files
committed
refactor
1 parent e808827 commit d1c94a2

File tree

6 files changed

+348
-154
lines changed

6 files changed

+348
-154
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,36 @@ All resources, tools, and prompts support an optional `title` field for better U
226226

227227
**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.
228228

229+
#### Title Precedence for Tools
230+
231+
For tools specifically, there are two ways to specify a title:
232+
- `title` field in the tool configuration
233+
- `annotations.title` field (when using the older `tool()` method with annotations)
234+
235+
The precedence order is: `title``annotations.title``name`
236+
237+
```typescript
238+
// Using registerTool (recommended)
239+
server.registerTool("my_tool", {
240+
title: "My Tool", // This title takes precedence
241+
annotations: {
242+
title: "Annotation Title" // This is ignored if title is set
243+
}
244+
}, handler);
245+
246+
// Using tool with annotations (older API)
247+
server.tool("my_tool", "description", {
248+
title: "Annotation Title" // This is used as title
249+
}, handler);
250+
```
229251

230252
When building clients, use the provided utility to get the appropriate display name:
231253

232254
```typescript
233255
import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
234256

235-
// Falls back to 'name' if 'title' is not provided
236-
const displayName = getDisplayName(tool); // Returns title if available, otherwise name
257+
// Automatically handles the precedence: title → annotations.title → name
258+
const displayName = getDisplayName(tool);
237259
```
238260

239261
## Running Your Server

src/examples/client/simpleStreamableHttp.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,12 @@ async function listTools(): Promise<void> {
318318
console.log(' No tools available');
319319
} else {
320320
for (const tool of toolsResult.tools) {
321-
console.log(` - ${getDisplayName(tool)}: ${tool.description}`);
321+
const displayName = getDisplayName(tool);
322+
if (displayName !== tool.name) {
323+
console.log(` - ${tool.name} (${displayName}): ${tool.description}`);
324+
} else {
325+
console.log(` - ${tool.name}: ${tool.description}`);
326+
}
322327
}
323328
}
324329
} catch (error) {

src/server/mcp.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { ResourceTemplate } from "./mcp.js";
1919
import { completable } from "./completable.js";
2020
import { UriTemplate } from "../shared/uriTemplate.js";
21+
import { getDisplayName } from "../shared/metadataUtils.js";
2122

2223
describe("McpServer", () => {
2324
/***
@@ -3598,3 +3599,140 @@ describe("prompt()", () => {
35983599
expect(result.resources[0].mimeType).toBe("text/markdown");
35993600
});
36003601
});
3602+
3603+
describe("Tool title precedence", () => {
3604+
test("should follow correct title precedence: title → annotations.title → name", async () => {
3605+
const mcpServer = new McpServer({
3606+
name: "test server",
3607+
version: "1.0",
3608+
});
3609+
const client = new Client({
3610+
name: "test client",
3611+
version: "1.0",
3612+
});
3613+
3614+
// Tool 1: Only name
3615+
mcpServer.tool(
3616+
"tool_name_only",
3617+
async () => ({
3618+
content: [{ type: "text", text: "Response" }],
3619+
})
3620+
);
3621+
3622+
// Tool 2: Name and annotations.title
3623+
mcpServer.tool(
3624+
"tool_with_annotations_title",
3625+
"Tool with annotations title",
3626+
{
3627+
title: "Annotations Title"
3628+
},
3629+
async () => ({
3630+
content: [{ type: "text", text: "Response" }],
3631+
})
3632+
);
3633+
3634+
// Tool 3: Name and title (using registerTool)
3635+
mcpServer.registerTool(
3636+
"tool_with_title",
3637+
{
3638+
title: "Regular Title",
3639+
description: "Tool with regular title"
3640+
},
3641+
async () => ({
3642+
content: [{ type: "text", text: "Response" }],
3643+
})
3644+
);
3645+
3646+
// Tool 4: All three - title should win
3647+
mcpServer.registerTool(
3648+
"tool_with_all_titles",
3649+
{
3650+
title: "Regular Title Wins",
3651+
description: "Tool with all titles",
3652+
annotations: {
3653+
title: "Annotations Title Should Not Show"
3654+
}
3655+
},
3656+
async () => ({
3657+
content: [{ type: "text", text: "Response" }],
3658+
})
3659+
);
3660+
3661+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
3662+
await Promise.all([
3663+
client.connect(clientTransport),
3664+
mcpServer.connect(serverTransport),
3665+
]);
3666+
3667+
const result = await client.request(
3668+
{ method: "tools/list" },
3669+
ListToolsResultSchema,
3670+
);
3671+
3672+
3673+
expect(result.tools).toHaveLength(4);
3674+
3675+
// Tool 1: Only name - should display name
3676+
const tool1 = result.tools.find(t => t.name === "tool_name_only");
3677+
expect(tool1).toBeDefined();
3678+
expect(getDisplayName(tool1!)).toBe("tool_name_only");
3679+
3680+
// Tool 2: Name and annotations.title - should display annotations.title
3681+
const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title");
3682+
expect(tool2).toBeDefined();
3683+
expect(tool2!.annotations?.title).toBe("Annotations Title");
3684+
expect(getDisplayName(tool2!)).toBe("Annotations Title");
3685+
3686+
// Tool 3: Name and title - should display title
3687+
const tool3 = result.tools.find(t => t.name === "tool_with_title");
3688+
expect(tool3).toBeDefined();
3689+
expect(tool3!.title).toBe("Regular Title");
3690+
expect(getDisplayName(tool3!)).toBe("Regular Title");
3691+
3692+
// Tool 4: All three - title should take precedence
3693+
const tool4 = result.tools.find(t => t.name === "tool_with_all_titles");
3694+
expect(tool4).toBeDefined();
3695+
expect(tool4!.title).toBe("Regular Title Wins");
3696+
expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show");
3697+
expect(getDisplayName(tool4!)).toBe("Regular Title Wins");
3698+
});
3699+
3700+
test("getDisplayName unit tests for title precedence", () => {
3701+
3702+
// Test 1: Only name
3703+
expect(getDisplayName({ name: "tool_name" })).toBe("tool_name");
3704+
3705+
// Test 2: Name and title - title wins
3706+
expect(getDisplayName({
3707+
name: "tool_name",
3708+
title: "Tool Title"
3709+
})).toBe("Tool Title");
3710+
3711+
// Test 3: Name and annotations.title - annotations.title wins
3712+
expect(getDisplayName({
3713+
name: "tool_name",
3714+
annotations: { title: "Annotations Title" }
3715+
})).toBe("Annotations Title");
3716+
3717+
// Test 4: All three - title wins (correct precedence)
3718+
expect(getDisplayName({
3719+
name: "tool_name",
3720+
title: "Regular Title",
3721+
annotations: { title: "Annotations Title" }
3722+
})).toBe("Regular Title");
3723+
3724+
// Test 5: Empty title should not be used
3725+
expect(getDisplayName({
3726+
name: "tool_name",
3727+
title: "",
3728+
annotations: { title: "Annotations Title" }
3729+
})).toBe("Annotations Title");
3730+
3731+
// Test 6: Undefined vs null handling
3732+
expect(getDisplayName({
3733+
name: "tool_name",
3734+
title: undefined,
3735+
annotations: { title: "Annotations Title" }
3736+
})).toBe("Annotations Title");
3737+
});
3738+
});

0 commit comments

Comments
 (0)