From 4d03b243e37a6b1965c8cbe601cd37c94b9b0080 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Sun, 15 Jun 2025 20:42:25 +0100 Subject: [PATCH] add title --- README.md | 138 +++++++++--- src/examples/client/simpleStreamableHttp.ts | 7 +- src/examples/server/simpleStreamableHttp.ts | 29 ++- src/server/mcp.ts | 158 +++++++++++++- src/server/title.test.ts | 194 +++++++++++++++++ src/shared/metadataUtils.ts | 21 ++ src/types.ts | 227 +++++++++----------- 7 files changed, 601 insertions(+), 173 deletions(-) create mode 100644 src/server/title.test.ts create mode 100644 src/shared/metadataUtils.ts diff --git a/README.md b/README.md index c9e27c27..d13378b5 100644 --- a/README.md +++ b/README.md @@ -54,26 +54,35 @@ import { z } from "zod"; // Create an MCP server const server = new McpServer({ - name: "Demo", - version: "1.0.0" + name: "demo-server", + version: "1.0.0", + title: "Demo Server" // Optional display name }); // Add an addition tool -server.tool("add", - { a: z.number(), b: z.number() }, +server.registerTool("add", + { + title: "Addition Tool", + description: "Add two numbers", + inputSchema: { a: z.number(), b: z.number() } + }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }) ); // Add a dynamic greeting resource -server.resource( +server.registerResource( "greeting", new ResourceTemplate("greeting://{name}", { list: undefined }), - async (uri, { name }) => ({ + { + title: "Greeting Resource", // Display name for UI + description: "Dynamic greeting generator" + }, + async (uri, params) => ({ contents: [{ uri: uri.href, - text: `Hello, ${name}!` + text: `Hello, ${params.name}!` }] }) ); @@ -100,8 +109,9 @@ The McpServer is your core interface to the MCP protocol. It handles connection ```typescript const server = new McpServer({ - name: "My App", - version: "1.0.0" + name: "my-app", // Unique identifier for your server + version: "1.0.0", // Server version + title: "My Application" // Optional display name for UI }); ``` @@ -111,9 +121,14 @@ Resources are how you expose data to LLMs. They're similar to GET endpoints in a ```typescript // Static resource -server.resource( +server.registerResource( "config", "config://app", + { + title: "Application Config", + description: "Application configuration data", + mimeType: "text/plain" + }, async (uri) => ({ contents: [{ uri: uri.href, @@ -123,13 +138,17 @@ server.resource( ); // Dynamic resource with parameters -server.resource( +server.registerResource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), - async (uri, { userId }) => ({ + { + title: "User Profile", + description: "User profile information" + }, + async (uri, params) => ({ contents: [{ uri: uri.href, - text: `Profile data for user ${userId}` + text: `Profile data for user ${params.userId}` }] }) ); @@ -141,11 +160,15 @@ Tools let LLMs take actions through your server. Unlike resources, tools are exp ```typescript // Simple tool with parameters -server.tool( +server.registerTool( "calculate-bmi", { - weightKg: z.number(), - heightM: z.number() + title: "BMI Calculator", + description: "Calculate Body Mass Index", + inputSchema: { + weightKg: z.number(), + heightM: z.number() + } }, async ({ weightKg, heightM }) => ({ content: [{ @@ -156,9 +179,13 @@ server.tool( ); // Async tool with external API call -server.tool( +server.registerTool( "fetch-weather", - { city: z.string() }, + { + title: "Weather Fetcher", + description: "Get weather data for a city", + inputSchema: { city: z.string() } + }, async ({ city }) => { const response = await fetch(`https://api.weather.com/${city}`); const data = await response.text(); @@ -174,9 +201,13 @@ server.tool( Prompts are reusable templates that help LLMs interact with your server effectively: ```typescript -server.prompt( +server.registerPrompt( "review-code", - { code: z.string() }, + { + title: "Code Review", + description: "Review code for best practices and potential issues", + arguments: { code: z.string() } + }, ({ code }) => ({ messages: [{ role: "user", @@ -189,6 +220,22 @@ server.prompt( ); ``` +### Display Names and Metadata + +All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name, while `name` remains the unique identifier. + +**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. + + +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 +``` + ## Running Your Server MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: @@ -401,32 +448,45 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mc import { z } from "zod"; const server = new McpServer({ - name: "Echo", - version: "1.0.0" + name: "echo-server", + version: "1.0.0", + title: "Echo Server" }); -server.resource( +server.registerResource( "echo", new ResourceTemplate("echo://{message}", { list: undefined }), - async (uri, { message }) => ({ + { + title: "Echo Resource", + description: "Echoes back messages as resources" + }, + async (uri, params) => ({ contents: [{ uri: uri.href, - text: `Resource echo: ${message}` + text: `Resource echo: ${params.message}` }] }) ); -server.tool( +server.registerTool( "echo", - { message: z.string() }, + { + title: "Echo Tool", + description: "Echoes back the provided message", + inputSchema: { message: z.string() } + }, async ({ message }) => ({ content: [{ type: "text", text: `Tool echo: ${message}` }] }) ); -server.prompt( +server.registerPrompt( "echo", - { message: z.string() }, + { + title: "Echo Prompt", + description: "Creates a prompt to process a message", + arguments: { message: z.string() } + }, ({ message }) => ({ messages: [{ role: "user", @@ -450,8 +510,9 @@ import { promisify } from "util"; import { z } from "zod"; const server = new McpServer({ - name: "SQLite Explorer", - version: "1.0.0" + name: "sqlite-explorer", + version: "1.0.0", + title: "SQLite Explorer" }); // Helper to create DB connection @@ -463,9 +524,14 @@ const getDb = () => { }; }; -server.resource( +server.registerResource( "schema", "schema://main", + { + title: "Database Schema", + description: "SQLite database schema", + mimeType: "text/plain" + }, async (uri) => { const db = getDb(); try { @@ -484,9 +550,13 @@ server.resource( } ); -server.tool( +server.registerTool( "query", - { sql: z.string() }, + { + title: "SQL Query", + description: "Execute SQL queries on the database", + inputSchema: { sql: z.string() } + }, async ({ sql }) => { const db = getDb(); try { diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index 19d32bbc..63d7d60a 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -15,6 +15,7 @@ import { LoggingMessageNotificationSchema, ResourceListChangedNotificationSchema, } from '../../types.js'; +import { getDisplayName } from '../../shared/metadataUtils.js'; // Create readline interface for user input const readline = createInterface({ @@ -317,7 +318,7 @@ async function listTools(): Promise { console.log(' No tools available'); } else { for (const tool of toolsResult.tools) { - console.log(` - ${tool.name}: ${tool.description}`); + console.log(` - ${getDisplayName(tool)}: ${tool.description}`); } } } catch (error) { @@ -429,7 +430,7 @@ async function listPrompts(): Promise { console.log(' No prompts available'); } else { for (const prompt of promptsResult.prompts) { - console.log(` - ${prompt.name}: ${prompt.description}`); + console.log(` - ${getDisplayName(prompt)}: ${prompt.description}`); } } } catch (error) { @@ -480,7 +481,7 @@ async function listResources(): Promise { console.log(' No resources available'); } else { for (const resource of resourcesResult.resources) { - console.log(` - ${resource.name}: ${resource.uri}`); + console.log(` - ${getDisplayName(resource)}: ${resource.uri}`); } } } catch (error) { diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 6c331192..b66be93d 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -18,14 +18,18 @@ const getServer = () => { const server = new McpServer({ name: 'simple-streamable-http-server', version: '1.0.0', + title: 'Simple Streamable HTTP Server', // Display name for UI }, { capabilities: { logging: {} } }); // Register a simple tool that returns a greeting - server.tool( + server.registerTool( 'greet', - 'A simple greeting tool', { - name: z.string().describe('Name to greet'), + title: 'Greeting Tool', // Display name for UI + description: 'A simple greeting tool', + inputSchema: { + name: z.string().describe('Name to greet'), + }, }, async ({ name }): Promise => { return { @@ -84,12 +88,15 @@ const getServer = () => { } ); - // Register a simple prompt - server.prompt( + // Register a simple prompt with title + server.registerPrompt( 'greeting-template', - 'A simple greeting prompt template', { - name: z.string().describe('Name to include in greeting'), + title: 'Greeting Template', // Display name for UI + description: 'A simple greeting prompt template', + arguments: { + name: z.string().describe('Name to include in greeting'), + }, }, async ({ name }): Promise => { return { @@ -148,10 +155,14 @@ const getServer = () => { ); // Create a simple resource at a fixed URI - server.resource( + server.registerResource( 'greeting-resource', 'https://example.com/greetings/default', - { mimeType: 'text/plain' }, + { + title: 'Default Greeting', // Display name for UI + description: 'A simple greeting resource', + mimeType: 'text/plain' + }, async (): Promise => { return { contents: [ diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7fba043f..7c77add1 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -122,6 +122,10 @@ export class McpServer { annotations: tool.annotations, }; + if (tool.title !== undefined) { + toolDefinition.title = tool.title; + } + if (tool.outputSchema) { toolDefinition.outputSchema = zodToJsonSchema( tool.outputSchema, @@ -467,13 +471,19 @@ export class McpServer { ([, prompt]) => prompt.enabled, ).map( ([name, prompt]): Prompt => { - return { + const promptDefinition: Prompt = { name, description: prompt.description, arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined, }; + + if (prompt.title !== undefined) { + promptDefinition.title = prompt.title; + } + + return promptDefinition; }, ), }), @@ -634,6 +644,83 @@ export class McpServer { } } + /** + * Registers a resource with a config object and callback. + * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate. + */ + registerResource( + name: string, + uriOrTemplate: string | ResourceTemplate, + config: ResourceMetadata & { + title?: string; + description?: string; + mimeType?: string; + }, + readCallback: ReadResourceCallback | ReadResourceTemplateCallback + ): RegisteredResource | RegisteredResourceTemplate { + if (typeof uriOrTemplate === "string") { + if (this._registeredResources[uriOrTemplate]) { + throw new Error(`Resource ${uriOrTemplate} is already registered`); + } + + const registeredResource: RegisteredResource = { + 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; + + this.setResourceRequestHandlers(); + this.sendResourceListChanged(); + return registeredResource; + } else { + if (this._registeredResourceTemplates[name]) { + 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; + + this.setResourceRequestHandlers(); + this.sendResourceListChanged(); + return registeredResourceTemplate; + } + } + private _createRegisteredTool( name: string, description: string | undefined, @@ -659,6 +746,7 @@ export class McpServer { delete this._registeredTools[name] if (updates.name) this._registeredTools[updates.name] = registeredTool } + if (typeof updates.title !== "undefined") registeredTool.title = updates.title if (typeof updates.description !== "undefined") registeredTool.description = updates.description if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema) if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback @@ -789,6 +877,7 @@ export class McpServer { registerTool( name: string, config: { + title?: string; description?: string; inputSchema?: InputArgs; outputSchema?: OutputArgs; @@ -800,16 +889,23 @@ export class McpServer { throw new Error(`Tool ${name} is already registered`); } - const { description, inputSchema, outputSchema, annotations } = config; + const { title, description, inputSchema, outputSchema, annotations } = config; - return this._createRegisteredTool( + const registeredTool = this._createRegisteredTool( name, description, inputSchema, outputSchema, annotations, cb as ToolCallback - ) + ); + + // Set title if provided + if (title !== undefined) { + registeredTool.title = title; + } + + return registeredTool; } /** @@ -870,6 +966,7 @@ export class McpServer { 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 @@ -885,6 +982,54 @@ export class McpServer { return registeredPrompt } + /** + * Registers a prompt with a config object and callback. + */ + registerPrompt( + name: string, + config: { + title?: string; + description?: string; + arguments?: Args; + }, + cb: PromptCallback + ): RegisteredPrompt { + if (this._registeredPrompts[name]) { + throw new Error(`Prompt ${name} is already registered`); + } + + const { title, description, arguments: argsSchema } = config; + + const registeredPrompt: RegisteredPrompt = { + title, + description, + argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), + callback: cb as PromptCallback, + 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; + + this.setPromptRequestHandlers(); + this.sendPromptListChanged() + + return registeredPrompt; + } + /** * Checks if the server is connected to a transport. * @returns True if the server is connected @@ -1000,6 +1145,7 @@ export type ToolCallback = : (extra: RequestHandlerExtra) => CallToolResult | Promise; export type RegisteredTool = { + title?: string; description?: string; inputSchema?: AnyZodObject; outputSchema?: AnyZodObject; @@ -1011,6 +1157,7 @@ export type RegisteredTool = { update( updates: { name?: string | null, + title?: string, description?: string, paramsSchema?: InputArgs, outputSchema?: OutputArgs, @@ -1110,13 +1257,14 @@ export type PromptCallback< : (extra: RequestHandlerExtra) => GetPromptResult | Promise; export type RegisteredPrompt = { + title?: string; description?: string; argsSchema?: ZodObject; callback: PromptCallback; enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, description?: string, argsSchema?: Args, callback?: PromptCallback, enabled?: boolean }): void + update(updates: { name?: string | null, title?: string, description?: string, argsSchema?: Args, callback?: PromptCallback, enabled?: boolean }): void remove(): void }; diff --git a/src/server/title.test.ts b/src/server/title.test.ts new file mode 100644 index 00000000..7874fb62 --- /dev/null +++ b/src/server/title.test.ts @@ -0,0 +1,194 @@ +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"; + +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", + { + title: "Test Tool Display Name", + description: "A test tool", + inputSchema: { + 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"); + expect(tools.tools[0].title).toBe("Test Tool Display Name"); + expect(tools.tools[0].description).toBe("A test tool"); + }); + + 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", + "A test tool", + { 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"); + expect(tools.tools[0].title).toBeUndefined(); + expect(tools.tools[0].description).toBe("A test tool"); + }); + + 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", + "A test prompt", + 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"); + expect(prompts.prompts[0].title).toBe("Test Prompt Display Name"); + expect(prompts.prompts[0].description).toBe("A test prompt"); + }); + + 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() } + }, + 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"); + expect(prompts.prompts[0].title).toBe("Test Prompt Display Name"); + expect(prompts.prompts[0].description).toBe("A test prompt"); + expect(prompts.prompts[0].arguments).toHaveLength(1); + }); + + 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", + "https://example.com/test", + { + title: "Test Resource Display Name", + description: "A test resource", + mimeType: "text/plain" + }, + async () => ({ + contents: [{ + uri: "https://example.com/test", + 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"); + expect(resources.resources[0].title).toBe("Test Resource Display Name"); + expect(resources.resources[0].description).toBe("A test resource"); + expect(resources.resources[0].mimeType).toBe("text/plain"); + }); + + 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"); + expect(serverInfo?.title).toBe("Test Server Display Name"); + }); +}); \ No newline at end of file diff --git a/src/shared/metadataUtils.ts b/src/shared/metadataUtils.ts new file mode 100644 index 00000000..d581a6b2 --- /dev/null +++ b/src/shared/metadataUtils.ts @@ -0,0 +1,21 @@ +import { BaseMetadata } from "../types.js"; + +/** + * Utilities for working with BaseMetadata objects. + */ + +/** + * Gets the display name for an object with BaseMetadata. + * Returns the title if available, otherwise falls back to 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; +} + +/** + * Checks if an object has a custom title different from its name. + */ +export function hasCustomTitle(metadata: BaseMetadata): boolean { + return metadata.title !== undefined && metadata.title !== metadata.name; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 822ba8d7..9ece70d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,16 +195,26 @@ export const CancelledNotificationSchema = NotificationSchema.extend({ }), }); +/* Base Metadata */ +/** + * Base metadata interface for common properties across resources, tools, prompts, and implementations. + */ +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 */ + title: z.optional(z.string()), + }) + .passthrough(); + /* Initialization */ /** * Describes the name and version of an MCP implementation. */ -export const ImplementationSchema = z - .object({ - name: z.string(), - version: z.string(), - }) - .passthrough(); +export const ImplementationSchema = BaseMetadataSchema.extend({ + version: z.string(), +}); /** * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. @@ -438,74 +448,56 @@ export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ /** * A known resource that the server is capable of reading. */ -export const ResourceSchema = z - .object({ - /** - * The URI of this resource. - */ - uri: z.string(), +export const ResourceSchema = BaseMetadataSchema.extend({ + /** + * The URI of this resource. + */ + uri: z.string(), - /** - * A human-readable name for this resource. - * - * This can be used by clients to populate UI elements. - */ - name: z.string(), + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description: z.optional(z.string()), - /** - * A description of what this resource represents. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description: z.optional(z.string()), + /** + * The MIME type of this resource, if known. + */ + mimeType: z.optional(z.string()), - /** - * The MIME type of this resource, if known. - */ - mimeType: z.optional(z.string()), - - /** - * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. + */ + _meta: z.optional(z.object({}).passthrough()), +}); /** * A template description for resources available on the server. */ -export const ResourceTemplateSchema = z - .object({ - /** - * A URI template (according to RFC 6570) that can be used to construct resource URIs. - */ - uriTemplate: z.string(), +export const ResourceTemplateSchema = BaseMetadataSchema.extend({ + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + */ + uriTemplate: z.string(), - /** - * A human-readable name for the type of resource this template refers to. - * - * This can be used by clients to populate UI elements. - */ - name: z.string(), + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description: z.optional(z.string()), - /** - * A description of what this template is for. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description: z.optional(z.string()), + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType: z.optional(z.string()), - /** - * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - */ - mimeType: z.optional(z.string()), - - /** - * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. + */ + _meta: z.optional(z.object({}).passthrough()), +}); /** * Sent from the client to request a list of resources the server has. @@ -629,22 +621,16 @@ export const PromptArgumentSchema = z /** * A prompt or prompt template that the server offers. */ -export const PromptSchema = z - .object({ - /** - * The name of the prompt or prompt template. - */ - name: z.string(), - /** - * An optional description of what this prompt provides - */ - description: z.optional(z.string()), - /** - * A list of arguments to use for templating the prompt. - */ - arguments: z.optional(z.array(PromptArgumentSchema)), - }) - .passthrough(); +export const PromptSchema = BaseMetadataSchema.extend({ + /** + * An optional description of what this prompt provides + */ + description: z.optional(z.string()), + /** + * A list of arguments to use for templating the prompt. + */ + arguments: z.optional(z.array(PromptArgumentSchema)), +}); /** * Sent from the client to request a list of prompts and prompt templates the server has. @@ -842,49 +828,43 @@ export const ToolAnnotationsSchema = z /** * Definition for a tool the client can call. */ -export const ToolSchema = z - .object({ - /** - * The name of the tool. - */ - name: z.string(), - /** - * A human-readable description of the tool. - */ - description: z.optional(z.string()), - /** - * A JSON Schema object defining the expected parameters for the tool. - */ - inputSchema: z - .object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough(), - /** - * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. - */ - outputSchema: z.optional( - z.object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough() - ), - /** - * Optional additional tool information. - */ - annotations: z.optional(ToolAnnotationsSchema), +export const ToolSchema = BaseMetadataSchema.extend({ + /** + * A human-readable description of the tool. + */ + description: z.optional(z.string()), + /** + * A JSON Schema object defining the expected parameters for the tool. + */ + inputSchema: z + .object({ + type: z.literal("object"), + properties: z.optional(z.object({}).passthrough()), + required: z.optional(z.array(z.string())), + }) + .passthrough(), + /** + * An optional JSON Schema object defining the structure of the tool's output returned in + * the structuredContent field of a CallToolResult. + */ + outputSchema: z.optional( + z.object({ + type: z.literal("object"), + properties: z.optional(z.object({}).passthrough()), + required: z.optional(z.array(z.string())), + }) + .passthrough() + ), + /** + * Optional additional tool information. + */ + annotations: z.optional(ToolAnnotationsSchema), - /** - * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents. + */ + _meta: z.optional(z.object({}).passthrough()), +}); /** * Sent from the client to request a list of tools the server has. @@ -1347,6 +1327,9 @@ export type EmptyResult = Infer; /* Cancellation */ export type CancelledNotification = Infer; +/* Base Metadata */ +export type BaseMetadata = Infer; + /* Initialization */ export type Implementation = Infer; export type ClientCapabilities = Infer;