From 2dbba419edb281eef045a9fd96f8ac296598d897 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 01:45:57 +0200 Subject: [PATCH 1/3] move registered primitives to classes --- CLAUDE.md | 3 +- docs/migration-SKILL.md | 162 ++++-- docs/migration.md | 139 +++-- .../src/experimental/tasks/interfaces.ts | 2 +- .../src/experimental/tasks/mcpServer.ts | 3 +- packages/server/src/index.ts | 1 + packages/server/src/server/mcp.ts | 536 +++++------------- .../server/src/server/primitives/index.ts | 41 ++ .../server/src/server/primitives/prompt.ts | 222 ++++++++ .../server/src/server/primitives/resource.ts | 210 +++++++ .../src/server/primitives/resourceTemplate.ts | 295 ++++++++++ packages/server/src/server/primitives/tool.ts | 245 ++++++++ .../server/src/server/primitives/types.ts | 23 + test/integration/test/server/mcp.test.ts | 10 +- 14 files changed, 1378 insertions(+), 514 deletions(-) create mode 100644 packages/server/src/server/primitives/index.ts create mode 100644 packages/server/src/server/primitives/prompt.ts create mode 100644 packages/server/src/server/primitives/resource.ts create mode 100644 packages/server/src/server/primitives/resourceTemplate.ts create mode 100644 packages/server/src/server/primitives/tool.ts create mode 100644 packages/server/src/server/primitives/types.ts diff --git a/CLAUDE.md b/CLAUDE.md index da47e4072..15aa1fee7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,8 +29,7 @@ When making breaking changes, document them in **both**: - `docs/migration.md` — human-readable guide with before/after code examples - `docs/migration-SKILL.md` — LLM-optimized mapping tables for mechanical migration -Include what changed, why, and how to migrate. Search for related sections and group -related changes together rather than adding new standalone sections. +Include what changed, why, and how to migrate. Search for related sections and group related changes together rather than adding new standalone sections. ## Code Style Guidelines diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 66db6d3dc..631bd45d6 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -20,13 +20,13 @@ Remove the old package and install only what you need: npm uninstall @modelcontextprotocol/sdk ``` -| You need | Install | -|----------|---------| -| Client only | `npm install @modelcontextprotocol/client` | -| Server only | `npm install @modelcontextprotocol/server` | -| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` | -| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | -| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | +| You need | Install | +| --------------------- | ------------------------------------------------------------------------ | +| Client only | `npm install @modelcontextprotocol/client` | +| Server only | `npm install @modelcontextprotocol/server` | +| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` | +| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | +| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | `@modelcontextprotocol/core` is installed automatically as a dependency. @@ -36,65 +36,67 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. ### Client imports -| v1 import path | v2 package | -|----------------|------------| -| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` | +| v1 import path | v2 package | +| ---------------------------------------------------- | ------------------------------ | +| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` | | `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/websocket.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/websocket.js` | `@modelcontextprotocol/client` | ### Server imports -| v1 import path | v2 package | -|----------------|------------| -| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` | +| v1 import path | v2 package | +| ---------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) | -| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) | -| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) | -| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) | +| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) | +| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) | +| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) | ### Types / shared imports -| v1 import path | v2 package | -|----------------|------------| -| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/core` | -| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/core` | -| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/core` | -| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/core` | +| v1 import path | v2 package | +| ------------------------------------------------- | ---------------------------- | +| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/core` | +| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/core` | +| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/core` | +| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/core` | | `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/core` | -| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/core` | +| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/core` | Notes: + - `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export everything from `@modelcontextprotocol/core`, so you can import types from whichever package you already depend on. - When multiple v1 imports map to the same v2 package, consolidate them into a single import statement. - If code imports from `sdk/client/...`, install `@modelcontextprotocol/client`. If from `sdk/server/...`, install `@modelcontextprotocol/server`. If from `sdk/types.js` or `sdk/shared/...` only, install `@modelcontextprotocol/core`. ## 4. Renamed Symbols -| v1 symbol | v2 symbol | v2 package | -|-----------|-----------|------------| +| v1 symbol | v2 symbol | v2 package | +| ------------------------------- | ----------------------------------- | ---------------------------- | | `StreamableHTTPServerTransport` | `NodeStreamableHTTPServerTransport` | `@modelcontextprotocol/node` | ## 5. Removed / Renamed Type Aliases and Symbols -| v1 (removed) | v2 (replacement) | -|--------------|------------------| -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | -| `isJSONRPCResponse` | `isJSONRPCResultResponse` | -| `ResourceReference` | `ResourceTemplateReference` | -| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | -| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | +| v1 (removed) | v2 (replacement) | +| ---------------------------------------- | ------------------------------------------------ | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` | `isJSONRPCResultResponse` | +| `ResourceReference` | `ResourceTemplateReference` | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) | All other symbols from `@modelcontextprotocol/sdk/types.js` retain their original names (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, etc.). -**Unchanged APIs** (only import paths changed): `Client` constructor and methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all Zod schemas, all callback return types. +**Unchanged APIs** (only import paths changed): `Client` constructor and methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all Zod +schemas, all callback return types. ## 6. McpServer API Changes @@ -105,21 +107,25 @@ The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the ```typescript // v1: server.tool(name, schema, callback) server.tool('greet', { name: z.string() }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // v1: server.tool(name, description, schema, callback) server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // v2: server.registerTool(name, config, callback) -server.registerTool('greet', { - description: 'Greet a user', - inputSchema: { name: z.string() }, -}, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; -}); +server.registerTool( + 'greet', + { + description: 'Greet a user', + inputSchema: { name: z.string() } + }, + async ({ name }) => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + } +); ``` Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`, `annotations?`, `_meta?` @@ -129,15 +135,19 @@ Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`, ```typescript // v1: server.prompt(name, schema, callback) server.prompt('summarize', { text: z.string() }, async ({ text }) => { - return { messages: [{ role: 'user', content: { type: 'text', text } }] }; + return { messages: [{ role: 'user', content: { type: 'text', text } }] }; }); // v2: server.registerPrompt(name, config, callback) -server.registerPrompt('summarize', { - argsSchema: { text: z.string() }, -}, async ({ text }) => { - return { messages: [{ role: 'user', content: { type: 'text', text } }] }; -}); +server.registerPrompt( + 'summarize', + { + argsSchema: { text: z.string() } + }, + async ({ text }) => { + return { messages: [{ role: 'user', content: { type: 'text', text } }] }; + } +); ``` Config object fields: `title?`, `description?`, `argsSchema?` @@ -146,13 +156,13 @@ Config object fields: `title?`, `description?`, `argsSchema?` ```typescript // v1: server.resource(name, uri, callback) -server.resource('config', 'config://app', async (uri) => { - return { contents: [{ uri: uri.href, text: '{}' }] }; +server.resource('config', 'config://app', async uri => { + return { contents: [{ uri: uri.href, text: '{}' }] }; }); // v2: server.registerResource(name, uri, metadata, callback) -server.registerResource('config', 'config://app', {}, async (uri) => { - return { contents: [{ uri: uri.href, text: '{}' }] }; +server.registerResource('config', 'config://app', {}, async uri => { + return { contents: [{ uri: uri.href, text: '{}' }] }; }); ``` @@ -180,7 +190,8 @@ extra.requestInfo?.headers.get('mcp-session-id') ### Server-side auth -All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See `examples/server/src/` for demos. +All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See +`examples/server/src/` for demos. ### Host header validation (Express) @@ -198,11 +209,38 @@ app.use(hostHeaderValidation(['example.com'])); The server package now exports framework-agnostic alternatives: `validateHostHeader()`, `localhostAllowedHostnames()`, `hostHeaderValidationResponse()`. -## 9. Client Behavioral Changes +## 9. 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 }); +``` + +New getter methods on `McpServer`: + +| Getter | Returns | +| ----------------------------- | ------------------------------------------------- | +| `mcpServer.tools` | `ReadonlyMap` | +| `mcpServer.prompts` | `ReadonlyMap` | +| `mcpServer.resources` | `ReadonlyMap` | +| `mcpServer.resourceTemplates` | `ReadonlyMap` | + +## 10. 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. -## 10. Migration Steps (apply in this order) +## 11. 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 9bbb25fc2..2d68f8687 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -4,7 +4,8 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD ## Overview -Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, `@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. +Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, +`@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. ## Breaking Changes @@ -12,11 +13,11 @@ Version 2 of the MCP TypeScript SDK introduces several breaking changes to impro The single `@modelcontextprotocol/sdk` package has been split into three packages: -| v1 | v2 | -|----|-----| +| v1 | v2 | +| --------------------------- | ---------------------------------------------------------- | | `@modelcontextprotocol/sdk` | `@modelcontextprotocol/core` (types, protocol, transports) | -| | `@modelcontextprotocol/client` (client implementation) | -| | `@modelcontextprotocol/server` (server implementation) | +| | `@modelcontextprotocol/client` (client implementation) | +| | `@modelcontextprotocol/server` (server implementation) | Remove the old package and install only the packages you need: @@ -64,6 +65,7 @@ Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re- v2 requires **Node.js 20+** and ships **ESM only** (no more CommonJS builds). If your project uses CommonJS (`require()`), you will need to either: + - Migrate to ESM (`import`/`export`) - Use dynamic `import()` to load the SDK @@ -71,11 +73,11 @@ If your project uses CommonJS (`require()`), you will need to either: The server package no longer depends on Express or Hono. HTTP framework integrations are now separate middleware packages: -| v1 | v2 | -|----|-----| +| v1 | v2 | +| -------------------------------------- | ------------------------------------------- | | Built into `@modelcontextprotocol/sdk` | `@modelcontextprotocol/node` (Node.js HTTP) | -| | `@modelcontextprotocol/express` (Express) | -| | `@modelcontextprotocol/hono` (Hono) | +| | `@modelcontextprotocol/express` (Express) | +| | `@modelcontextprotocol/hono` (Hono) | Install the middleware package for your framework: @@ -128,12 +130,12 @@ This affects both transport constructors and request handler code that reads hea ```typescript // Transport headers const transport = new StreamableHTTPClientTransport(url, { - requestInit: { - headers: { - 'Authorization': 'Bearer token', - 'X-Custom': 'value', - }, - }, + requestInit: { + headers: { + Authorization: 'Bearer token', + 'X-Custom': 'value' + } + } }); // Reading headers in a request handler @@ -145,12 +147,12 @@ const sessionId = extra.requestInfo?.headers['mcp-session-id']; ```typescript // Transport headers const transport = new StreamableHTTPClientTransport(url, { - requestInit: { - headers: new Headers({ - 'Authorization': 'Bearer token', - 'X-Custom': 'value', - }), - }, + requestInit: { + headers: new Headers({ + Authorization: 'Bearer token', + 'X-Custom': 'value' + }) + } }); // Reading headers in a request handler @@ -170,22 +172,22 @@ const server = new McpServer({ name: 'demo', version: '1.0.0' }); // Tool with schema server.tool('greet', { name: z.string() }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // Tool with description server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // Prompt server.prompt('summarize', { text: z.string() }, async ({ text }) => { - return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] }; + return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] }; }); // Resource -server.resource('config', 'config://app', async (uri) => { - return { contents: [{ uri: uri.href, text: '{}' }] }; +server.resource('config', 'config://app', async uri => { + return { contents: [{ uri: uri.href, text: '{}' }] }; }); ``` @@ -198,28 +200,29 @@ const server = new McpServer({ name: 'demo', version: '1.0.0' }); // Tool with schema server.registerTool('greet', { inputSchema: { name: z.string() } }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // Tool with description server.registerTool('greet', { description: 'Greet a user', inputSchema: { name: z.string() } }, async ({ name }) => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); // Prompt server.registerPrompt('summarize', { argsSchema: { text: z.string() } }, async ({ text }) => { - return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] }; + return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] }; }); // Resource -server.registerResource('config', 'config://app', {}, async (uri) => { - return { contents: [{ uri: uri.href, text: '{}' }] }; +server.registerResource('config', 'config://app', {}, async uri => { + return { contents: [{ uri: uri.href, text: '{}' }] }; }); ``` ### Host header validation moved -Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`, `hostHeaderValidationResponse()`. +Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`, +`hostHeaderValidationResponse()`. **Before (v1):** @@ -237,6 +240,48 @@ app.use(hostHeaderValidation(['example.com'])); Note: the v2 signature takes a plain `string[]` instead of an options object. +### 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 +}); +``` + +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. @@ -244,24 +289,27 @@ Note: the v2 signature takes a plain `string[]` instead of an options object. To restore v1 behavior (throw an error when capabilities are missing), set `enforceStrictCapabilities: true`: ```typescript -const client = new Client({ name: 'my-client', version: '1.0.0' }, { - enforceStrictCapabilities: true, -}); +const client = new Client( + { name: 'my-client', version: '1.0.0' }, + { + enforceStrictCapabilities: true + } +); ``` ### Removed type aliases and deprecated exports The following deprecated type aliases have been removed from `@modelcontextprotocol/core`: -| Removed | Replacement | -|---------|-------------| -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | -| `isJSONRPCResponse` | `isJSONRPCResultResponse` | -| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | -| `ResourceReference` | `ResourceTemplateReference` | -| `IsomorphicHeaders` | Use Web Standard `Headers` | +| Removed | Replacement | +| ---------------------------------------- | ------------------------------------------------ | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` | `isJSONRPCResultResponse` | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `ResourceReference` | `ResourceTemplateReference` | +| `IsomorphicHeaders` | Use Web Standard `Headers` | | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) | All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names in `@modelcontextprotocol/core`. @@ -292,7 +340,8 @@ The following APIs are unchanged between v1 and v2 (only the import paths change ## Using an LLM to migrate your code -An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as a skill. +An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as +a skill. ## Need Help? 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 975cca257..de0e2f392 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 { @@ -43,8 +35,6 @@ import { getObjectShape, getParseErrorMessage, GetPromptRequestSchema, - getSchemaDescription, - isSchemaOptional, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, @@ -53,16 +43,24 @@ import { normalizeObjectSchema, objectFromShape, ReadResourceRequestSchema, - 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'; @@ -105,6 +103,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. * @@ -140,39 +170,9 @@ export class McpServer { this.server.setRequestHandler( ListToolsRequestSchema, (): 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()) }) ); @@ -500,14 +500,10 @@ export class McpServer { } }); - this.server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => { - const resources = Object.entries(this._registeredResources) - .filter(([_, resource]) => resource.enabled) - .map(([uri, resource]) => ({ - uri, - name: resource.name, - ...resource.metadata - })); + this.server.setRequestHandler(ListResourcesRequestSchema, async (_request, extra) => { + 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)) { @@ -529,11 +525,9 @@ export class McpServer { }); this.server.setRequestHandler(ListResourceTemplatesRequestSchema, 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 }; }); @@ -583,16 +577,9 @@ export class McpServer { this.server.setRequestHandler( ListPromptsRequestSchema, (): 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()) }) ); @@ -687,30 +674,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( @@ -720,29 +704,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; @@ -751,7 +732,7 @@ export class McpServer { this.setCompletionRequestHandler(); } - return registeredResourceTemplate; + return resourceTemplate; } private _createRegisteredPrompt( @@ -761,29 +742,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) { @@ -796,7 +774,7 @@ export class McpServer { } } - return registeredPrompt; + return prompt; } private _createRegisteredTool( @@ -810,47 +788,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; } /** @@ -965,125 +931,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. @@ -1155,114 +1003,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 getMethodValue(schema: AnyObjectSchema): string { const shape = getObjectShape(schema); const methodSchema = shape?.method as AnySchema | undefined; 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 5d811848b..5d1b7b78a 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -351,7 +351,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Update the tool tool.update({ - callback: async () => ({ + handler: async () => ({ content: [ { type: 'text', @@ -424,11 +424,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', @@ -522,7 +522,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { result: z.number(), sum: z.number() }, - callback: async () => ({ + handler: async () => ({ content: [{ type: 'text', text: '' }], structuredContent: { result: 42, @@ -607,7 +607,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Now update the tool tool.update({ - callback: async () => ({ + handler: async () => ({ content: [ { type: 'text', From b733470caa5d446dd78acf6b6f93bcae8e4fc609 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 01:57:36 +0200 Subject: [PATCH 2/3] docs --- docs/migration-SKILL.md | 4 ++++ docs/migration.md | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 631bd45d6..fec05a5f7 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -227,6 +227,10 @@ tool.update({ paramsSchema: { name: z.string() }, callback: handler }); 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 | diff --git a/docs/migration.md b/docs/migration.md index 2d68f8687..efbbecd6c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -268,6 +268,14 @@ tool.update({ }); ``` +**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 From 549723ea5e306f7ac29172d0bcac168d270563fe Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 20:28:05 +0200 Subject: [PATCH 3/3] prettier fix --- packages/server/src/server/mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 34329081b..7b31d5ca3 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -34,7 +34,7 @@ import { objectFromShape, ProtocolError, ProtocolErrorCode, - safeParseAsync, + safeParseAsync } from '@modelcontextprotocol/core'; import { ZodOptional } from 'zod';