diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index c20abffbc..d3ddcccce 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -356,11 +356,42 @@ Schema to method string mapping: Request/notification params remain fully typed. Remove unused schema imports after migration. -## 10. Client Behavioral Changes +## 10. Registered Primitives API Changes + +`RegisteredTool`, `RegisteredPrompt`, `RegisteredResource`, `RegisteredResourceTemplate` are now proper classes. The `update()` method signature changed: + +| v1 (update field) | v2 (update field) | Applies to | +| ----------------- | ----------------- | ---------------------------------------------------------------------------- | +| `paramsSchema` | `inputSchema` | RegisteredTool | +| `callback` | `handler` | RegisteredTool | +| `callback` | `callback` | RegisteredPrompt, RegisteredResource, RegisteredResourceTemplate (unchanged) | + +```typescript +// v1 +tool.update({ paramsSchema: { name: z.string() }, callback: handler }); + +// v2 +tool.update({ inputSchema: { name: z.string() }, handler: handler }); +``` + +**Note:** In v1, `paramsSchema` inconsistently differed from `inputSchema` used in `registerTool()`. Fixed in v2. + +**New:** `RegisteredTool` now supports `icons` field (parity with protocol `Tool` type). + +New getter methods on `McpServer`: + +| Getter | Returns | +|--------|---------| +| `mcpServer.tools` | `ReadonlyMap` | +| `mcpServer.prompts` | `ReadonlyMap` | +| `mcpServer.resources` | `ReadonlyMap` | +| `mcpServer.resourceTemplates` | `ReadonlyMap` | + +## 11. Client Behavioral Changes `Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead. -## 11. Migration Steps (apply in this order) +## 12. Migration Steps (apply in this order) 1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages 2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport` diff --git a/docs/migration.md b/docs/migration.md index 3446f3f26..265591655 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -300,6 +300,56 @@ Common method string replacements: | `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` | | `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` | +### Registered primitives are now classes + +`RegisteredTool`, `RegisteredPrompt`, `RegisteredResource`, and `RegisteredResourceTemplate` are now proper classes instead of plain object types. They are exported from `@modelcontextprotocol/server`. + +The `update()` method now uses `inputSchema` and `handler` instead of `paramsSchema` and `callback`: + +**Before (v1):** + +```typescript +const tool = server.registerTool('my-tool', { inputSchema: { name: z.string() } }, handler); + +tool.update({ + paramsSchema: { name: z.string(), value: z.number() }, + callback: newHandler +}); +``` + +**After (v2):** + +```typescript +const tool = server.registerTool('my-tool', { inputSchema: { name: z.string() } }, handler); + +tool.update({ + inputSchema: { name: z.string(), value: z.number() }, + handler: newHandler +}); +``` + +**Note:** In v1, `RegisteredTool.update()` used `paramsSchema` which inconsistently differed from the `inputSchema` field used in `registerTool()`. This has been fixed in v2. + +**New:** `RegisteredTool` now supports the `icons` field for parity with the protocol `Tool` type: + +```typescript +tool.update({ icons: [{ type: 'base64', mediaType: 'image/png', data: '...' }] }); +``` + +New getter methods are available on `McpServer` to access all registered items: + +```typescript +// Get all registered tools +for (const [name, tool] of mcpServer.tools) { + console.log(name, tool.description, tool.enabled); +} + +// Similarly for prompts, resources, resourceTemplates +mcpServer.prompts; +mcpServer.resources; +mcpServer.resourceTemplates; +``` + ### Client list methods return empty results for missing capabilities `Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation. diff --git a/packages/server/src/experimental/tasks/interfaces.ts b/packages/server/src/experimental/tasks/interfaces.ts index 0b32be213..b0fbfe2a9 100644 --- a/packages/server/src/experimental/tasks/interfaces.ts +++ b/packages/server/src/experimental/tasks/interfaces.ts @@ -14,7 +14,7 @@ import type { ZodRawShapeCompat } from '@modelcontextprotocol/core'; -import type { BaseToolCallback } from '../../server/mcp.js'; +import type { BaseToolCallback } from '../../server/primitives/index.js'; // ============================================================================ // Task Handler Types (for registerToolTask) diff --git a/packages/server/src/experimental/tasks/mcpServer.ts b/packages/server/src/experimental/tasks/mcpServer.ts index 6fd5a6cc5..6e71b147d 100644 --- a/packages/server/src/experimental/tasks/mcpServer.ts +++ b/packages/server/src/experimental/tasks/mcpServer.ts @@ -7,7 +7,8 @@ import type { AnySchema, TaskToolExecution, ToolAnnotations, ToolExecution, ZodRawShapeCompat } from '@modelcontextprotocol/core'; -import type { AnyToolHandler, McpServer, RegisteredTool } from '../../server/mcp.js'; +import type { McpServer } from '../../server/mcp.js'; +import type { AnyToolHandler, RegisteredTool } from '../../server/primitives/index.js'; import type { ToolTaskHandler } from './interfaces.js'; /** diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1a8dbf143..c23ee7619 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,6 +1,7 @@ export * from './server/completable.js'; export * from './server/mcp.js'; export * from './server/middleware/hostHeaderValidation.js'; +export * from './server/primitives/index.js'; export * from './server/server.js'; export * from './server/stdio.js'; export * from './server/streamableHttp.js'; diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 2d902728f..7b31d5ca3 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -11,26 +11,18 @@ import type { GetPromptResult, Implementation, ListPromptsResult, - ListResourcesResult, ListToolsResult, LoggingMessageNotification, - Prompt, - PromptArgument, PromptReference, - ReadResourceResult, RequestHandlerExtra, Resource, ResourceTemplateReference, - Result, SchemaOutput, ServerNotification, ServerRequest, - ShapeOutput, - Tool, ToolAnnotations, ToolExecution, Transport, - Variables, ZodRawShapeCompat } from '@modelcontextprotocol/core'; import { @@ -38,22 +30,28 @@ import { assertCompleteRequestResourceTemplate, getObjectShape, getParseErrorMessage, - getSchemaDescription, - isSchemaOptional, normalizeObjectSchema, objectFromShape, ProtocolError, ProtocolErrorCode, - safeParseAsync, - toJsonSchemaCompat, - UriTemplate, - validateAndWarnToolName + safeParseAsync } from '@modelcontextprotocol/core'; import { ZodOptional } from 'zod'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js'; import { getCompleter, isCompletable } from './completable.js'; +import type { + AnyToolHandler, + PromptArgsRawShape, + PromptCallback, + ReadResourceCallback, + ReadResourceTemplateCallback, + ResourceMetadata, + ResourceTemplate, + ToolCallback +} from './primitives/index.js'; +import { RegisteredPrompt, RegisteredResource, RegisteredResourceTemplate, RegisteredTool } from './primitives/index.js'; import type { ServerOptions } from './server.js'; import { Server } from './server.js'; @@ -96,6 +94,38 @@ export class McpServer { return this._experimental; } + /** + * Gets all registered tools. + * @returns A read-only map of tool names to RegisteredTool instances + */ + get tools(): ReadonlyMap { + return new Map(Object.entries(this._registeredTools)); + } + + /** + * Gets all registered prompts. + * @returns A read-only map of prompt names to RegisteredPrompt instances + */ + get prompts(): ReadonlyMap { + return new Map(Object.entries(this._registeredPrompts)); + } + + /** + * Gets all registered resources. + * @returns A read-only map of resource URIs to RegisteredResource instances + */ + get resources(): ReadonlyMap { + return new Map(Object.entries(this._registeredResources)); + } + + /** + * Gets all registered resource templates. + * @returns A read-only map of template names to RegisteredResourceTemplate instances + */ + get resourceTemplates(): ReadonlyMap { + return new Map(Object.entries(this._registeredResourceTemplates)); + } + /** * Attaches to the given transport, starts it, and starts listening for messages. * @@ -131,39 +161,9 @@ export class McpServer { this.server.setRequestHandler( 'tools/list', (): ListToolsResult => ({ - tools: Object.entries(this._registeredTools) - .filter(([, tool]) => tool.enabled) - .map(([name, tool]): Tool => { - const toolDefinition: Tool = { - name, - title: tool.title, - description: tool.description, - inputSchema: (() => { - const obj = normalizeObjectSchema(tool.inputSchema); - return obj - ? (toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: 'input' - }) as Tool['inputSchema']) - : EMPTY_OBJECT_JSON_SCHEMA; - })(), - annotations: tool.annotations, - execution: tool.execution, - _meta: tool._meta - }; - - if (tool.outputSchema) { - const obj = normalizeObjectSchema(tool.outputSchema); - if (obj) { - toolDefinition.outputSchema = toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: 'output' - }) as Tool['outputSchema']; - } - } - - return toolDefinition; - }) + tools: Object.values(this._registeredTools) + .filter(tool => tool.enabled) + .map(tool => tool.toProtocolTool()) }) ); @@ -495,13 +495,9 @@ export class McpServer { }); this.server.setRequestHandler('resources/list', async (_request, extra) => { - const resources = Object.entries(this._registeredResources) - .filter(([_, resource]) => resource.enabled) - .map(([uri, resource]) => ({ - uri, - name: resource.name, - ...resource.metadata - })); + const resources = Object.values(this._registeredResources) + .filter(resource => resource.enabled) + .map(resource => resource.toProtocolResource()); const templateResources: Resource[] = []; for (const template of Object.values(this._registeredResourceTemplates)) { @@ -523,11 +519,9 @@ export class McpServer { }); this.server.setRequestHandler('resources/templates/list', async () => { - const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({ - name, - uriTemplate: template.resourceTemplate.uriTemplate.toString(), - ...template.metadata - })); + const resourceTemplates = Object.values(this._registeredResourceTemplates).map(template => + template.toProtocolResourceTemplate() + ); return { resourceTemplates }; }); @@ -577,16 +571,9 @@ export class McpServer { this.server.setRequestHandler( 'prompts/list', (): ListPromptsResult => ({ - prompts: Object.entries(this._registeredPrompts) - .filter(([, prompt]) => prompt.enabled) - .map(([name, prompt]): Prompt => { - return { - name, - title: prompt.title, - description: prompt.description, - arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined - }; - }) + prompts: Object.values(this._registeredPrompts) + .filter(prompt => prompt.enabled) + .map(prompt => prompt.toProtocolPrompt()) }) ); @@ -684,30 +671,27 @@ export class McpServer { 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 (updates.uri !== undefined && updates.uri !== uri) { - delete this._registeredResources[uri]; - if (updates.uri) this._registeredResources[updates.uri] = registeredResource; - } - if (updates.name !== undefined) registeredResource.name = updates.name; - if (updates.title !== undefined) registeredResource.title = updates.title; - if (updates.metadata !== undefined) registeredResource.metadata = updates.metadata; - if (updates.callback !== undefined) registeredResource.readCallback = updates.callback; - if (updates.enabled !== undefined) registeredResource.enabled = updates.enabled; + const resource = new RegisteredResource( + { + name, + title, + uri, + ...metadata, + readCallback + }, + () => this.sendResourceListChanged(), + (oldUri, newUri, r) => { + delete this._registeredResources[oldUri]; + this._registeredResources[newUri] = r; + this.sendResourceListChanged(); + }, + resourceUri => { + delete this._registeredResources[resourceUri]; this.sendResourceListChanged(); } - }; - this._registeredResources[uri] = registeredResource; - return registeredResource; + ); + this._registeredResources[uri] = resource; + return resource; } private _createRegisteredResourceTemplate( @@ -717,29 +701,26 @@ export class McpServer { 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 (updates.name !== undefined && updates.name !== name) { - delete this._registeredResourceTemplates[name]; - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; - } - if (updates.title !== undefined) registeredResourceTemplate.title = updates.title; - if (updates.template !== undefined) registeredResourceTemplate.resourceTemplate = updates.template; - if (updates.metadata !== undefined) registeredResourceTemplate.metadata = updates.metadata; - if (updates.callback !== undefined) registeredResourceTemplate.readCallback = updates.callback; - if (updates.enabled !== undefined) registeredResourceTemplate.enabled = updates.enabled; + const resourceTemplate = new RegisteredResourceTemplate( + { + name, + title, + resourceTemplate: template, + ...metadata, + readCallback + }, + () => this.sendResourceListChanged(), + (oldName, newName, rt) => { + delete this._registeredResourceTemplates[oldName]; + this._registeredResourceTemplates[newName] = rt; + this.sendResourceListChanged(); + }, + templateName => { + delete this._registeredResourceTemplates[templateName]; this.sendResourceListChanged(); } - }; - this._registeredResourceTemplates[name] = registeredResourceTemplate; + ); + this._registeredResourceTemplates[name] = resourceTemplate; // If the resource template has any completion callbacks, enable completions capability const variableNames = template.uriTemplate.variableNames; @@ -748,7 +729,7 @@ export class McpServer { this.setCompletionRequestHandler(); } - return registeredResourceTemplate; + return resourceTemplate; } private _createRegisteredPrompt( @@ -758,29 +739,26 @@ export class McpServer { argsSchema: PromptArgsRawShape | undefined, callback: PromptCallback ): RegisteredPrompt { - const registeredPrompt: RegisteredPrompt = { - title, - description, - argsSchema: argsSchema === undefined ? undefined : objectFromShape(argsSchema), - callback, - enabled: true, - disable: () => registeredPrompt.update({ enabled: false }), - enable: () => registeredPrompt.update({ enabled: true }), - remove: () => registeredPrompt.update({ name: null }), - update: updates => { - if (updates.name !== undefined && updates.name !== name) { - delete this._registeredPrompts[name]; - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt; - } - if (updates.title !== undefined) registeredPrompt.title = updates.title; - if (updates.description !== undefined) registeredPrompt.description = updates.description; - if (updates.argsSchema !== undefined) registeredPrompt.argsSchema = objectFromShape(updates.argsSchema); - if (updates.callback !== undefined) registeredPrompt.callback = updates.callback; - if (updates.enabled !== undefined) registeredPrompt.enabled = updates.enabled; + const prompt = new RegisteredPrompt( + { + name, + title, + description, + argsSchema: argsSchema === undefined ? undefined : objectFromShape(argsSchema), + callback + }, + () => this.sendPromptListChanged(), + (oldName, newName, p) => { + delete this._registeredPrompts[oldName]; + this._registeredPrompts[newName] = p; + this.sendPromptListChanged(); + }, + promptName => { + delete this._registeredPrompts[promptName]; this.sendPromptListChanged(); } - }; - this._registeredPrompts[name] = registeredPrompt; + ); + this._registeredPrompts[name] = prompt; // If any argument uses a Completable schema, enable completions capability if (argsSchema) { @@ -793,7 +771,7 @@ export class McpServer { } } - return registeredPrompt; + return prompt; } private _createRegisteredTool( @@ -807,47 +785,35 @@ export class McpServer { _meta: Record | undefined, handler: AnyToolHandler ): RegisteredTool { - // Validate tool name according to SEP specification - validateAndWarnToolName(name); - - const registeredTool: RegisteredTool = { - title, - description, - inputSchema: getZodSchemaObject(inputSchema), - outputSchema: getZodSchemaObject(outputSchema), - annotations, - execution, - _meta, - handler: handler, - enabled: true, - disable: () => registeredTool.update({ enabled: false }), - enable: () => registeredTool.update({ enabled: true }), - remove: () => registeredTool.update({ name: null }), - update: updates => { - if (updates.name !== undefined && updates.name !== name) { - if (typeof updates.name === 'string') { - validateAndWarnToolName(updates.name); - } - delete this._registeredTools[name]; - if (updates.name) this._registeredTools[updates.name] = registeredTool; - } - if (updates.title !== undefined) registeredTool.title = updates.title; - if (updates.description !== undefined) registeredTool.description = updates.description; - if (updates.paramsSchema !== undefined) registeredTool.inputSchema = objectFromShape(updates.paramsSchema); - if (updates.outputSchema !== undefined) registeredTool.outputSchema = objectFromShape(updates.outputSchema); - if (updates.callback !== undefined) registeredTool.handler = updates.callback; - if (updates.annotations !== undefined) registeredTool.annotations = updates.annotations; - if (updates._meta !== undefined) registeredTool._meta = updates._meta; - if (updates.enabled !== undefined) registeredTool.enabled = updates.enabled; + const tool = new RegisteredTool( + { + name, + title, + description, + inputSchema: getZodSchemaObject(inputSchema), + outputSchema: getZodSchemaObject(outputSchema), + annotations, + execution, + _meta, + handler + }, + () => this.sendToolListChanged(), + (oldName, newName, t) => { + delete this._registeredTools[oldName]; + this._registeredTools[newName] = t; + this.sendToolListChanged(); + }, + toolName => { + delete this._registeredTools[toolName]; this.sendToolListChanged(); } - }; - this._registeredTools[name] = registeredTool; + ); + this._registeredTools[name] = tool; this.setToolRequestHandlers(); this.sendToolListChanged(); - return registeredTool; + return tool; } /** @@ -962,125 +928,7 @@ export class McpServer { } } -/** - * A callback to complete one variable within a resource template's URI template. - */ -export type CompleteResourceTemplateCallback = ( - value: string, - context?: { - arguments?: Record; - } -) => string[] | Promise; - -/** - * A resource template combines a URI pattern with optional functionality to enumerate - * all resources matching that pattern. - */ -export class ResourceTemplate { - private _uriTemplate: UriTemplate; - - constructor( - uriTemplate: string | UriTemplate, - private _callbacks: { - /** - * A callback to list all resources matching this template. This is required to specified, even if `undefined`, to avoid accidentally forgetting resource listing. - */ - list: ListResourcesCallback | undefined; - - /** - * An optional callback to autocomplete variables within the URI template. Useful for clients and users to discover possible values. - */ - complete?: { - [variable: string]: CompleteResourceTemplateCallback; - }; - } - ) { - this._uriTemplate = typeof uriTemplate === 'string' ? new UriTemplate(uriTemplate) : uriTemplate; - } - - /** - * Gets the URI template pattern. - */ - get uriTemplate(): UriTemplate { - return this._uriTemplate; - } - - /** - * Gets the list callback, if one was provided. - */ - get listCallback(): ListResourcesCallback | undefined { - return this._callbacks.list; - } - - /** - * Gets the callback for completing a specific URI template variable, if one was provided. - */ - completeCallback(variable: string): CompleteResourceTemplateCallback | undefined { - return this._callbacks.complete?.[variable]; - } -} - -export type BaseToolCallback< - SendResultT extends Result, - Extra extends RequestHandlerExtra, - Args extends undefined | ZodRawShapeCompat | AnySchema -> = Args extends ZodRawShapeCompat - ? (args: ShapeOutput, extra: Extra) => SendResultT | Promise - : Args extends AnySchema - ? (args: SchemaOutput, extra: Extra) => SendResultT | Promise - : (extra: Extra) => SendResultT | Promise; - -/** - * Callback for a tool handler registered with Server.tool(). - * - * Parameters will include tool arguments, if applicable, as well as other request handler context. - * - * The callback should return: - * - `structuredContent` if the tool has an outputSchema defined - * - `content` if the tool does not have an outputSchema - * - Both fields are optional but typically one should be provided - */ -export type ToolCallback = BaseToolCallback< - CallToolResult, - RequestHandlerExtra, - Args ->; - -/** - * Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object). - */ -export type AnyToolHandler = ToolCallback | ToolTaskHandler; - -export type RegisteredTool = { - title?: string; - description?: string; - inputSchema?: AnySchema; - outputSchema?: AnySchema; - annotations?: ToolAnnotations; - execution?: ToolExecution; - _meta?: Record; - handler: AnyToolHandler; - enabled: boolean; - enable(): void; - disable(): void; - update(updates: { - name?: string | null; - title?: string; - description?: string; - paramsSchema?: InputArgs; - outputSchema?: OutputArgs; - annotations?: ToolAnnotations; - _meta?: Record; - callback?: ToolCallback; - enabled?: boolean; - }): void; - remove(): void; -}; - -const EMPTY_OBJECT_JSON_SCHEMA = { - type: 'object' as const, - properties: {} -}; +// Utility functions for schema handling /** * Checks if a value looks like a Zod schema by checking for parse/safeParse methods. @@ -1152,114 +1000,6 @@ function getZodSchemaObject(schema: ZodRawShapeCompat | AnySchema | undefined): return schema; } -/** - * Additional, optional information for annotating a resource. - */ -export type ResourceMetadata = Omit; - -/** - * Callback to list all resources matching a given template. - */ -export type ListResourcesCallback = ( - extra: RequestHandlerExtra -) => ListResourcesResult | Promise; - -/** - * Callback to read a resource at a given URI. - */ -export type ReadResourceCallback = ( - uri: URL, - extra: RequestHandlerExtra -) => ReadResourceResult | Promise; - -export type RegisteredResource = { - name: string; - title?: string; - metadata?: ResourceMetadata; - readCallback: ReadResourceCallback; - enabled: boolean; - enable(): void; - disable(): void; - update(updates: { - name?: string; - title?: string; - uri?: string | null; - metadata?: ResourceMetadata; - callback?: ReadResourceCallback; - enabled?: boolean; - }): void; - remove(): void; -}; - -/** - * Callback to read a resource at a given URI, following a filled-in URI template. - */ -export type ReadResourceTemplateCallback = ( - uri: URL, - variables: Variables, - extra: RequestHandlerExtra -) => ReadResourceResult | Promise; - -export type RegisteredResourceTemplate = { - resourceTemplate: ResourceTemplate; - title?: string; - metadata?: ResourceMetadata; - readCallback: ReadResourceTemplateCallback; - enabled: boolean; - enable(): void; - disable(): void; - update(updates: { - name?: string | null; - title?: string; - template?: ResourceTemplate; - metadata?: ResourceMetadata; - callback?: ReadResourceTemplateCallback; - enabled?: boolean; - }): void; - remove(): void; -}; - -type PromptArgsRawShape = ZodRawShapeCompat; - -export type PromptCallback = Args extends PromptArgsRawShape - ? (args: ShapeOutput, extra: RequestHandlerExtra) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; - -export type RegisteredPrompt = { - title?: string; - description?: string; - argsSchema?: AnyObjectSchema; - callback: PromptCallback; - enabled: boolean; - enable(): void; - disable(): void; - update(updates: { - name?: string | null; - title?: string; - description?: string; - argsSchema?: Args; - callback?: PromptCallback; - enabled?: boolean; - }): void; - remove(): void; -}; - -function promptArgumentsFromSchema(schema: AnyObjectSchema): PromptArgument[] { - const shape = getObjectShape(schema); - if (!shape) return []; - return Object.entries(shape).map(([name, field]): PromptArgument => { - // Get description - works for both v3 and v4 - const description = getSchemaDescription(field); - // Check if optional - works for both v3 and v4 - const isOptional = isSchemaOptional(field); - return { - name, - description, - required: !isOptional - }; - }); -} - function createCompletionResult(suggestions: string[]): CompleteResult { return { completion: { diff --git a/packages/server/src/server/primitives/index.ts b/packages/server/src/server/primitives/index.ts new file mode 100644 index 000000000..9b415d48d --- /dev/null +++ b/packages/server/src/server/primitives/index.ts @@ -0,0 +1,41 @@ +/** + * Registered primitives for MCP server (tools, prompts, resources). + * These classes manage the lifecycle of registered items and provide + * methods to enable, disable, update, and remove them. + */ + +// Shared types +export type { OnRemove, OnRename, OnUpdate } from './types.js'; + +// Tool exports +export { + type AnyToolHandler, + type BaseToolCallback, + RegisteredTool, + type ToolCallback, + type ToolConfig, + type ToolProtocolFields +} from './tool.js'; + +// Prompt exports +export { type PromptArgsRawShape, type PromptCallback, type PromptConfig, type PromptProtocolFields, RegisteredPrompt } from './prompt.js'; + +// Resource exports +export { + type ReadResourceCallback, + RegisteredResource, + type ResourceConfig, + type ResourceMetadata, + type ResourceProtocolFields +} from './resource.js'; + +// Resource template exports +export { + type CompleteResourceTemplateCallback, + type ListResourcesCallback, + type ReadResourceTemplateCallback, + RegisteredResourceTemplate, + ResourceTemplate, + type ResourceTemplateConfig, + type ResourceTemplateProtocolFields +} from './resourceTemplate.js'; diff --git a/packages/server/src/server/primitives/prompt.ts b/packages/server/src/server/primitives/prompt.ts new file mode 100644 index 000000000..b5b89c054 --- /dev/null +++ b/packages/server/src/server/primitives/prompt.ts @@ -0,0 +1,222 @@ +import type { + AnyObjectSchema, + GetPromptResult, + Icon, + Prompt, + PromptArgument, + RequestHandlerExtra, + ServerNotification, + ServerRequest, + ShapeOutput, + ZodRawShapeCompat +} from '@modelcontextprotocol/core'; +import { getObjectShape, getSchemaDescription, isSchemaOptional, objectFromShape } from '@modelcontextprotocol/core'; + +import type { OnRemove, OnRename, OnUpdate } from './types.js'; + +/** + * Raw shape type for prompt arguments (Zod schema shape). + */ +export type PromptArgsRawShape = ZodRawShapeCompat; + +/** + * Callback for a prompt handler registered with McpServer.registerPrompt(). + */ +export type PromptCallback = Args extends PromptArgsRawShape + ? (args: ShapeOutput, extra: RequestHandlerExtra) => GetPromptResult | Promise + : (extra: RequestHandlerExtra) => GetPromptResult | Promise; + +/** + * Protocol fields for Prompt, derived from the Prompt type. + * Uses argsSchema (Zod shape) instead of arguments array (converted in toProtocolPrompt). + */ +export type PromptProtocolFields = Omit & { + argsSchema?: AnyObjectSchema; +}; + +/** + * Configuration for creating a RegisteredPrompt. + * Combines protocol fields with SDK-specific callback. + */ +export type PromptConfig = PromptProtocolFields & { + callback: PromptCallback; +}; + +/** + * Converts a Zod object schema to an array of PromptArguments. + */ +function promptArgumentsFromSchema(schema: AnyObjectSchema): PromptArgument[] { + const shape = getObjectShape(schema); + if (!shape) return []; + return Object.entries(shape).map(([name, field]): PromptArgument => { + const description = getSchemaDescription(field); + const isOptional = isSchemaOptional(field); + return { + name, + description, + required: !isOptional + }; + }); +} + +/** + * A registered prompt in the MCP server. + * Provides methods to enable, disable, update, rename, and remove the prompt. + */ +export class RegisteredPrompt { + // Protocol fields - stored together for easy spreading + #protocolFields: PromptProtocolFields; + + // SDK-specific fields - separate from protocol + #callback: PromptCallback; + #enabled: boolean = true; + + // Callbacks for McpServer communication + readonly #onUpdate: OnUpdate; + readonly #onRename: OnRename; + readonly #onRemove: OnRemove; + + constructor(config: PromptConfig, onUpdate: OnUpdate, onRename: OnRename, onRemove: OnRemove) { + // Separate protocol fields from SDK fields + const { callback, ...protocolFields } = config; + this.#protocolFields = protocolFields; + this.#callback = callback; + + this.#onUpdate = onUpdate; + this.#onRename = onRename; + this.#onRemove = onRemove; + } + + // Protocol field getters (delegate to #protocolFields) + get name(): string { + return this.#protocolFields.name; + } + get title(): string | undefined { + return this.#protocolFields.title; + } + get description(): string | undefined { + return this.#protocolFields.description; + } + get icons(): Icon[] | undefined { + return this.#protocolFields.icons; + } + get argsSchema(): AnyObjectSchema | undefined { + return this.#protocolFields.argsSchema; + } + get _meta(): Record | undefined { + return this.#protocolFields._meta; + } + + // SDK-specific getters + get callback(): PromptCallback { + return this.#callback; + } + get enabled(): boolean { + return this.#enabled; + } + + /** + * Enables the prompt. + * @returns this for chaining + */ + enable(): this { + if (!this.#enabled) { + this.#enabled = true; + this.#onUpdate(); + } + return this; + } + + /** + * Disables the prompt. + * @returns this for chaining + */ + disable(): this { + if (this.#enabled) { + this.#enabled = false; + this.#onUpdate(); + } + return this; + } + + /** + * Renames the prompt. + * @param newName - The new name for the prompt + * @returns this for chaining + */ + rename(newName: string): this { + if (newName !== this.#protocolFields.name) { + const oldName = this.#protocolFields.name; + this.#protocolFields.name = newName; + this.#onRename(oldName, newName, this); + } + return this; + } + + /** + * Removes the prompt from the registry. + */ + remove(): void { + this.#onRemove(this.#protocolFields.name); + } + + /** + * Updates the prompt's properties. + * @param updates - The properties to update + */ + update( + updates: { + name?: string | null; + argsSchema?: Args; + callback?: PromptCallback; + enabled?: boolean; + } & Omit, 'name' | 'argsSchema'> + ): void { + // Handle name change (rename or remove) + if (updates.name !== undefined) { + if (updates.name === null) { + this.remove(); + return; + } + this.rename(updates.name); + } + + // Extract special fields, update protocol fields in one go + const { name: _name, enabled, argsSchema, callback, ...protocolUpdates } = updates; + void _name; // Already handled above + Object.assign(this.#protocolFields, protocolUpdates); + + // Convert argsSchema from raw shape to object schema if provided + if (argsSchema !== undefined) { + this.#protocolFields.argsSchema = objectFromShape(argsSchema); + } + + // Update SDK-specific fields + if (callback !== undefined) { + this.#callback = callback as PromptCallback; + } + + // Handle enabled (triggers its own notification) + if (enabled === undefined) { + this.#onUpdate(); + } else if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + /** + * Converts to the Prompt protocol type (for list responses). + * Converts argsSchema to arguments array. + */ + toProtocolPrompt(): Prompt { + return { + ...this.#protocolFields, + // Convert argsSchema to arguments array + arguments: this.#protocolFields.argsSchema ? promptArgumentsFromSchema(this.#protocolFields.argsSchema) : undefined, + // Remove argsSchema from output (it's SDK-specific) + argsSchema: undefined + } as Prompt; + } +} diff --git a/packages/server/src/server/primitives/resource.ts b/packages/server/src/server/primitives/resource.ts new file mode 100644 index 000000000..138532901 --- /dev/null +++ b/packages/server/src/server/primitives/resource.ts @@ -0,0 +1,210 @@ +import type { + Icon, + ReadResourceResult, + RequestHandlerExtra, + Resource, + ServerNotification, + ServerRequest +} from '@modelcontextprotocol/core'; + +import type { OnRemove, OnRename, OnUpdate } from './types.js'; + +/** + * Additional, optional information for annotating a resource. + */ +export type ResourceMetadata = Omit; + +/** + * Callback to read a resource at a given URI. + */ +export type ReadResourceCallback = ( + uri: URL, + extra: RequestHandlerExtra +) => ReadResourceResult | Promise; + +/** + * Protocol fields for Resource, derived from the Resource type. + */ +export type ResourceProtocolFields = Resource; + +/** + * Configuration for creating a RegisteredResource. + * Combines protocol fields with SDK-specific callback. + */ +export type ResourceConfig = ResourceProtocolFields & { + readCallback: ReadResourceCallback; +}; + +/** + * A registered resource in the MCP server. + * Provides methods to enable, disable, update, rename, and remove the resource. + */ +export class RegisteredResource { + // Protocol fields - stored together for easy spreading + #protocolFields: ResourceProtocolFields; + + // SDK-specific fields - separate from protocol + #readCallback: ReadResourceCallback; + #enabled: boolean = true; + + // Callbacks for McpServer communication + readonly #onUpdate: OnUpdate; + readonly #onRename: OnRename; + readonly #onRemove: OnRemove; + + constructor(config: ResourceConfig, onUpdate: OnUpdate, onRename: OnRename, onRemove: OnRemove) { + // Separate protocol fields from SDK fields + const { readCallback, ...protocolFields } = config; + this.#protocolFields = protocolFields; + this.#readCallback = readCallback; + + this.#onUpdate = onUpdate; + this.#onRename = onRename; + this.#onRemove = onRemove; + } + + // Protocol field getters (delegate to #protocolFields) + get name(): string { + return this.#protocolFields.name; + } + get title(): string | undefined { + return this.#protocolFields.title; + } + get uri(): string { + return this.#protocolFields.uri; + } + get description(): string | undefined { + return this.#protocolFields.description; + } + get mimeType(): string | undefined { + return this.#protocolFields.mimeType; + } + get icons(): Icon[] | undefined { + return this.#protocolFields.icons; + } + get annotations(): Resource['annotations'] | undefined { + return this.#protocolFields.annotations; + } + get _meta(): Record | undefined { + return this.#protocolFields._meta; + } + + /** + * Gets the resource metadata (all fields except uri and name). + */ + get metadata(): ResourceMetadata { + return { + title: this.#protocolFields.title, + description: this.#protocolFields.description, + mimeType: this.#protocolFields.mimeType, + icons: this.#protocolFields.icons, + annotations: this.#protocolFields.annotations, + _meta: this.#protocolFields._meta + }; + } + + // SDK-specific getters + get readCallback(): ReadResourceCallback { + return this.#readCallback; + } + get enabled(): boolean { + return this.#enabled; + } + + /** + * Enables the resource. + * @returns this for chaining + */ + public enable(): this { + if (!this.#enabled) { + this.#enabled = true; + this.#onUpdate(); + } + return this; + } + + /** + * Disables the resource. + * @returns this for chaining + */ + public disable(): this { + if (this.#enabled) { + this.#enabled = false; + this.#onUpdate(); + } + return this; + } + + /** + * Changes the resource's URI (which is also the registry key). + * @param newUri - The new URI for the resource + * @returns this for chaining + */ + public changeUri(newUri: string): this { + if (newUri !== this.#protocolFields.uri) { + const oldUri = this.#protocolFields.uri; + this.#protocolFields.uri = newUri; + this.#onRename(oldUri, newUri, this); + } + return this; + } + + /** + * Removes the resource from the registry. + */ + public remove(): void { + this.#onRemove(this.#protocolFields.uri); + } + + /** + * Updates the resource's properties. + * @param updates - The properties to update + */ + public update( + updates: Partial & { + enabled?: boolean; + uri?: string | null; + callback?: ReadResourceCallback; + } + ): void { + const { + uri: uriUpdate, + enabled: enabledUpdate, + readCallback: readCallbackUpdate, + callback: callbackUpdate, + ...protocolUpdates + } = updates; + + // Handle uri change (change key or remove) + if (uriUpdate !== undefined) { + if (uriUpdate === null) { + this.remove(); + return; + } + this.changeUri(uriUpdate); + } + + // Extract special fields, update protocol fields in one go + Object.assign(this.#protocolFields, protocolUpdates); + + // Update SDK-specific fields (support both readCallback and callback) + if (readCallbackUpdate !== undefined) this.#readCallback = readCallbackUpdate; + if (callbackUpdate !== undefined) this.#readCallback = callbackUpdate; + + // Handle enabled (triggers its own notification) + if (enabledUpdate === undefined) { + this.#onUpdate(); + } else if (enabledUpdate) { + this.enable(); + } else { + this.disable(); + } + } + + /** + * Converts to the Resource protocol type (for list responses). + */ + public toProtocolResource(): Resource { + return { ...this.#protocolFields }; + } +} diff --git a/packages/server/src/server/primitives/resourceTemplate.ts b/packages/server/src/server/primitives/resourceTemplate.ts new file mode 100644 index 000000000..c22759d27 --- /dev/null +++ b/packages/server/src/server/primitives/resourceTemplate.ts @@ -0,0 +1,295 @@ +import type { + Icon, + ListResourcesResult, + ReadResourceResult, + RequestHandlerExtra, + ResourceTemplateType, + ServerNotification, + ServerRequest, + Variables +} from '@modelcontextprotocol/core'; +import { UriTemplate } from '@modelcontextprotocol/core'; + +import type { ResourceMetadata } from './resource.js'; +import type { OnRemove, OnRename, OnUpdate } from './types.js'; + +/** + * Callback to list all resources matching a given template. + */ +export type ListResourcesCallback = ( + extra: RequestHandlerExtra +) => ListResourcesResult | Promise; + +/** + * Callback to read a resource at a given URI, following a filled-in URI template. + */ +export type ReadResourceTemplateCallback = ( + uri: URL, + variables: Variables, + extra: RequestHandlerExtra +) => ReadResourceResult | Promise; + +/** + * A callback to complete one variable within a resource template's URI template. + */ +export type CompleteResourceTemplateCallback = ( + value: string, + context?: { + arguments?: Record; + } +) => string[] | Promise; + +/** + * A resource template combines a URI pattern with optional functionality to enumerate + * all resources matching that pattern. + */ +export class ResourceTemplate { + #uriTemplate: UriTemplate; + + constructor( + uriTemplate: string | UriTemplate, + private _callbacks: { + /** + * A callback to list all resources matching this template. + * This is required to be specified, even if `undefined`, to avoid accidentally forgetting resource listing. + */ + list: ListResourcesCallback | undefined; + + /** + * An optional callback to autocomplete variables within the URI template. + * Useful for clients and users to discover possible values. + */ + complete?: { + [variable: string]: CompleteResourceTemplateCallback; + }; + } + ) { + this.#uriTemplate = typeof uriTemplate === 'string' ? new UriTemplate(uriTemplate) : uriTemplate; + } + + /** + * Gets the URI template pattern. + */ + get uriTemplate(): UriTemplate { + return this.#uriTemplate; + } + + /** + * Gets the list callback, if one was provided. + */ + get listCallback(): ListResourcesCallback | undefined { + return this._callbacks.list; + } + + /** + * Gets the callback for completing a specific URI template variable, if one was provided. + */ + completeCallback(variable: string): CompleteResourceTemplateCallback | undefined { + return this._callbacks.complete?.[variable]; + } +} + +/** + * Protocol fields for ResourceTemplate, derived from the ResourceTemplateType protocol type. + * Note: The SDK ResourceTemplate class is separate from the protocol type. + */ +export type ResourceTemplateProtocolFields = Omit & { + resourceTemplate: ResourceTemplate; +}; + +/** + * Configuration for creating a RegisteredResourceTemplate. + * Combines protocol fields with SDK-specific callback. + */ +export type ResourceTemplateConfig = ResourceTemplateProtocolFields & { + readCallback: ReadResourceTemplateCallback; +}; + +/** + * A registered resource template in the MCP server. + * Provides methods to enable, disable, update, rename, and remove the resource template. + */ +export class RegisteredResourceTemplate { + // Protocol fields - stored together for easy spreading + #protocolFields: ResourceTemplateProtocolFields; + + // SDK-specific fields - separate from protocol + #readCallback: ReadResourceTemplateCallback; + #enabled: boolean = true; + + // Callbacks for McpServer communication + readonly #onUpdate: OnUpdate; + readonly #onRename: OnRename; + readonly #onRemove: OnRemove; + + constructor(config: ResourceTemplateConfig, onUpdate: OnUpdate, onRename: OnRename, onRemove: OnRemove) { + // Separate protocol fields from SDK fields + const { readCallback, ...protocolFields } = config; + this.#protocolFields = protocolFields; + this.#readCallback = readCallback; + + this.#onUpdate = onUpdate; + this.#onRename = onRename; + this.#onRemove = onRemove; + } + + // Protocol field getters (delegate to #protocolFields) + get name(): string { + return this.#protocolFields.name; + } + get title(): string | undefined { + return this.#protocolFields.title; + } + get description(): string | undefined { + return this.#protocolFields.description; + } + get mimeType(): string | undefined { + return this.#protocolFields.mimeType; + } + get icons(): Icon[] | undefined { + return this.#protocolFields.icons; + } + get annotations(): ResourceTemplateType['annotations'] | undefined { + return this.#protocolFields.annotations; + } + get _meta(): Record | undefined { + return this.#protocolFields._meta; + } + get resourceTemplate(): ResourceTemplate { + return this.#protocolFields.resourceTemplate; + } + + /** + * Gets the resource metadata (all fields except name and resourceTemplate). + */ + get metadata(): ResourceMetadata { + return { + title: this.#protocolFields.title, + description: this.#protocolFields.description, + mimeType: this.#protocolFields.mimeType, + icons: this.#protocolFields.icons, + annotations: this.#protocolFields.annotations, + _meta: this.#protocolFields._meta + }; + } + + // SDK-specific getters + get readCallback(): ReadResourceTemplateCallback { + return this.#readCallback; + } + get enabled(): boolean { + return this.#enabled; + } + + /** + * Enables the resource template. + * @returns this for chaining + */ + public enable(): this { + if (!this.#enabled) { + this.#enabled = true; + this.#onUpdate(); + } + return this; + } + + /** + * Disables the resource template. + * @returns this for chaining + */ + public disable(): this { + if (this.#enabled) { + this.#enabled = false; + this.#onUpdate(); + } + return this; + } + + /** + * Renames the resource template. + * @param newName - The new name for the resource template + * @returns this for chaining + */ + public rename(newName: string): this { + if (newName !== this.#protocolFields.name) { + const oldName = this.#protocolFields.name; + this.#protocolFields.name = newName; + this.#onRename(oldName, newName, this); + } + return this; + } + + /** + * Removes the resource template from the registry. + */ + public remove(): void { + this.#onRemove(this.#protocolFields.name); + } + + /** + * Updates the resource template's properties. + * @param updates - The properties to update + */ + public update( + updates: Partial & { + enabled?: boolean; + name?: string | null; + template?: ResourceTemplate; + callback?: ReadResourceTemplateCallback; + } + ): void { + const { + name: nameUpdate, + enabled: enabledUpdate, + template: templateUpdate, + readCallback: readCallbackUpdate, + callback: callbackUpdate, + resourceTemplate: resourceTemplateUpdate, + ...protocolUpdates + } = updates; + + // Handle name change (rename or remove) + if (nameUpdate !== undefined) { + if (nameUpdate === null) { + this.remove(); + return; + } + this.rename(nameUpdate); + } + + // Extract special fields, update protocol fields in one go + Object.assign(this.#protocolFields, protocolUpdates); + + // Handle template specially (maps to resourceTemplate in protocol fields) + if (templateUpdate !== undefined) { + this.#protocolFields.resourceTemplate = templateUpdate; + } + if (resourceTemplateUpdate !== undefined) { + this.#protocolFields.resourceTemplate = resourceTemplateUpdate; + } + + // Update SDK-specific fields (support both readCallback and callback) + if (readCallbackUpdate !== undefined) this.#readCallback = readCallbackUpdate; + if (callbackUpdate !== undefined) this.#readCallback = callbackUpdate; + + // Handle enabled (triggers its own notification) + if (enabledUpdate === undefined) { + this.#onUpdate(); + } else if (enabledUpdate) { + this.enable(); + } else { + this.disable(); + } + } + + /** + * Converts to the ResourceTemplate protocol type (for list responses). + */ + public toProtocolResourceTemplate(): ResourceTemplateType { + const { resourceTemplate, ...rest } = this.#protocolFields; + return { + ...rest, + uriTemplate: resourceTemplate.uriTemplate.toString() + }; + } +} diff --git a/packages/server/src/server/primitives/tool.ts b/packages/server/src/server/primitives/tool.ts new file mode 100644 index 000000000..b3a5e4970 --- /dev/null +++ b/packages/server/src/server/primitives/tool.ts @@ -0,0 +1,245 @@ +import type { + AnySchema, + CallToolResult, + Icon, + RequestHandlerExtra, + Result, + SchemaOutput, + ServerNotification, + ServerRequest, + ShapeOutput, + Tool, + ToolAnnotations, + ToolExecution, + ZodRawShapeCompat +} from '@modelcontextprotocol/core'; +import { normalizeObjectSchema, toJsonSchemaCompat, validateAndWarnToolName } from '@modelcontextprotocol/core'; + +import type { ToolTaskHandler } from '../../experimental/tasks/interfaces.js'; +import type { OnRemove, OnRename, OnUpdate } from './types.js'; + +/** + * Base callback type for tool handlers. + */ +export type BaseToolCallback< + SendResultT extends Result, + Extra extends RequestHandlerExtra, + Args extends undefined | ZodRawShapeCompat | AnySchema +> = Args extends ZodRawShapeCompat + ? (args: ShapeOutput, extra: Extra) => SendResultT | Promise + : Args extends AnySchema + ? (args: SchemaOutput, extra: Extra) => SendResultT | Promise + : (extra: Extra) => SendResultT | Promise; + +/** + * Callback for a tool handler registered with McpServer.registerTool(). + * + * Parameters will include tool arguments, if applicable, as well as other request handler context. + * + * The callback should return: + * - `structuredContent` if the tool has an outputSchema defined + * - `content` if the tool does not have an outputSchema + * - Both fields are optional but typically one should be provided + */ +export type ToolCallback = BaseToolCallback< + CallToolResult, + RequestHandlerExtra, + Args +>; + +/** + * Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object). + */ +export type AnyToolHandler = ToolCallback | ToolTaskHandler; + +/** + * Protocol fields for Tool, derived from the Tool type. + * Uses Zod schemas instead of JSON Schema (converted in toProtocolTool). + */ +export type ToolProtocolFields = Omit & { + inputSchema?: AnySchema; + outputSchema?: AnySchema; +}; + +/** + * Configuration for creating a RegisteredTool. + * Combines protocol fields with SDK-specific handler. + */ +export type ToolConfig = ToolProtocolFields & { + handler: AnyToolHandler; +}; + +const EMPTY_OBJECT_JSON_SCHEMA = { + type: 'object' as const, + properties: {} +}; + +/** + * A registered tool in the MCP server. + * Provides methods to enable, disable, update, rename, and remove the tool. + */ +export class RegisteredTool { + // Protocol fields - stored together for easy spreading + #protocolFields: ToolProtocolFields; + + // SDK-specific fields - separate from protocol + #handler: AnyToolHandler; + #enabled: boolean = true; + + // Callbacks for McpServer communication + readonly #onUpdate: OnUpdate; + readonly #onRename: OnRename; + readonly #onRemove: OnRemove; + + constructor(config: ToolConfig, onUpdate: OnUpdate, onRename: OnRename, onRemove: OnRemove) { + validateAndWarnToolName(config.name); + + // Separate protocol fields from SDK fields + const { handler, ...protocolFields } = config; + this.#protocolFields = protocolFields; + this.#handler = handler; + + this.#onUpdate = onUpdate; + this.#onRename = onRename; + this.#onRemove = onRemove; + } + + // Protocol field getters (delegate to #protocolFields) + get name(): string { + return this.#protocolFields.name; + } + get title(): string | undefined { + return this.#protocolFields.title; + } + get description(): string | undefined { + return this.#protocolFields.description; + } + get icons(): Icon[] | undefined { + return this.#protocolFields.icons; + } + get inputSchema(): AnySchema | undefined { + return this.#protocolFields.inputSchema; + } + get outputSchema(): AnySchema | undefined { + return this.#protocolFields.outputSchema; + } + get annotations(): ToolAnnotations | undefined { + return this.#protocolFields.annotations; + } + get execution(): ToolExecution | undefined { + return this.#protocolFields.execution; + } + get _meta(): Record | undefined { + return this.#protocolFields._meta; + } + + // SDK-specific getters + get handler(): AnyToolHandler { + return this.#handler; + } + get enabled(): boolean { + return this.#enabled; + } + + /** + * Enables the tool. + * @returns this for chaining + */ + public enable(): this { + if (!this.#enabled) { + this.#enabled = true; + this.#onUpdate(); + } + return this; + } + + /** + * Disables the tool. + * @returns this for chaining + */ + public disable(): this { + if (this.#enabled) { + this.#enabled = false; + this.#onUpdate(); + } + return this; + } + + /** + * Renames the tool. + * @param newName - The new name for the tool + * @returns this for chaining + */ + public rename(newName: string): this { + if (newName !== this.#protocolFields.name) { + validateAndWarnToolName(newName); + const oldName = this.#protocolFields.name; + this.#protocolFields.name = newName; + this.#onRename(oldName, newName, this); + } + return this; + } + + /** + * Removes the tool from the registry. + */ + public remove(): void { + this.#onRemove(this.#protocolFields.name); + } + + /** + * Updates the tool's properties. + * @param updates - The properties to update + */ + public update(updates: Partial & { enabled?: boolean; name?: string | null }): void { + const { name: nameUpdate, enabled: enabledUpdate, handler: handlerUpdate, ...protocolUpdates } = updates; + // Handle name change (rename or remove) + if (nameUpdate !== undefined) { + if (nameUpdate === null) { + this.remove(); + return; + } + this.rename(nameUpdate); + } + + // Extract special fields, update protocol fields in one go + Object.assign(this.#protocolFields, protocolUpdates); + + // Update SDK-specific fields + if (handlerUpdate !== undefined) this.#handler = handlerUpdate; + + // Handle enabled (triggers its own notification) + if (enabledUpdate === undefined) { + this.#onUpdate(); + } else if (enabledUpdate) { + this.enable(); + } else { + this.disable(); + } + } + + /** + * Converts to the Tool protocol type (for list responses). + * Converts Zod schemas to JSON Schema format. + */ + public toProtocolTool(): Tool { + return { + ...this.#protocolFields, + // Override schemas with JSON Schema conversion + inputSchema: (() => { + const obj = normalizeObjectSchema(this.#protocolFields.inputSchema); + return obj + ? (toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' }) as Tool['inputSchema']) + : EMPTY_OBJECT_JSON_SCHEMA; + })(), + outputSchema: this.#protocolFields.outputSchema + ? (() => { + const obj = normalizeObjectSchema(this.#protocolFields.outputSchema); + return obj + ? (toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'output' }) as Tool['outputSchema']) + : undefined; + })() + : undefined + }; + } +} diff --git a/packages/server/src/server/primitives/types.ts b/packages/server/src/server/primitives/types.ts new file mode 100644 index 000000000..a60da774b --- /dev/null +++ b/packages/server/src/server/primitives/types.ts @@ -0,0 +1,23 @@ +/** + * Shared callback types for registered primitives (tools, prompts, resources). + * These callbacks are passed to class constructors for McpServer communication. + */ + +/** + * Callback invoked when a registered item is updated (properties changed, enabled/disabled). + */ +export type OnUpdate = () => void; + +/** + * Callback invoked when a registered item is renamed. + * @param oldName - The previous name + * @param newName - The new name + * @param item - The item being renamed + */ +export type OnRename = (oldName: string, newName: string, item: T) => void; + +/** + * Callback invoked when a registered item is removed. + * @param name - The name of the item being removed + */ +export type OnRemove = (name: string) => void; diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index ded5fea55..8179dcac3 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -349,7 +349,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Update the tool tool.update({ - callback: async () => ({ + handler: async () => ({ content: [ { type: 'text', @@ -422,11 +422,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Update the tool with a different schema tool.update({ - paramsSchema: { + inputSchema: { name: z.string(), value: z.number() }, - callback: async ({ name, value }) => ({ + handler: async ({ name, value }) => ({ content: [ { type: 'text', @@ -520,7 +520,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { result: z.number(), sum: z.number() }, - callback: async () => ({ + handler: async () => ({ content: [{ type: 'text', text: '' }], structuredContent: { result: 42, @@ -605,7 +605,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Now update the tool tool.update({ - callback: async () => ({ + handler: async () => ({ content: [ { type: 'text',