Merge branch 'main' into ihrpr/resource-link
This commit is contained in:
commit
cacc3bc09e
8 changed files with 451 additions and 233 deletions
54
README.md
54
README.md
|
|
@ -55,8 +55,7 @@ import { z } from "zod";
|
|||
// Create an MCP server
|
||||
const server = new McpServer({
|
||||
name: "demo-server",
|
||||
version: "1.0.0",
|
||||
title: "Demo Server" // Optional display name
|
||||
version: "1.0.0"
|
||||
});
|
||||
|
||||
// Add an addition tool
|
||||
|
|
@ -79,10 +78,10 @@ server.registerResource(
|
|||
title: "Greeting Resource", // Display name for UI
|
||||
description: "Dynamic greeting generator"
|
||||
},
|
||||
async (uri, params) => ({
|
||||
async (uri, { name }) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `Hello, ${params.name}!`
|
||||
text: `Hello, ${name}!`
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
|
@ -109,9 +108,8 @@ The McpServer is your core interface to the MCP protocol. It handles connection
|
|||
|
||||
```typescript
|
||||
const server = new McpServer({
|
||||
name: "my-app", // Unique identifier for your server
|
||||
version: "1.0.0", // Server version
|
||||
title: "My Application" // Optional display name for UI
|
||||
name: "my-app",
|
||||
version: "1.0.0"
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -145,10 +143,10 @@ server.registerResource(
|
|||
title: "User Profile",
|
||||
description: "User profile information"
|
||||
},
|
||||
async (uri, params) => ({
|
||||
async (uri, { userId }) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `Profile data for user ${params.userId}`
|
||||
text: `Profile data for user ${userId}`
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
|
@ -258,14 +256,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
|
||||
|
|
@ -481,8 +501,7 @@ import { z } from "zod";
|
|||
|
||||
const server = new McpServer({
|
||||
name: "echo-server",
|
||||
version: "1.0.0",
|
||||
title: "Echo Server"
|
||||
version: "1.0.0"
|
||||
});
|
||||
|
||||
server.registerResource(
|
||||
|
|
@ -492,10 +511,10 @@ server.registerResource(
|
|||
title: "Echo Resource",
|
||||
description: "Echoes back messages as resources"
|
||||
},
|
||||
async (uri, params) => ({
|
||||
async (uri, { message }) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `Resource echo: ${params.message}`
|
||||
text: `Resource echo: ${message}`
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
|
@ -517,7 +536,7 @@ server.registerPrompt(
|
|||
{
|
||||
title: "Echo Prompt",
|
||||
description: "Creates a prompt to process a message",
|
||||
arguments: { message: z.string() }
|
||||
argsSchema: { message: z.string() }
|
||||
},
|
||||
({ message }) => ({
|
||||
messages: [{
|
||||
|
|
@ -543,8 +562,7 @@ import { z } from "zod";
|
|||
|
||||
const server = new McpServer({
|
||||
name: "sqlite-explorer",
|
||||
version: "1.0.0",
|
||||
title: "SQLite Explorer"
|
||||
version: "1.0.0"
|
||||
});
|
||||
|
||||
// Helper to create DB connection
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ async function listTools(): Promise<void> {
|
|||
console.log(' No tools available');
|
||||
} else {
|
||||
for (const tool of toolsResult.tools) {
|
||||
console.log(` - ${getDisplayName(tool)}: ${tool.description}`);
|
||||
console.log(` - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -417,7 +417,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
|
|||
try {
|
||||
console.log(`Starting notification stream with resumability: interval=${interval}ms, count=${count || 'unlimited'}`);
|
||||
console.log(`Using resumption token: ${notificationsToolLastEventId || 'none'}`);
|
||||
|
||||
|
||||
const request: CallToolRequest = {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
|
|
@ -430,7 +430,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
|
|||
notificationsToolLastEventId = event;
|
||||
console.log(`Updated resumption token: ${event}`);
|
||||
};
|
||||
|
||||
|
||||
const result = await client.request(request, CallToolResultSchema, {
|
||||
resumptionToken: notificationsToolLastEventId,
|
||||
onresumptiontoken: onLastEventIdUpdate
|
||||
|
|
@ -466,7 +466,7 @@ async function listPrompts(): Promise<void> {
|
|||
console.log(' No prompts available');
|
||||
} else {
|
||||
for (const prompt of promptsResult.prompts) {
|
||||
console.log(` - ${getDisplayName(prompt)}: ${prompt.description}`);
|
||||
console.log(` - id: ${prompt.name}, name: ${getDisplayName(prompt)}, description: ${prompt.description}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -517,7 +517,7 @@ async function listResources(): Promise<void> {
|
|||
console.log(' No resources available');
|
||||
} else {
|
||||
for (const resource of resourcesResult.resources) {
|
||||
console.log(` - ${getDisplayName(resource)}: ${resource.uri}`);
|
||||
console.log(` - id: ${resource.name}, name: ${getDisplayName(resource)}, description: ${resource.uri}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ const useOAuth = process.argv.includes('--oauth');
|
|||
const getServer = () => {
|
||||
const server = new McpServer({
|
||||
name: 'simple-streamable-http-server',
|
||||
version: '1.0.0',
|
||||
title: 'Simple Streamable HTTP Server', // Display name for UI
|
||||
version: '1.0.0'
|
||||
}, { capabilities: { logging: {} } });
|
||||
|
||||
// Register a simple tool that returns a greeting
|
||||
|
|
@ -94,7 +93,7 @@ const getServer = () => {
|
|||
{
|
||||
title: 'Greeting Template', // Display name for UI
|
||||
description: 'A simple greeting prompt template',
|
||||
arguments: {
|
||||
argsSchema: {
|
||||
name: z.string().describe('Name to include in greeting'),
|
||||
},
|
||||
},
|
||||
|
|
@ -158,10 +157,10 @@ const getServer = () => {
|
|||
server.registerResource(
|
||||
'greeting-resource',
|
||||
'https://example.com/greetings/default',
|
||||
{
|
||||
{
|
||||
title: 'Default Greeting', // Display name for UI
|
||||
description: 'A simple greeting resource',
|
||||
mimeType: 'text/plain'
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -113,6 +114,7 @@ export class McpServer {
|
|||
([name, tool]): Tool => {
|
||||
const toolDefinition: Tool = {
|
||||
name,
|
||||
title: tool.title,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema
|
||||
? (zodToJsonSchema(tool.inputSchema, {
|
||||
|
|
@ -122,13 +124,9 @@ export class McpServer {
|
|||
annotations: tool.annotations,
|
||||
};
|
||||
|
||||
if (tool.title !== undefined) {
|
||||
toolDefinition.title = tool.title;
|
||||
}
|
||||
|
||||
if (tool.outputSchema) {
|
||||
toolDefinition.outputSchema = zodToJsonSchema(
|
||||
tool.outputSchema,
|
||||
tool.outputSchema,
|
||||
{ strictUnions: true }
|
||||
) as Tool["outputSchema"];
|
||||
}
|
||||
|
|
@ -471,19 +469,14 @@ export class McpServer {
|
|||
([, prompt]) => prompt.enabled,
|
||||
).map(
|
||||
([name, prompt]): Prompt => {
|
||||
const promptDefinition: Prompt = {
|
||||
return {
|
||||
name,
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
arguments: prompt.argsSchema
|
||||
? promptArgumentsFromSchema(prompt.argsSchema)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (prompt.title !== undefined) {
|
||||
promptDefinition.title = prompt.title;
|
||||
}
|
||||
|
||||
return promptDefinition;
|
||||
},
|
||||
),
|
||||
}),
|
||||
|
|
@ -586,27 +579,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 +595,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 +616,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 +624,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 +640,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 +654,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 +763,7 @@ export class McpServer {
|
|||
callback: ToolCallback<ZodRawShape | undefined>
|
||||
): RegisteredTool {
|
||||
const registeredTool: RegisteredTool = {
|
||||
title,
|
||||
description,
|
||||
inputSchema:
|
||||
inputSchema === undefined ? undefined : z.object(inputSchema),
|
||||
|
|
@ -868,7 +902,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 +925,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 +981,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()
|
||||
|
|
@ -990,7 +1003,7 @@ export class McpServer {
|
|||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
arguments?: Args;
|
||||
argsSchema?: Args;
|
||||
},
|
||||
cb: PromptCallback<Args>
|
||||
): RegisteredPrompt {
|
||||
|
|
@ -998,31 +1011,15 @@ export class McpServer {
|
|||
throw new Error(`Prompt ${name} is already registered`);
|
||||
}
|
||||
|
||||
const { title, description, arguments: argsSchema } = config;
|
||||
const { title, description, 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 +1152,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 +1209,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 +1230,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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { Server } from "./index.js";
|
|||
import { Client } from "../client/index.js";
|
||||
import { InMemoryTransport } from "../inMemory.js";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from "./mcp.js";
|
||||
import { McpServer, ResourceTemplate } from "./mcp.js";
|
||||
|
||||
describe("Title field backwards compatibility", () => {
|
||||
it("should work with tools that have title", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
// Register tool with title
|
||||
server.registerTool(
|
||||
"test-tool",
|
||||
|
|
@ -25,12 +25,12 @@ describe("Title field backwards compatibility", () => {
|
|||
},
|
||||
async () => ({ content: [{ type: "text", text: "result" }] })
|
||||
);
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const tools = await client.listTools();
|
||||
expect(tools.tools).toHaveLength(1);
|
||||
expect(tools.tools[0].name).toBe("test-tool");
|
||||
|
|
@ -40,12 +40,12 @@ describe("Title field backwards compatibility", () => {
|
|||
|
||||
it("should work with tools without title", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
// Register tool without title
|
||||
server.tool(
|
||||
"test-tool",
|
||||
|
|
@ -53,12 +53,12 @@ describe("Title field backwards compatibility", () => {
|
|||
{ value: z.string() },
|
||||
async () => ({ content: [{ type: "text", text: "result" }] })
|
||||
);
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const tools = await client.listTools();
|
||||
expect(tools.tools).toHaveLength(1);
|
||||
expect(tools.tools[0].name).toBe("test-tool");
|
||||
|
|
@ -68,12 +68,12 @@ describe("Title field backwards compatibility", () => {
|
|||
|
||||
it("should work with prompts that have title using update", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
// Register prompt with title by updating after creation
|
||||
const prompt = server.prompt(
|
||||
"test-prompt",
|
||||
|
|
@ -81,12 +81,12 @@ describe("Title field backwards compatibility", () => {
|
|||
async () => ({ messages: [{ role: "user", content: { type: "text", text: "test" } }] })
|
||||
);
|
||||
prompt.update({ title: "Test Prompt Display Name" });
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const prompts = await client.listPrompts();
|
||||
expect(prompts.prompts).toHaveLength(1);
|
||||
expect(prompts.prompts[0].name).toBe("test-prompt");
|
||||
|
|
@ -96,33 +96,33 @@ describe("Title field backwards compatibility", () => {
|
|||
|
||||
it("should work with prompts using registerPrompt", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
// Register prompt with title using registerPrompt
|
||||
server.registerPrompt(
|
||||
"test-prompt",
|
||||
{
|
||||
title: "Test Prompt Display Name",
|
||||
description: "A test prompt",
|
||||
arguments: { input: z.string() }
|
||||
argsSchema: { input: z.string() }
|
||||
},
|
||||
async ({ input }) => ({
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: { type: "text", text: `test: ${input}` }
|
||||
}]
|
||||
async ({ input }) => ({
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: { type: "text", text: `test: ${input}` }
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const prompts = await client.listPrompts();
|
||||
expect(prompts.prompts).toHaveLength(1);
|
||||
expect(prompts.prompts[0].name).toBe("test-prompt");
|
||||
|
|
@ -133,12 +133,12 @@ describe("Title field backwards compatibility", () => {
|
|||
|
||||
it("should work with resources using registerResource", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
// Register resource with title using registerResource
|
||||
server.registerResource(
|
||||
"test-resource",
|
||||
|
|
@ -148,19 +148,19 @@ describe("Title field backwards compatibility", () => {
|
|||
description: "A test resource",
|
||||
mimeType: "text/plain"
|
||||
},
|
||||
async () => ({
|
||||
contents: [{
|
||||
async () => ({
|
||||
contents: [{
|
||||
uri: "https://example.com/test",
|
||||
text: "test content"
|
||||
}]
|
||||
text: "test content"
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const resources = await client.listResources();
|
||||
expect(resources.resources).toHaveLength(1);
|
||||
expect(resources.resources[0].name).toBe("test-resource");
|
||||
|
|
@ -169,23 +169,65 @@ describe("Title field backwards compatibility", () => {
|
|||
expect(resources.resources[0].mimeType).toBe("text/plain");
|
||||
});
|
||||
|
||||
it("should work with dynamic resources using registerResource", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: "test-server", version: "1.0.0" },
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
// Register dynamic resource with title using registerResource
|
||||
server.registerResource(
|
||||
"user-profile",
|
||||
new ResourceTemplate("users://{userId}/profile", { list: undefined }),
|
||||
{
|
||||
title: "User Profile",
|
||||
description: "User profile information"
|
||||
},
|
||||
async (uri, { userId }, _extra) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `Profile data for user ${userId}`
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
await server.server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const resourceTemplates = await client.listResourceTemplates();
|
||||
expect(resourceTemplates.resourceTemplates).toHaveLength(1);
|
||||
expect(resourceTemplates.resourceTemplates[0].name).toBe("user-profile");
|
||||
expect(resourceTemplates.resourceTemplates[0].title).toBe("User Profile");
|
||||
expect(resourceTemplates.resourceTemplates[0].description).toBe("User profile information");
|
||||
expect(resourceTemplates.resourceTemplates[0].uriTemplate).toBe("users://{userId}/profile");
|
||||
|
||||
// Test reading the resource
|
||||
const readResult = await client.readResource({ uri: "users://123/profile" });
|
||||
expect(readResult.contents).toHaveLength(1);
|
||||
expect(readResult.contents[0].text).toBe("Profile data for user 123");
|
||||
});
|
||||
|
||||
it("should support serverInfo with title", async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
{
|
||||
name: "test-server",
|
||||
version: "1.0.0",
|
||||
title: "Test Server Display Name"
|
||||
},
|
||||
{ capabilities: {} }
|
||||
);
|
||||
|
||||
|
||||
const client = new Client({ name: "test-client", version: "1.0.0" });
|
||||
|
||||
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
|
||||
const serverInfo = client.getServerVersion();
|
||||
expect(serverInfo?.name).toBe("test-server");
|
||||
expect(serverInfo?.version).toBe("1.0.0");
|
||||
|
|
|
|||
|
|
@ -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