add title
This commit is contained in:
parent
e7284d29c8
commit
4d03b243e3
7 changed files with 599 additions and 171 deletions
138
README.md
138
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 {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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<void> {
|
|||
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<void> {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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<CallToolResult> => {
|
||||
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<GetPromptResult> => {
|
||||
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<ReadResourceResult> => {
|
||||
return {
|
||||
contents: [
|
||||
|
|
|
|||
|
|
@ -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<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
|
||||
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<ZodRawShape | undefined>
|
||||
)
|
||||
);
|
||||
|
||||
// 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<Args extends PromptArgsRawShape>(
|
||||
name: string,
|
||||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
arguments?: Args;
|
||||
},
|
||||
cb: PromptCallback<Args>
|
||||
): 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<PromptArgsRawShape | undefined>,
|
||||
enabled: true,
|
||||
disable: () => registeredPrompt.update({ enabled: false }),
|
||||
enable: () => registeredPrompt.update({ enabled: true }),
|
||||
remove: () => registeredPrompt.update({ name: null }),
|
||||
update: (updates) => {
|
||||
if (typeof updates.name !== "undefined" && updates.name !== name) {
|
||||
delete this._registeredPrompts[name]
|
||||
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
|
||||
}
|
||||
if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title
|
||||
if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
|
||||
if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
|
||||
if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
|
||||
if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
|
||||
this.sendPromptListChanged()
|
||||
},
|
||||
};
|
||||
this._registeredPrompts[name] = registeredPrompt;
|
||||
|
||||
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<Args extends undefined | ZodRawShape = undefined> =
|
|||
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
|
||||
|
||||
export type RegisteredTool = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
inputSchema?: AnyZodObject;
|
||||
outputSchema?: AnyZodObject;
|
||||
|
|
@ -1011,6 +1157,7 @@ export type RegisteredTool = {
|
|||
update<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
|
||||
updates: {
|
||||
name?: string | null,
|
||||
title?: string,
|
||||
description?: string,
|
||||
paramsSchema?: InputArgs,
|
||||
outputSchema?: OutputArgs,
|
||||
|
|
@ -1110,13 +1257,14 @@ export type PromptCallback<
|
|||
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
|
||||
|
||||
export type RegisteredPrompt = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
argsSchema?: ZodObject<PromptArgsRawShape>;
|
||||
callback: PromptCallback<undefined | PromptArgsRawShape>;
|
||||
enabled: boolean;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
update<Args extends PromptArgsRawShape>(updates: { name?: string | null, description?: string, argsSchema?: Args, callback?: PromptCallback<Args>, enabled?: boolean }): void
|
||||
update<Args extends PromptArgsRawShape>(updates: { name?: string | null, title?: string, description?: string, argsSchema?: Args, callback?: PromptCallback<Args>, enabled?: boolean }): void
|
||||
remove(): void
|
||||
};
|
||||
|
||||
|
|
|
|||
194
src/server/title.test.ts
Normal file
194
src/server/title.test.ts
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
21
src/shared/metadataUtils.ts
Normal file
21
src/shared/metadataUtils.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
227
src/types.ts
227
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<typeof EmptyResultSchema>;
|
|||
/* Cancellation */
|
||||
export type CancelledNotification = Infer<typeof CancelledNotificationSchema>;
|
||||
|
||||
/* Base Metadata */
|
||||
export type BaseMetadata = Infer<typeof BaseMetadataSchema>;
|
||||
|
||||
/* Initialization */
|
||||
export type Implementation = Infer<typeof ImplementationSchema>;
|
||||
export type ClientCapabilities = Infer<typeof ClientCapabilitiesSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue