From 4d03b243e37a6b1965c8cbe601cd37c94b9b0080 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Sun, 15 Jun 2025 20:42:25 +0100 Subject: [PATCH 1/8] 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 | 223 +++++++++----------- 7 files changed, 599 insertions(+), 171 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 c9e27c275..d13378b53 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 19d32bbcf..63d7d60a9 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 6c3311920..b66be93d0 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 7fba043f0..7c77add13 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 000000000..7874fb62c --- /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 000000000..d581a6b24 --- /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 822ba8d77..9ece70d46 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,17 +195,27 @@ export const CancelledNotificationSchema = NotificationSchema.extend({ }), }); -/* Initialization */ +/* Base Metadata */ /** - * Describes the name and version of an MCP implementation. + * Base metadata interface for common properties across resources, tools, prompts, and implementations. */ -export const ImplementationSchema = z +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(), - version: 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 = 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(), - - /** - * A human-readable name for this resource. - * - * This can be used by clients to populate UI elements. - */ - name: z.string(), +export const ResourceSchema = BaseMetadataSchema.extend({ + /** + * The URI of this resource. + */ + uri: 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(), - - /** - * 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(), +export const ResourceTemplateSchema = BaseMetadataSchema.extend({ + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + */ + uriTemplate: 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; From d1c94a2c696c131f65419f2e3d68efe4a18a1d27 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 10:43:31 +0100 Subject: [PATCH 2/8] refactor --- README.md | 26 +- src/examples/client/simpleStreamableHttp.ts | 7 +- src/server/mcp.test.ts | 138 +++++++++ src/server/mcp.ts | 303 ++++++++++---------- src/shared/metadataUtils.ts | 19 +- src/types.ts | 9 +- 6 files changed, 348 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index d13378b53..f9f080b92 100644 --- a/README.md +++ b/README.md @@ -226,14 +226,36 @@ All resources, tools, and prompts support an optional `title` field for better U **Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility. +#### Title Precedence for Tools + +For tools specifically, there are two ways to specify a title: +- `title` field in the tool configuration +- `annotations.title` field (when using the older `tool()` method with annotations) + +The precedence order is: `title` → `annotations.title` → `name` + +```typescript +// Using registerTool (recommended) +server.registerTool("my_tool", { + title: "My Tool", // This title takes precedence + annotations: { + title: "Annotation Title" // This is ignored if title is set + } +}, handler); + +// Using tool with annotations (older API) +server.tool("my_tool", "description", { + title: "Annotation Title" // This is used as title +}, handler); +``` When building clients, use the provided utility to get the appropriate display name: ```typescript import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js"; -// Falls back to 'name' if 'title' is not provided -const displayName = getDisplayName(tool); // Returns title if available, otherwise name +// Automatically handles the precedence: title → annotations.title → name +const displayName = getDisplayName(tool); ``` ## Running Your Server diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index 63d7d60a9..efa176df5 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -318,7 +318,12 @@ async function listTools(): Promise { console.log(' No tools available'); } else { for (const tool of toolsResult.tools) { - console.log(` - ${getDisplayName(tool)}: ${tool.description}`); + const displayName = getDisplayName(tool); + if (displayName !== tool.name) { + console.log(` - ${tool.name} (${displayName}): ${tool.description}`); + } else { + console.log(` - ${tool.name}: ${tool.description}`); + } } } } catch (error) { diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 6ef33540c..36a8f7b88 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -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"); + }); +}); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7c77add13..3c1c00d9e 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -22,6 +22,7 @@ import { CompleteResult, PromptReference, ResourceTemplateReference, + BaseMetadata, Resource, ListResourcesResult, ListResourceTemplatesRequestSchema, @@ -128,7 +129,7 @@ export class McpServer { if (tool.outputSchema) { toolDefinition.outputSchema = zodToJsonSchema( - tool.outputSchema, + tool.outputSchema, { strictUnions: true } ) as Tool["outputSchema"]; } @@ -586,27 +587,13 @@ export class McpServer { throw new Error(`Resource ${uriOrTemplate} is already registered`); } - const registeredResource: RegisteredResource = { + const registeredResource = this._createRegisteredResource( name, + undefined, + uriOrTemplate, metadata, - readCallback: readCallback as ReadResourceCallback, - enabled: true, - disable: () => registeredResource.update({ enabled: false }), - enable: () => registeredResource.update({ enabled: true }), - remove: () => registeredResource.update({ uri: null }), - update: (updates) => { - if (typeof updates.uri !== "undefined" && updates.uri !== uriOrTemplate) { - delete this._registeredResources[uriOrTemplate] - if (updates.uri) this._registeredResources[updates.uri] = registeredResource - } - if (typeof updates.name !== "undefined") registeredResource.name = updates.name - if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled - this.sendResourceListChanged() - }, - }; - this._registeredResources[uriOrTemplate] = registeredResource; + readCallback as ReadResourceCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -616,27 +603,13 @@ export class McpServer { throw new Error(`Resource template ${name} is already registered`); } - const registeredResourceTemplate: RegisteredResourceTemplate = { - resourceTemplate: uriOrTemplate, + const registeredResourceTemplate = this._createRegisteredResourceTemplate( + name, + undefined, + uriOrTemplate, metadata, - readCallback: readCallback as ReadResourceTemplateCallback, - enabled: true, - disable: () => registeredResourceTemplate.update({ enabled: false }), - enable: () => registeredResourceTemplate.update({ enabled: true }), - remove: () => registeredResourceTemplate.update({ name: null }), - update: (updates) => { - if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredResourceTemplates[name] - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate - } - if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template - if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled - this.sendResourceListChanged() - }, - }; - this._registeredResourceTemplates[name] = registeredResourceTemplate; + readCallback as ReadResourceTemplateCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -651,11 +624,7 @@ export class McpServer { registerResource( name: string, uriOrTemplate: string | ResourceTemplate, - config: ResourceMetadata & { - title?: string; - description?: string; - mimeType?: string; - }, + config: ResourceMetadata, readCallback: ReadResourceCallback | ReadResourceTemplateCallback ): RegisteredResource | RegisteredResourceTemplate { if (typeof uriOrTemplate === "string") { @@ -663,27 +632,13 @@ export class McpServer { throw new Error(`Resource ${uriOrTemplate} is already registered`); } - const registeredResource: RegisteredResource = { + const registeredResource = this._createRegisteredResource( name, - metadata: config, - readCallback: readCallback as ReadResourceCallback, - enabled: true, - disable: () => registeredResource.update({ enabled: false }), - enable: () => registeredResource.update({ enabled: true }), - remove: () => registeredResource.update({ uri: null }), - update: (updates) => { - if (typeof updates.uri !== "undefined" && updates.uri !== uriOrTemplate) { - delete this._registeredResources[uriOrTemplate] - if (updates.uri) this._registeredResources[updates.uri] = registeredResource - } - if (typeof updates.name !== "undefined") registeredResource.name = updates.name - if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled - this.sendResourceListChanged() - }, - }; - this._registeredResources[uriOrTemplate] = registeredResource; + (config as BaseMetadata).title, + uriOrTemplate, + config, + readCallback as ReadResourceCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -693,27 +648,13 @@ export class McpServer { throw new Error(`Resource template ${name} is already registered`); } - const registeredResourceTemplate: RegisteredResourceTemplate = { - resourceTemplate: uriOrTemplate, - metadata: config, - readCallback: readCallback as ReadResourceTemplateCallback, - enabled: true, - disable: () => registeredResourceTemplate.update({ enabled: false }), - enable: () => registeredResourceTemplate.update({ enabled: true }), - remove: () => registeredResourceTemplate.update({ name: null }), - update: (updates) => { - if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredResourceTemplates[name] - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate - } - if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template - if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled - this.sendResourceListChanged() - }, - }; - this._registeredResourceTemplates[name] = registeredResourceTemplate; + const registeredResourceTemplate = this._createRegisteredResourceTemplate( + name, + (config as BaseMetadata).title, + uriOrTemplate, + config, + readCallback as ReadResourceTemplateCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -721,8 +662,108 @@ export class McpServer { } } + private _createRegisteredResource( + name: string, + title: string | undefined, + uri: string, + metadata: ResourceMetadata | undefined, + readCallback: ReadResourceCallback + ): RegisteredResource { + const registeredResource: RegisteredResource = { + name, + title, + metadata, + readCallback, + enabled: true, + disable: () => registeredResource.update({ enabled: false }), + enable: () => registeredResource.update({ enabled: true }), + remove: () => registeredResource.update({ uri: null }), + update: (updates) => { + if (typeof updates.uri !== "undefined" && updates.uri !== uri) { + delete this._registeredResources[uri] + if (updates.uri) this._registeredResources[updates.uri] = registeredResource + } + if (typeof updates.name !== "undefined") registeredResource.name = updates.name + if (typeof updates.title !== "undefined") registeredResource.title = updates.title + if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata + if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback + if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled + this.sendResourceListChanged() + }, + }; + this._registeredResources[uri] = registeredResource; + return registeredResource; + } + + private _createRegisteredResourceTemplate( + name: string, + title: string | undefined, + template: ResourceTemplate, + metadata: ResourceMetadata | undefined, + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate { + const registeredResourceTemplate: RegisteredResourceTemplate = { + resourceTemplate: template, + title, + metadata, + readCallback, + enabled: true, + disable: () => registeredResourceTemplate.update({ enabled: false }), + enable: () => registeredResourceTemplate.update({ enabled: true }), + remove: () => registeredResourceTemplate.update({ name: null }), + update: (updates) => { + if (typeof updates.name !== "undefined" && updates.name !== name) { + delete this._registeredResourceTemplates[name] + if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate + } + if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title + if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template + if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata + if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback + if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled + this.sendResourceListChanged() + }, + }; + this._registeredResourceTemplates[name] = registeredResourceTemplate; + return registeredResourceTemplate; + } + + private _createRegisteredPrompt( + name: string, + title: string | undefined, + description: string | undefined, + argsSchema: PromptArgsRawShape | undefined, + callback: PromptCallback + ): RegisteredPrompt { + const registeredPrompt: RegisteredPrompt = { + title, + description, + argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), + callback, + enabled: true, + disable: () => registeredPrompt.update({ enabled: false }), + enable: () => registeredPrompt.update({ enabled: true }), + remove: () => registeredPrompt.update({ name: null }), + update: (updates) => { + if (typeof updates.name !== "undefined" && updates.name !== name) { + delete this._registeredPrompts[name] + if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt + } + if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title + if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description + if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema) + if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback + if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled + this.sendPromptListChanged() + }, + }; + this._registeredPrompts[name] = registeredPrompt; + return registeredPrompt; + } + private _createRegisteredTool( name: string, + title: string | undefined, description: string | undefined, inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, @@ -730,6 +771,7 @@ export class McpServer { callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { + title, description, inputSchema: inputSchema === undefined ? undefined : z.object(inputSchema), @@ -868,7 +910,7 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, description, inputSchema, outputSchema, annotations, callback) + return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback) } /** @@ -891,21 +933,15 @@ export class McpServer { const { title, description, inputSchema, outputSchema, annotations } = config; - const registeredTool = this._createRegisteredTool( + return this._createRegisteredTool( name, + title, description, inputSchema, outputSchema, annotations, cb as ToolCallback ); - - // Set title if provided - if (title !== undefined) { - registeredTool.title = title; - } - - return registeredTool; } /** @@ -953,28 +989,13 @@ export class McpServer { } const cb = rest[0] as PromptCallback; - const registeredPrompt: RegisteredPrompt = { + const registeredPrompt = this._createRegisteredPrompt( + name, + undefined, description, - argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), - callback: cb, - enabled: true, - disable: () => registeredPrompt.update({ enabled: false }), - enable: () => registeredPrompt.update({ enabled: true }), - remove: () => registeredPrompt.update({ name: null }), - update: (updates) => { - if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredPrompts[name] - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt - } - if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title - if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description - if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema) - if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback - if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled - this.sendPromptListChanged() - }, - }; - this._registeredPrompts[name] = registeredPrompt; + argsSchema, + cb + ); this.setPromptRequestHandlers(); this.sendPromptListChanged() @@ -1000,29 +1021,13 @@ export class McpServer { const { title, description, arguments: argsSchema } = config; - const registeredPrompt: RegisteredPrompt = { + const registeredPrompt = this._createRegisteredPrompt( + name, title, description, - argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), - callback: cb as PromptCallback, - 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 + ); this.setPromptRequestHandlers(); this.sendPromptListChanged() @@ -1155,16 +1160,16 @@ export type RegisteredTool = { enable(): void; disable(): void; update( - updates: { - name?: string | null, + updates: { + name?: string | null, title?: string, - description?: string, - paramsSchema?: InputArgs, - outputSchema?: OutputArgs, - annotations?: ToolAnnotations, - callback?: ToolCallback, - enabled?: boolean - }): void + description?: string, + paramsSchema?: InputArgs, + outputSchema?: OutputArgs, + annotations?: ToolAnnotations, + callback?: ToolCallback, + enabled?: boolean + }): void remove(): void }; @@ -1212,12 +1217,13 @@ export type ReadResourceCallback = ( export type RegisteredResource = { name: string; + title?: string; metadata?: ResourceMetadata; readCallback: ReadResourceCallback; enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void + update(updates: { name?: string, title?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void remove(): void }; @@ -1232,12 +1238,13 @@ export type ReadResourceTemplateCallback = ( export type RegisteredResourceTemplate = { resourceTemplate: ResourceTemplate; + title?: string; metadata?: ResourceMetadata; readCallback: ReadResourceTemplateCallback; enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void + update(updates: { name?: string | null, title?: string, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void remove(): void }; diff --git a/src/shared/metadataUtils.ts b/src/shared/metadataUtils.ts index d581a6b24..410827a5f 100644 --- a/src/shared/metadataUtils.ts +++ b/src/shared/metadataUtils.ts @@ -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; } /** diff --git a/src/types.ts b/src/types.ts index 9ece70d46..4d1d14d24 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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(); From bce2dbfb83c6936be6ab05598d30b3d4d5e09d82 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 10:55:02 +0100 Subject: [PATCH 3/8] small fixes like argsSchema --- README.md | 13 ++-- src/examples/server/simpleStreamableHttp.ts | 9 ++- src/server/mcp.ts | 4 +- src/server/title.test.ts | 80 ++++++++++----------- 4 files changed, 51 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index f9f080b92..5ae9e0436 100644 --- a/README.md +++ b/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 @@ -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" }); ``` @@ -471,8 +469,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( @@ -507,7 +504,7 @@ server.registerPrompt( { title: "Echo Prompt", description: "Creates a prompt to process a message", - arguments: { message: z.string() } + argsSchema: { message: z.string() } }, ({ message }) => ({ messages: [{ diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index b66be93d0..fca0ff5af 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -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 => { return { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 3c1c00d9e..7394ecbf5 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1011,7 +1011,7 @@ export class McpServer { config: { title?: string; description?: string; - arguments?: Args; + argsSchema?: Args; }, cb: PromptCallback ): RegisteredPrompt { @@ -1019,7 +1019,7 @@ export class McpServer { throw new Error(`Prompt ${name} is already registered`); } - const { title, description, arguments: argsSchema } = config; + const { title, description, argsSchema } = config; const registeredPrompt = this._createRegisteredPrompt( name, diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 7874fb62c..02bafe127 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -7,12 +7,12 @@ 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", @@ -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"); @@ -171,21 +171,21 @@ describe("Title field backwards compatibility", () => { 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"); From 8719f933571e5ba8e0f5b1dc47fb901cb84d9a48 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 11:00:20 +0100 Subject: [PATCH 4/8] clean up mcp.ts --- src/server/mcp.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7394ecbf5..ac6cf7727 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -114,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, { @@ -123,10 +124,6 @@ export class McpServer { annotations: tool.annotations, }; - if (tool.title !== undefined) { - toolDefinition.title = tool.title; - } - if (tool.outputSchema) { toolDefinition.outputSchema = zodToJsonSchema( tool.outputSchema, @@ -472,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; }, ), }), From 3f939be6c7336af0fd91e3fd4ded20a4d0d02a57 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 11:21:07 +0100 Subject: [PATCH 5/8] readme fixes and template tests for title --- README.md | 12 +++++------ src/server/title.test.ts | 44 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5ae9e0436..c753e3b18 100644 --- a/README.md +++ b/README.md @@ -78,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}!` }] }) ); @@ -143,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}` }] }) ); @@ -479,10 +479,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}` }] }) ); diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 02bafe127..480257cd5 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -2,7 +2,7 @@ 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 () => { @@ -169,6 +169,48 @@ 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(); From e78c1b787d663c17d3e122d9372d96998f268be7 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 11:27:34 +0100 Subject: [PATCH 6/8] fix --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c753e3b18..0170b5fe1 100644 --- a/README.md +++ b/README.md @@ -530,8 +530,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 From 90e45950b3c0100469a340b661731f929c02298e Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 11:36:08 +0100 Subject: [PATCH 7/8] show name and title in example --- src/examples/client/simpleStreamableHttp.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index efa176df5..e7232fe0f 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -318,12 +318,7 @@ async function listTools(): Promise { console.log(' No tools available'); } else { for (const tool of toolsResult.tools) { - const displayName = getDisplayName(tool); - if (displayName !== tool.name) { - console.log(` - ${tool.name} (${displayName}): ${tool.description}`); - } else { - console.log(` - ${tool.name}: ${tool.description}`); - } + console.log(` - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`); } } } catch (error) { @@ -386,7 +381,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: { @@ -399,7 +394,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 @@ -435,7 +430,7 @@ async function listPrompts(): Promise { 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) { @@ -486,7 +481,7 @@ async function listResources(): Promise { 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) { From f1a07a940a13b79da8d2ac3b13ba7f2590de65d5 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 16 Jun 2025 11:45:35 +0100 Subject: [PATCH 8/8] ci fix --- src/server/title.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 480257cd5..3f64570b8 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -185,7 +185,7 @@ describe("Title field backwards compatibility", () => { title: "User Profile", description: "User profile information" }, - async (uri, { userId }, extra) => ({ + async (uri, { userId }, _extra) => ({ contents: [{ uri: uri.href, text: `Profile data for user ${userId}`