Skip to content

Commit cacc3bc

Browse files
committed
Merge branch 'main' into ihrpr/resource-link
2 parents 7e59081 + ddc1a0c commit cacc3bc

File tree

8 files changed

+451
-233
lines changed

8 files changed

+451
-233
lines changed

README.md

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ import { z } from "zod";
5555
// Create an MCP server
5656
const server = new McpServer({
5757
name: "demo-server",
58-
version: "1.0.0",
59-
title: "Demo Server" // Optional display name
58+
version: "1.0.0"
6059
});
6160

6261
// Add an addition tool
@@ -79,10 +78,10 @@ server.registerResource(
7978
title: "Greeting Resource", // Display name for UI
8079
description: "Dynamic greeting generator"
8180
},
82-
async (uri, params) => ({
81+
async (uri, { name }) => ({
8382
contents: [{
8483
uri: uri.href,
85-
text: `Hello, ${params.name}!`
84+
text: `Hello, ${name}!`
8685
}]
8786
})
8887
);
@@ -109,9 +108,8 @@ The McpServer is your core interface to the MCP protocol. It handles connection
109108

110109
```typescript
111110
const server = new McpServer({
112-
name: "my-app", // Unique identifier for your server
113-
version: "1.0.0", // Server version
114-
title: "My Application" // Optional display name for UI
111+
name: "my-app",
112+
version: "1.0.0"
115113
});
116114
```
117115

@@ -145,10 +143,10 @@ server.registerResource(
145143
title: "User Profile",
146144
description: "User profile information"
147145
},
148-
async (uri, params) => ({
146+
async (uri, { userId }) => ({
149147
contents: [{
150148
uri: uri.href,
151-
text: `Profile data for user ${params.userId}`
149+
text: `Profile data for user ${userId}`
152150
}]
153151
})
154152
);
@@ -258,14 +256,36 @@ All resources, tools, and prompts support an optional `title` field for better U
258256

259257
**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.
260258

259+
#### Title Precedence for Tools
260+
261+
For tools specifically, there are two ways to specify a title:
262+
- `title` field in the tool configuration
263+
- `annotations.title` field (when using the older `tool()` method with annotations)
264+
265+
The precedence order is: `title``annotations.title``name`
266+
267+
```typescript
268+
// Using registerTool (recommended)
269+
server.registerTool("my_tool", {
270+
title: "My Tool", // This title takes precedence
271+
annotations: {
272+
title: "Annotation Title" // This is ignored if title is set
273+
}
274+
}, handler);
275+
276+
// Using tool with annotations (older API)
277+
server.tool("my_tool", "description", {
278+
title: "Annotation Title" // This is used as title
279+
}, handler);
280+
```
261281

262282
When building clients, use the provided utility to get the appropriate display name:
263283

264284
```typescript
265285
import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
266286

267-
// Falls back to 'name' if 'title' is not provided
268-
const displayName = getDisplayName(tool); // Returns title if available, otherwise name
287+
// Automatically handles the precedence: title → annotations.title → name
288+
const displayName = getDisplayName(tool);
269289
```
270290

271291
## Running Your Server
@@ -481,8 +501,7 @@ import { z } from "zod";
481501

482502
const server = new McpServer({
483503
name: "echo-server",
484-
version: "1.0.0",
485-
title: "Echo Server"
504+
version: "1.0.0"
486505
});
487506

488507
server.registerResource(
@@ -492,10 +511,10 @@ server.registerResource(
492511
title: "Echo Resource",
493512
description: "Echoes back messages as resources"
494513
},
495-
async (uri, params) => ({
514+
async (uri, { message }) => ({
496515
contents: [{
497516
uri: uri.href,
498-
text: `Resource echo: ${params.message}`
517+
text: `Resource echo: ${message}`
499518
}]
500519
})
501520
);
@@ -517,7 +536,7 @@ server.registerPrompt(
517536
{
518537
title: "Echo Prompt",
519538
description: "Creates a prompt to process a message",
520-
arguments: { message: z.string() }
539+
argsSchema: { message: z.string() }
521540
},
522541
({ message }) => ({
523542
messages: [{
@@ -543,8 +562,7 @@ import { z } from "zod";
543562

544563
const server = new McpServer({
545564
name: "sqlite-explorer",
546-
version: "1.0.0",
547-
title: "SQLite Explorer"
565+
version: "1.0.0"
548566
});
549567

550568
// Helper to create DB connection

src/examples/client/simpleStreamableHttp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ async function listTools(): Promise<void> {
330330
console.log(' No tools available');
331331
} else {
332332
for (const tool of toolsResult.tools) {
333-
console.log(` - ${getDisplayName(tool)}: ${tool.description}`);
333+
console.log(` - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`);
334334
}
335335
}
336336
} catch (error) {
@@ -417,7 +417,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
417417
try {
418418
console.log(`Starting notification stream with resumability: interval=${interval}ms, count=${count || 'unlimited'}`);
419419
console.log(`Using resumption token: ${notificationsToolLastEventId || 'none'}`);
420-
420+
421421
const request: CallToolRequest = {
422422
method: 'tools/call',
423423
params: {
@@ -430,7 +430,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
430430
notificationsToolLastEventId = event;
431431
console.log(`Updated resumption token: ${event}`);
432432
};
433-
433+
434434
const result = await client.request(request, CallToolResultSchema, {
435435
resumptionToken: notificationsToolLastEventId,
436436
onresumptiontoken: onLastEventIdUpdate
@@ -466,7 +466,7 @@ async function listPrompts(): Promise<void> {
466466
console.log(' No prompts available');
467467
} else {
468468
for (const prompt of promptsResult.prompts) {
469-
console.log(` - ${getDisplayName(prompt)}: ${prompt.description}`);
469+
console.log(` - id: ${prompt.name}, name: ${getDisplayName(prompt)}, description: ${prompt.description}`);
470470
}
471471
}
472472
} catch (error) {
@@ -517,7 +517,7 @@ async function listResources(): Promise<void> {
517517
console.log(' No resources available');
518518
} else {
519519
for (const resource of resourcesResult.resources) {
520-
console.log(` - ${getDisplayName(resource)}: ${resource.uri}`);
520+
console.log(` - id: ${resource.name}, name: ${getDisplayName(resource)}, description: ${resource.uri}`);
521521
}
522522
}
523523
} catch (error) {

src/examples/server/simpleStreamableHttp.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const useOAuth = process.argv.includes('--oauth');
1717
const getServer = () => {
1818
const server = new McpServer({
1919
name: 'simple-streamable-http-server',
20-
version: '1.0.0',
21-
title: 'Simple Streamable HTTP Server', // Display name for UI
20+
version: '1.0.0'
2221
}, { capabilities: { logging: {} } });
2322

2423
// Register a simple tool that returns a greeting
@@ -94,7 +93,7 @@ const getServer = () => {
9493
{
9594
title: 'Greeting Template', // Display name for UI
9695
description: 'A simple greeting prompt template',
97-
arguments: {
96+
argsSchema: {
9897
name: z.string().describe('Name to include in greeting'),
9998
},
10099
},
@@ -158,10 +157,10 @@ const getServer = () => {
158157
server.registerResource(
159158
'greeting-resource',
160159
'https://example.com/greetings/default',
161-
{
160+
{
162161
title: 'Default Greeting', // Display name for UI
163162
description: 'A simple greeting resource',
164-
mimeType: 'text/plain'
163+
mimeType: 'text/plain'
165164
},
166165
async (): Promise<ReadResourceResult> => {
167166
return {

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)