From 4bad158ed0588af62e0df4e863a0e9a8f376b8c7 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 28 Mar 2025 16:34:30 +1100 Subject: [PATCH 1/6] Making a manual starting point for Claude to have a go --- package-lock.json | 4 ++-- src/server/mcp.ts | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73f1cbba..8338e3c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 8f4a909c..c031894c 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -43,6 +43,11 @@ import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; +type McpServerOptions = ServerOptions & { + render?: >(inserts: any, args: T) => void; + locked?: boolean; +} + /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying @@ -60,9 +65,11 @@ export class McpServer { } = {}; private _registeredTools: { [name: string]: RegisteredTool } = {}; private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; + private _options?: McpServerOptions - constructor(serverInfo: Implementation, options?: ServerOptions) { + constructor(serverInfo: Implementation, options?: McpServerOptions) { this.server = new Server(serverInfo, options); + this._options = options } /** @@ -87,7 +94,7 @@ export class McpServer { if (this._toolHandlersInitialized) { return; } - + this.server.assertCanSetRequestHandler( ListToolsRequestSchema.shape.method.value, ); @@ -96,7 +103,9 @@ export class McpServer { ); this.server.registerCapabilities({ - tools: {}, + tools: { + listChanged: true + }, }); this.server.setRequestHandler( @@ -285,7 +294,9 @@ export class McpServer { ); this.server.registerCapabilities({ - resources: {}, + resources: { + listChanged: true + }, }); this.server.setRequestHandler( @@ -366,7 +377,7 @@ export class McpServer { ); this.setCompletionRequestHandler(); - + this._resourceHandlersInitialized = true; } @@ -385,7 +396,9 @@ export class McpServer { ); this.server.registerCapabilities({ - prompts: {}, + prompts: { + listChanged: true + }, }); this.server.setRequestHandler( @@ -438,10 +451,25 @@ export class McpServer { ); this.setCompletionRequestHandler(); - + this._promptHandlersInitialized = true; } + /** + * These are called after a render() method has added, removed, or updated any prompts/resources/tools + */ + private emitPromptsChangedEvent(/* todo: how to track changes? */) { + // todo send notification event + } + + private emitResourcesChangedEvent(/* todo: how to track changes? */) { + // todo send notification event + } + + private emitToolsChangedEvent(/* todo: how to track changes? */) { + // todo send notification event + } + /** * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. */ From a0332a7c09d28e9a83fa449b85c4f1b7230efdd6 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 28 Mar 2025 17:10:53 +1100 Subject: [PATCH 2/6] Implemented render method with change detection --- src/server/mcp.ts | 274 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 251 insertions(+), 23 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index c031894c..c6155238 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -43,17 +43,24 @@ import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; -type McpServerOptions = ServerOptions & { - render?: >(inserts: any, args: T) => void; +type RenderApi = { + resource: McpServer["resource"]; + tool: McpServer["tool"]; + prompt: McpServer["prompt"]; +}; + +type McpServerOptions = Record> = + ServerOptions & { + render?: (api: RenderApi, args: T) => void | Promise; locked?: boolean; -} +}; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying * Server instance available via the `server` property. */ -export class McpServer { +export class McpServer = Record> { /** * The underlying Server instance, useful for advanced operations like sending notifications. */ @@ -65,11 +72,24 @@ export class McpServer { } = {}; private _registeredTools: { [name: string]: RegisteredTool } = {}; private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; - private _options?: McpServerOptions - constructor(serverInfo: Implementation, options?: McpServerOptions) { + private readonly _renderFunction?: ( + api: RenderApi, + args: RenderArgs, + ) => void | Promise; + private readonly _locked: boolean; + private _isFirstRender: boolean = true; + + constructor(serverInfo: Implementation, options?: McpServerOptions) { this.server = new Server(serverInfo, options); - this._options = options + this._renderFunction = options?.render; + this._locked = options?.locked ?? false; + + if (this._locked && !this._renderFunction) { + throw new Error( + "McpServer is locked, but no render function was provided. No resources, tools, or prompts can be registered.", + ); + } } /** @@ -456,27 +476,176 @@ export class McpServer { } /** - * These are called after a render() method has added, removed, or updated any prompts/resources/tools + * Clears existing resources, tools, and prompts, then runs the configured `render` function + * to define a new set based on the provided arguments. + * If the set of registered items changes compared to the previous state (and it's not the first render), + * appropriate `/listChanged` notifications are sent. + * + * @param args Arguments to pass to the configured `render` function. + * @throws Error if no `render` function was provided in the constructor options. */ - private emitPromptsChangedEvent(/* todo: how to track changes? */) { - // todo send notification event - } + async render(args: RenderArgs): Promise { + if (!this._renderFunction) { + throw new Error( + "Cannot call render(). No render function was provided during McpServer initialization.", + ); + } - private emitResourcesChangedEvent(/* todo: how to track changes? */) { - // todo send notification event - } + // --- 1. Prepare for new render --- + const newResources: { [uri: string]: RegisteredResource } = {}; + const newResourceTemplates: { [name: string]: RegisteredResourceTemplate } = + {}; + const newTools: { [name: string]: RegisteredTool } = {}; + const newPrompts: { [name: string]: RegisteredPrompt } = {}; + + // --- 2. Create temporary registration API for the render function --- + // These functions capture the definitions into the 'new*' objects above. + // They mirror the public API but don't check for locking or emit events immediately. + const renderApi: RenderApi = { + resource: ( + name: string, + uriOrTemplate: string | ResourceTemplate, + ...rest: unknown[] + ): void => { + let metadata: ResourceMetadata | undefined; + if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(rest[0] instanceof Function)) { + metadata = rest.shift() as ResourceMetadata; + } + + const readCallback = rest[0] as + | ReadResourceCallback + | ReadResourceTemplateCallback; + + if (typeof uriOrTemplate === "string") { + if (newResources[uriOrTemplate]) { + console.warn( + `Resource URI '${uriOrTemplate}' defined multiple times within the same render cycle. Last definition wins.`, + ); + } + newResources[uriOrTemplate] = { + name, + metadata, + readCallback: readCallback as ReadResourceCallback, + }; + } else { + if (newResourceTemplates[name]) { + console.warn( + `Resource template '${name}' defined multiple times within the same render cycle. Last definition wins.`, + ); + } + newResourceTemplates[name] = { + resourceTemplate: uriOrTemplate, + metadata, + readCallback: readCallback as ReadResourceTemplateCallback, + }; + } + }, + tool: (name: string, ...rest: unknown[]): void => { + if (newTools[name]) { + console.warn( + `Tool '${name}' defined multiple times within the same render cycle. Last definition wins.`, + ); + } + + let description: string | undefined; + if (typeof rest[0] === "string") { + description = rest.shift() as string; + } + + let paramsSchema: ZodRawShape | undefined; + // Check if the next item is an object but not the callback function + if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !(rest[0] instanceof Function)) { + paramsSchema = rest.shift() as ZodRawShape; + } + + const cb = rest[0] as ToolCallback; + newTools[name] = { + description, + inputSchema: + paramsSchema === undefined ? undefined : z.object(paramsSchema), + callback: cb, + }; + }, + prompt: (name: string, ...rest: unknown[]): void => { + if (newPrompts[name]) { + console.warn( + `Prompt '${name}' defined multiple times within the same render cycle. Last definition wins.`, + ); + } + + let description: string | undefined; + if (typeof rest[0] === "string") { + description = rest.shift() as string; + } + + let argsSchema: PromptArgsRawShape | undefined; + // Check if the next item is an object but not the callback function + if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !(rest[0] instanceof Function)) { + argsSchema = rest.shift() as PromptArgsRawShape; + } - private emitToolsChangedEvent(/* todo: how to track changes? */) { - // todo send notification event + + const cb = rest[0] as PromptCallback< + PromptArgsRawShape | undefined + >; + newPrompts[name] = { + description, + argsSchema: + argsSchema === undefined ? undefined : z.object(argsSchema), + callback: cb, + }; + }, + }; + + // --- 3. Execute the user's render function --- + this._renderFunction(renderApi, args) + + // --- 4. Compare old state with new state --- + const toolsChanged = haveKeysChanged(this._registeredTools, newTools); + const promptsChanged = haveKeysChanged(this._registeredPrompts, newPrompts); + const resourcesChanged = haveKeysChanged( + { + ...this._registeredResources, + ...mapKeys(this._registeredResourceTemplates, (t) => t.resourceTemplate.uriTemplate.toString()) // Use template URI for comparison consistency if needed, or just name + }, + { + ...newResources, + ...mapKeys(newResourceTemplates, (t) => t.resourceTemplate.uriTemplate.toString()) + } + ) || haveKeysChanged(this._registeredResourceTemplates, newResourceTemplates); // Also check template names directly + + // --- 5. Always update internal state (currently we're not emitting events for changes in parameters or descriptions + // of tools, but we should at least store the new values + this._registeredTools = newTools; + this._registeredPrompts = newPrompts; + this._registeredResources = newResources; + this._registeredResourceTemplates = newResourceTemplates; + + // Ensure handlers are set up + this.setToolRequestHandlers(); + this.setPromptRequestHandlers(); + this.setResourceRequestHandlers(); + + // Emit change events only if state *actually* changed and it's not the first render + if (!this._isFirstRender) { + if (toolsChanged) this.server.sendToolListChanged() + if (promptsChanged) this.server.sendPromptListChanged() + if (resourcesChanged) this.server.sendResourceListChanged() + } + + // --- 6. Mark first render as complete --- + this._isFirstRender = false; } /** * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. + * @throws Error if the server is locked. */ resource(name: string, uri: string, readCallback: ReadResourceCallback): void; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. + * @throws Error if the server is locked. */ resource( name: string, @@ -487,6 +656,7 @@ export class McpServer { /** * Registers a resource `name` with a template pattern, which will use the given callback to respond to read requests. + * @throws Error if the server is locked. */ resource( name: string, @@ -496,6 +666,7 @@ export class McpServer { /** * Registers a resource `name` with a template pattern and metadata, which will use the given callback to respond to read requests. + * @throws Error if the server is locked. */ resource( name: string, @@ -509,9 +680,16 @@ export class McpServer { uriOrTemplate: string | ResourceTemplate, ...rest: unknown[] ): void { + if (this._locked) { + throw new Error( + "Server is locked. Resources can only be registered via the render() method.", + ); + } + let metadata: ResourceMetadata | undefined; - if (typeof rest[0] === "object") { - metadata = rest.shift() as ResourceMetadata; + // Check if the first rest arg is metadata (object, not function) + if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(rest[0] instanceof Function)) { + metadata = rest.shift() as ResourceMetadata; } const readCallback = rest[0] as @@ -540,21 +718,25 @@ export class McpServer { }; } - this.setResourceRequestHandlers(); + this.setResourceRequestHandlers() + this.server.sendResourceListChanged() } /** * Registers a zero-argument tool `name`, which will run the given function when the client calls it. + * @throws Error if the server is locked. */ tool(name: string, cb: ToolCallback): void; /** * Registers a zero-argument tool `name` (with a description) which will run the given function when the client calls it. + * @throws Error if the server is locked. */ tool(name: string, description: string, cb: ToolCallback): void; /** * Registers a tool `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. + * @throws Error if the server is locked. */ tool( name: string, @@ -564,6 +746,7 @@ export class McpServer { /** * Registers a tool `name` (with a description) accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. + * @throws Error if the server is locked. */ tool( name: string, @@ -573,8 +756,10 @@ export class McpServer { ): void; tool(name: string, ...rest: unknown[]): void { - if (this._registeredTools[name]) { - throw new Error(`Tool ${name} is already registered`); + if (this._locked) { + throw new Error( + "Server is locked. Tools can only be registered via the render() method.", + ); } let description: string | undefined; @@ -596,20 +781,24 @@ export class McpServer { }; this.setToolRequestHandlers(); + this.server.sendToolListChanged(); } /** * Registers a zero-argument prompt `name`, which will run the given function when the client calls it. + * @throws Error if the server is locked. */ prompt(name: string, cb: PromptCallback): void; /** * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. + * @throws Error if the server is locked. */ prompt(name: string, description: string, cb: PromptCallback): void; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. + * @throws Error if the server is locked. */ prompt( name: string, @@ -619,6 +808,7 @@ export class McpServer { /** * Registers a prompt `name` (with a description) accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. + * @throws Error if the server is locked. */ prompt( name: string, @@ -628,8 +818,10 @@ export class McpServer { ): void; prompt(name: string, ...rest: unknown[]): void { - if (this._registeredPrompts[name]) { - throw new Error(`Prompt ${name} is already registered`); + if (this._locked) { + throw new Error( + "Server is locked. Prompts can only be registered via the render() method.", + ); } let description: string | undefined; @@ -650,9 +842,45 @@ export class McpServer { }; this.setPromptRequestHandlers(); + this.server.sendPromptListChanged() + } +} + +// --- Helper Function for Change Detection --- + +/** Checks if the keys of two objects are different. */ +function haveKeysChanged(oldObj: object, newObj: object): boolean { + const oldKeys = Object.keys(oldObj).sort(); + const newKeys = Object.keys(newObj).sort(); + + if (oldKeys.length !== newKeys.length) { + return true; + } + + for (let i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] !== newKeys[i]) { + return true; + } } + + return false; } +/** Helper to map object keys while preserving values. */ +function mapKeys(obj: Record, keyMapper: (value: V, key: string) => string): Record { + const result: Record = {}; + for(const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const newKey = keyMapper(obj[key], key); + result[newKey] = obj[key]; + } + } + return result; +} + + +// --- Constants and Type Definitions (mostly unchanged) --- + /** * A callback to complete one variable within a resource template's URI template. */ From ae54ebd54ea682f8330fc241f273ca7b962c93cd Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 28 Mar 2025 17:29:52 +1100 Subject: [PATCH 3/6] Removing duplication of addTool/Resource/Prompt logic between render method and accessors --- src/server/mcp.ts | 282 +++++++++++++++++----------------------------- 1 file changed, 103 insertions(+), 179 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index c6155238..7799d837 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1,47 +1,37 @@ -import { Server, ServerOptions } from "./index.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { Server, ServerOptions } from './index.js' +import { zodToJsonSchema } from 'zod-to-json-schema' +import { AnyZodObject, z, ZodObject, ZodOptional, ZodRawShape, ZodString, ZodType, ZodTypeAny, ZodTypeDef } from 'zod' import { - z, - ZodRawShape, - ZodObject, - ZodString, - AnyZodObject, - ZodTypeAny, - ZodType, - ZodTypeDef, - ZodOptional, -} from "zod"; -import { - Implementation, - Tool, - ListToolsResult, + CallToolRequestSchema, CallToolResult, - McpError, - ErrorCode, CompleteRequest, + CompleteRequestSchema, CompleteResult, - PromptReference, - ResourceReference, - Resource, + ErrorCode, + GetPromptRequestSchema, + GetPromptResult, + Implementation, + ListPromptsRequestSchema, + ListPromptsResult, + ListResourcesRequestSchema, ListResourcesResult, ListResourceTemplatesRequestSchema, - ReadResourceRequestSchema, ListToolsRequestSchema, - CallToolRequestSchema, - ListResourcesRequestSchema, - ListPromptsRequestSchema, - GetPromptRequestSchema, - CompleteRequestSchema, - ListPromptsResult, + ListToolsResult, + McpError, Prompt, PromptArgument, - GetPromptResult, + PromptReference, + ReadResourceRequestSchema, ReadResourceResult, -} from "../types.js"; -import { Completable, CompletableDef } from "./completable.js"; -import { UriTemplate, Variables } from "../shared/uriTemplate.js"; -import { RequestHandlerExtra } from "../shared/protocol.js"; -import { Transport } from "../shared/transport.js"; + Resource, + ResourceReference, + Tool +} from '../types.js' +import { Completable, CompletableDef } from './completable.js' +import { UriTemplate, Variables } from '../shared/uriTemplate.js' +import { RequestHandlerExtra } from '../shared/protocol.js' +import { Transport } from '../shared/transport.js' type RenderApi = { resource: McpServer["resource"]; @@ -507,93 +497,13 @@ export class McpServer = Record { - let metadata: ResourceMetadata | undefined; - if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(rest[0] instanceof Function)) { - metadata = rest.shift() as ResourceMetadata; - } - - const readCallback = rest[0] as - | ReadResourceCallback - | ReadResourceTemplateCallback; - - if (typeof uriOrTemplate === "string") { - if (newResources[uriOrTemplate]) { - console.warn( - `Resource URI '${uriOrTemplate}' defined multiple times within the same render cycle. Last definition wins.`, - ); - } - newResources[uriOrTemplate] = { - name, - metadata, - readCallback: readCallback as ReadResourceCallback, - }; - } else { - if (newResourceTemplates[name]) { - console.warn( - `Resource template '${name}' defined multiple times within the same render cycle. Last definition wins.`, - ); - } - newResourceTemplates[name] = { - resourceTemplate: uriOrTemplate, - metadata, - readCallback: readCallback as ReadResourceTemplateCallback, - }; - } + addResources(newResources, newResourceTemplates, name, uriOrTemplate, ...rest) }, tool: (name: string, ...rest: unknown[]): void => { - if (newTools[name]) { - console.warn( - `Tool '${name}' defined multiple times within the same render cycle. Last definition wins.`, - ); - } - - let description: string | undefined; - if (typeof rest[0] === "string") { - description = rest.shift() as string; - } - - let paramsSchema: ZodRawShape | undefined; - // Check if the next item is an object but not the callback function - if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !(rest[0] instanceof Function)) { - paramsSchema = rest.shift() as ZodRawShape; - } - - const cb = rest[0] as ToolCallback; - newTools[name] = { - description, - inputSchema: - paramsSchema === undefined ? undefined : z.object(paramsSchema), - callback: cb, - }; + addTool(newTools, name, ...rest) }, prompt: (name: string, ...rest: unknown[]): void => { - if (newPrompts[name]) { - console.warn( - `Prompt '${name}' defined multiple times within the same render cycle. Last definition wins.`, - ); - } - - let description: string | undefined; - if (typeof rest[0] === "string") { - description = rest.shift() as string; - } - - let argsSchema: PromptArgsRawShape | undefined; - // Check if the next item is an object but not the callback function - if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !(rest[0] instanceof Function)) { - argsSchema = rest.shift() as PromptArgsRawShape; - } - - - const cb = rest[0] as PromptCallback< - PromptArgsRawShape | undefined - >; - newPrompts[name] = { - description, - argsSchema: - argsSchema === undefined ? undefined : z.object(argsSchema), - callback: cb, - }; + addPrompt(newPrompts, name, ...rest) }, }; @@ -686,37 +596,7 @@ export class McpServer = Record 1 && typeof rest[0] === "object" && rest[0] !== null && !(rest[0] instanceof Function)) { - metadata = rest.shift() as ResourceMetadata; - } - - const readCallback = rest[0] as - | ReadResourceCallback - | ReadResourceTemplateCallback; - - if (typeof uriOrTemplate === "string") { - if (this._registeredResources[uriOrTemplate]) { - throw new Error(`Resource ${uriOrTemplate} is already registered`); - } - - this._registeredResources[uriOrTemplate] = { - name, - metadata, - readCallback: readCallback as ReadResourceCallback, - }; - } else { - if (this._registeredResourceTemplates[name]) { - throw new Error(`Resource template ${name} is already registered`); - } - - this._registeredResourceTemplates[name] = { - resourceTemplate: uriOrTemplate, - metadata, - readCallback: readCallback as ReadResourceTemplateCallback, - }; - } + addResources(this._registeredResources, this._registeredResourceTemplates, name, uriOrTemplate, ...rest) this.setResourceRequestHandlers() this.server.sendResourceListChanged() @@ -762,23 +642,7 @@ export class McpServer = Record 1) { - paramsSchema = rest.shift() as ZodRawShape; - } - - const cb = rest[0] as ToolCallback; - this._registeredTools[name] = { - description, - inputSchema: - paramsSchema === undefined ? undefined : z.object(paramsSchema), - callback: cb, - }; + addTool(this._registeredTools, name, ...rest) this.setToolRequestHandlers(); this.server.sendToolListChanged(); @@ -824,26 +688,86 @@ export class McpServer = Record 1 && typeof rest[0] === "object" && rest[0] !== null && !(rest[0] instanceof Function)) { + metadata = rest.shift() as ResourceMetadata; + } + + const readCallback = rest[0] as + | ReadResourceCallback + | ReadResourceTemplateCallback; + + if (typeof uriOrTemplate === "string") { + if (resources[uriOrTemplate]) { + throw new Error(`Resource ${uriOrTemplate} is already registered`); } - let argsSchema: PromptArgsRawShape | undefined; - if (rest.length > 1) { - argsSchema = rest.shift() as PromptArgsRawShape; + resources[uriOrTemplate] = { + name, + metadata, + readCallback: readCallback as ReadResourceCallback, + }; + } else { + if (resourceTemplates[name]) { + throw new Error(`Resource template ${name} is already registered`); } - const cb = rest[0] as PromptCallback; - this._registeredPrompts[name] = { - description, - argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), - callback: cb, + resourceTemplates[name] = { + resourceTemplate: uriOrTemplate, + metadata, + readCallback: readCallback as ReadResourceTemplateCallback, }; + } +} - this.setPromptRequestHandlers(); - this.server.sendPromptListChanged() +function addTool(tools: { [p: string]: RegisteredTool }, name: string, ...rest: unknown[]) { + let description: string | undefined; + if (typeof rest[0] === "string") { + description = rest.shift() as string; + } + + let paramsSchema: ZodRawShape | undefined; + if (rest.length > 1) { + paramsSchema = rest.shift() as ZodRawShape; + } + + const cb = rest[0] as ToolCallback; + tools[name] = { + description, + inputSchema: + paramsSchema === undefined ? undefined : z.object(paramsSchema), + callback: cb, + }; +} + +function addPrompt(prompts: { [p: string]: RegisteredPrompt }, name: string, ...rest: unknown[]) { + let description: string | undefined; + if (typeof rest[0] === "string") { + description = rest.shift() as string; + } + + let argsSchema: PromptArgsRawShape | undefined; + if (rest.length > 1) { + argsSchema = rest.shift() as PromptArgsRawShape; } + + const cb = rest[0] as PromptCallback; + prompts[name] = { + description, + argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), + callback: cb, + }; } // --- Helper Function for Change Detection --- @@ -879,7 +803,7 @@ function mapKeys(obj: Record, keyMapper: (value: V, key: string) = } -// --- Constants and Type Definitions (mostly unchanged) --- +// --- Constants and Type Definitions --- /** * A callback to complete one variable within a resource template's URI template. From 878be70511a5ed84774d73054f118712ee85a19a Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 28 Mar 2025 21:10:41 +1100 Subject: [PATCH 4/6] no need for a separate config for locked servers, imo the presence of the render function is enough --- src/server/mcp.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7799d837..fe280c02 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -42,7 +42,6 @@ type RenderApi = { type McpServerOptions = Record> = ServerOptions & { render?: (api: RenderApi, args: T) => void | Promise; - locked?: boolean; }; /** @@ -73,13 +72,7 @@ export class McpServer = Record) { this.server = new Server(serverInfo, options); this._renderFunction = options?.render; - this._locked = options?.locked ?? false; - - if (this._locked && !this._renderFunction) { - throw new Error( - "McpServer is locked, but no render function was provided. No resources, tools, or prompts can be registered.", - ); - } + this._locked = Boolean(this._renderFunction); } /** From 15c1c97dc6052c9cba7a14c54b21ee20254a647d Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 28 Mar 2025 21:20:03 +1100 Subject: [PATCH 5/6] can use the presence of a connection instead of a first render flag --- src/server/mcp.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index fe280c02..6d57f779 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -39,8 +39,7 @@ type RenderApi = { prompt: McpServer["prompt"]; }; -type McpServerOptions = Record> = - ServerOptions & { +type McpServerOptions = ServerOptions & { render?: (api: RenderApi, args: T) => void | Promise; }; @@ -49,7 +48,7 @@ type McpServerOptions = Record> = * For advanced usage (like sending notifications or setting custom request handlers), use the underlying * Server instance available via the `server` property. */ -export class McpServer = Record> { +export class McpServer | undefined = undefined> { /** * The underlying Server instance, useful for advanced operations like sending notifications. */ @@ -67,7 +66,7 @@ export class McpServer = Record void | Promise; private readonly _locked: boolean; - private _isFirstRender: boolean = true; + private _isConnected: boolean = true; constructor(serverInfo: Implementation, options?: McpServerOptions) { this.server = new Server(serverInfo, options); @@ -81,6 +80,7 @@ export class McpServer = Record { + this._isConnected = true return await this.server.connect(transport); } @@ -529,15 +529,12 @@ export class McpServer = Record Date: Fri, 28 Mar 2025 21:39:57 +1100 Subject: [PATCH 6/6] unmangled imports --- src/server/mcp.ts | 58 +++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 6d57f779..d4382155 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1,37 +1,47 @@ -import { Server, ServerOptions } from './index.js' -import { zodToJsonSchema } from 'zod-to-json-schema' -import { AnyZodObject, z, ZodObject, ZodOptional, ZodRawShape, ZodString, ZodType, ZodTypeAny, ZodTypeDef } from 'zod' +import { Server, ServerOptions } from "./index.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { - CallToolRequestSchema, + z, + ZodRawShape, + ZodObject, + ZodString, + AnyZodObject, + ZodTypeAny, + ZodType, + ZodTypeDef, + ZodOptional, +} from "zod"; +import { + Implementation, + Tool, + ListToolsResult, CallToolResult, + McpError, + ErrorCode, CompleteRequest, - CompleteRequestSchema, CompleteResult, - ErrorCode, - GetPromptRequestSchema, - GetPromptResult, - Implementation, - ListPromptsRequestSchema, - ListPromptsResult, - ListResourcesRequestSchema, + PromptReference, + ResourceReference, + Resource, ListResourcesResult, ListResourceTemplatesRequestSchema, + ReadResourceRequestSchema, ListToolsRequestSchema, - ListToolsResult, - McpError, + CallToolRequestSchema, + ListResourcesRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, + CompleteRequestSchema, + ListPromptsResult, Prompt, PromptArgument, - PromptReference, - ReadResourceRequestSchema, + GetPromptResult, ReadResourceResult, - Resource, - ResourceReference, - Tool -} from '../types.js' -import { Completable, CompletableDef } from './completable.js' -import { UriTemplate, Variables } from '../shared/uriTemplate.js' -import { RequestHandlerExtra } from '../shared/protocol.js' -import { Transport } from '../shared/transport.js' +} from "../types.js"; +import { Completable, CompletableDef } from "./completable.js"; +import { UriTemplate, Variables } from "../shared/uriTemplate.js"; +import { RequestHandlerExtra } from "../shared/protocol.js"; +import { Transport } from "../shared/transport.js"; type RenderApi = { resource: McpServer["resource"];