diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/src/mcp.ts index a819858c8f37..ceccc13d088a 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/src/mcp.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/src/mcp.ts @@ -35,6 +35,14 @@ server.tool('echo', { message: z.string() }, async ({ message }, rest) => { }; }); +server.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + server.prompt('echo', { message: z.string() }, ({ message }, extra) => ({ messages: [ { @@ -103,6 +111,14 @@ streamableServer.tool('echo', { message: z.string() }, async ({ message }) => { }; }); +streamableServer.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + streamableServer.prompt('echo', { message: z.string() }, ({ message }) => ({ messages: [ { diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts index 73dfc1d69432..9d290075892f 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts @@ -60,6 +60,38 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); + await test.step('registerTool handler', async () => { + const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { + return transactionEvent.transaction === 'POST /messages'; + }); + const toolTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { + return transactionEvent.transaction === 'tools/call echo-register'; + }); + + const toolResult = await client.callTool({ + name: 'echo-register', + arguments: { + message: 'foobar', + }, + }); + + expect(toolResult).toMatchObject({ + content: [ + { + text: 'registerTool echo: foobar', + type: 'text', + }, + ], + }); + + const postTransaction = await postTransactionPromise; + expect(postTransaction).toBeDefined(); + + const toolTransaction = await toolTransactionPromise; + expect(toolTransaction).toBeDefined(); + expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); + }); + await test.step('resource handler', async () => { const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { return transactionEvent.transaction === 'POST /messages'; diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts index 638462423d11..7f59668de738 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts @@ -35,6 +35,14 @@ server.tool('echo', { message: z.string() }, async ({ message }, rest) => { }; }); +server.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + server.prompt('echo', { message: z.string() }, ({ message }, extra) => ({ messages: [ { @@ -103,6 +111,14 @@ streamableServer.tool('echo', { message: z.string() }, async ({ message }) => { }; }); +streamableServer.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + streamableServer.prompt('echo', { message: z.string() }, ({ message }) => ({ messages: [ { diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts index 143867c773e6..12eacc1259af 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts @@ -62,6 +62,41 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); + await test.step('registerTool handler', async () => { + const postTransactionPromise = waitForTransaction('node-express', transactionEvent => { + return transactionEvent.transaction === 'POST /messages'; + }); + const toolTransactionPromise = waitForTransaction('node-express', transactionEvent => { + return transactionEvent.transaction === 'tools/call echo-register'; + }); + + const toolResult = await client.callTool({ + name: 'echo-register', + arguments: { + message: 'foobar', + }, + }); + + expect(toolResult).toMatchObject({ + content: [ + { + text: 'registerTool echo: foobar', + type: 'text', + }, + ], + }); + + const postTransaction = await postTransactionPromise; + expect(postTransaction).toBeDefined(); + expect(postTransaction.contexts?.trace?.op).toEqual('http.server'); + + const toolTransaction = await toolTransactionPromise; + expect(toolTransaction).toBeDefined(); + expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); + expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); + }); + await test.step('resource handler', async () => { const postTransactionPromise = waitForTransaction('node-express', transactionEvent => { return transactionEvent.transaction === 'POST /messages'; diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/src/mcp.ts b/dev-packages/e2e-tests/test-applications/tsx-express/src/mcp.ts index b3b401ac294c..bc9ec26a3477 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/src/mcp.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/src/mcp.ts @@ -35,6 +35,14 @@ server.tool('echo', { message: z.string() }, async ({ message }, rest) => { }; }); +server.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + server.prompt('echo', { message: z.string() }, ({ message }, extra) => ({ messages: [ { @@ -103,6 +111,14 @@ streamableServer.tool('echo', { message: z.string() }, async ({ message }) => { }; }); +streamableServer.registerTool( + 'echo-register', + { description: 'Echo tool (register API)', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `registerTool echo: ${message}` }], + }), +); + streamableServer.prompt('echo', { message: z.string() }, ({ message }) => ({ messages: [ { diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts index a89cfcaa11c6..71694a2b72b0 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts @@ -63,6 +63,40 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => { // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); + await test.step('registerTool handler', async () => { + const postTransactionPromise = waitForTransaction('tsx-express', transactionEvent => { + return transactionEvent.transaction === 'POST /messages'; + }); + const toolTransactionPromise = waitForTransaction('tsx-express', transactionEvent => { + return transactionEvent.transaction === 'tools/call echo-register'; + }); + + const toolResult = await client.callTool({ + name: 'echo-register', + arguments: { + message: 'foobar', + }, + }); + + expect(toolResult).toMatchObject({ + content: [ + { + text: 'registerTool echo: foobar', + type: 'text', + }, + ], + }); + + const postTransaction = await postTransactionPromise; + expect(postTransaction).toBeDefined(); + + const toolTransaction = await toolTransactionPromise; + expect(toolTransaction).toBeDefined(); + expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); + expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); + }); + await test.step('resource handler', async () => { const postTransactionPromise = waitForTransaction('tsx-express', transactionEvent => { return transactionEvent.transaction === 'POST /messages'; diff --git a/packages/core/src/integrations/mcp-server/handlers.ts b/packages/core/src/integrations/mcp-server/handlers.ts index dd8e0296a95e..5ac0d0e0722a 100644 --- a/packages/core/src/integrations/mcp-server/handlers.ts +++ b/packages/core/src/integrations/mcp-server/handlers.ts @@ -96,7 +96,7 @@ function captureHandlerError(error: Error, methodName: keyof MCPServerInstance, try { const extraData: Record = {}; - if (methodName === 'tool') { + if (methodName === 'tool' || methodName === 'registerTool') { extraData.tool_name = handlerName; if ( @@ -114,10 +114,10 @@ function captureHandlerError(error: Error, methodName: keyof MCPServerInstance, } else { captureError(error, 'tool_execution', extraData); } - } else if (methodName === 'resource') { + } else if (methodName === 'resource' || methodName === 'registerResource') { extraData.resource_uri = handlerName; captureError(error, 'resource_execution', extraData); - } else if (methodName === 'prompt') { + } else if (methodName === 'prompt' || methodName === 'registerPrompt') { extraData.prompt_name = handlerName; captureError(error, 'prompt_execution', extraData); } @@ -127,31 +127,39 @@ function captureHandlerError(error: Error, methodName: keyof MCPServerInstance, } /** - * Wraps tool handlers to associate them with request spans + * Wraps tool handlers to associate them with request spans. + * Instruments both `tool` (legacy API) and `registerTool` (new API) if present. * @param serverInstance - MCP server instance */ export function wrapToolHandlers(serverInstance: MCPServerInstance): void { - wrapMethodHandler(serverInstance, 'tool'); + if (typeof serverInstance.tool === 'function') wrapMethodHandler(serverInstance, 'tool'); + if (typeof serverInstance.registerTool === 'function') wrapMethodHandler(serverInstance, 'registerTool'); } /** - * Wraps resource handlers to associate them with request spans + * Wraps resource handlers to associate them with request spans. + * Instruments both `resource` (legacy API) and `registerResource` (new API) if present. * @param serverInstance - MCP server instance */ export function wrapResourceHandlers(serverInstance: MCPServerInstance): void { - wrapMethodHandler(serverInstance, 'resource'); + if (typeof serverInstance.resource === 'function') wrapMethodHandler(serverInstance, 'resource'); + if (typeof serverInstance.registerResource === 'function') wrapMethodHandler(serverInstance, 'registerResource'); } /** - * Wraps prompt handlers to associate them with request spans + * Wraps prompt handlers to associate them with request spans. + * Instruments both `prompt` (legacy API) and `registerPrompt` (new API) if present. * @param serverInstance - MCP server instance */ export function wrapPromptHandlers(serverInstance: MCPServerInstance): void { - wrapMethodHandler(serverInstance, 'prompt'); + if (typeof serverInstance.prompt === 'function') wrapMethodHandler(serverInstance, 'prompt'); + if (typeof serverInstance.registerPrompt === 'function') wrapMethodHandler(serverInstance, 'registerPrompt'); } /** - * Wraps all MCP handler types (tool, resource, prompt) for span correlation + * Wraps all MCP handler types for span correlation. + * Supports both the legacy API (`tool`, `resource`, `prompt`) and the newer API + * (`registerTool`, `registerResource`, `registerPrompt`), instrumenting whichever methods are present. * @param serverInstance - MCP server instance */ export function wrapAllMCPHandlers(serverInstance: MCPServerInstance): void { diff --git a/packages/core/src/integrations/mcp-server/index.ts b/packages/core/src/integrations/mcp-server/index.ts index 5698cd445834..b4ef87f0fa0a 100644 --- a/packages/core/src/integrations/mcp-server/index.ts +++ b/packages/core/src/integrations/mcp-server/index.ts @@ -14,7 +14,8 @@ const wrappedMcpServerInstances = new WeakSet(); /** * Wraps a MCP Server instance from the `@modelcontextprotocol/sdk` package with Sentry instrumentation. * - * Compatible with versions `^1.9.0` of the `@modelcontextprotocol/sdk` package. + * Compatible with versions `^1.9.0` of the `@modelcontextprotocol/sdk` package (legacy `tool`/`resource`/`prompt` API) + * and versions that expose the newer `registerTool`/`registerResource`/`registerPrompt` API (introduced in 1.x, sole API in 2.x). * Automatically instruments transport methods and handler functions for comprehensive monitoring. * * @example diff --git a/packages/core/src/integrations/mcp-server/types.ts b/packages/core/src/integrations/mcp-server/types.ts index 35dbcffcabb0..e6fd873fa4fa 100644 --- a/packages/core/src/integrations/mcp-server/types.ts +++ b/packages/core/src/integrations/mcp-server/types.ts @@ -87,17 +87,53 @@ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcRespo /** * MCP server instance interface - * @description MCP server methods for registering handlers + * @description MCP server methods for registering handlers. + * Supports both the legacy API (`tool`, `resource`, `prompt`) used in SDK 1.x + * and the newer API (`registerTool`, `registerResource`, `registerPrompt`) introduced in SDK 1.x + * and made the only option in SDK 2.x. */ export interface MCPServerInstance { - /** Register a resource handler */ - resource: (name: string, ...args: unknown[]) => void; + /** + * Register a resource handler. + * Supported in `@modelcontextprotocol/sdk` v1.x alongside `registerResource`. + * @deprecated Removed in `@modelcontextprotocol/sdk` v2.0.0 — use `registerResource` instead. + */ + resource?: (name: string, ...args: unknown[]) => void; + + /** + * Register a tool handler. + * Supported in `@modelcontextprotocol/sdk` v1.x alongside `registerTool`. + * @deprecated Removed in `@modelcontextprotocol/sdk` v2.0.0 — use `registerTool` instead. + */ + tool?: (name: string, ...args: unknown[]) => void; - /** Register a tool handler */ - tool: (name: string, ...args: unknown[]) => void; + /** + * Register a prompt handler. + * Supported in `@modelcontextprotocol/sdk` v1.x alongside `registerPrompt`. + * @deprecated Removed in `@modelcontextprotocol/sdk` v2.0.0 — use `registerPrompt` instead. + */ + prompt?: (name: string, ...args: unknown[]) => void; - /** Register a prompt handler */ - prompt: (name: string, ...args: unknown[]) => void; + /** + * Register a resource handler. + * Available in `@modelcontextprotocol/sdk` v1.x (alongside the legacy `resource` method) + * and the only supported form in v2.0.0+. + */ + registerResource?: (name: string, ...args: unknown[]) => void; + + /** + * Register a tool handler. + * Available in `@modelcontextprotocol/sdk` v1.x (alongside the legacy `tool` method) + * and the only supported form in v2.0.0+. + */ + registerTool?: (name: string, ...args: unknown[]) => void; + + /** + * Register a prompt handler. + * Available in `@modelcontextprotocol/sdk` v1.x (alongside the legacy `prompt` method) + * and the only supported form in v2.0.0+. + */ + registerPrompt?: (name: string, ...args: unknown[]) => void; /** Connect the server to a transport */ connect(transport: MCPTransport): Promise; diff --git a/packages/core/src/integrations/mcp-server/validation.ts b/packages/core/src/integrations/mcp-server/validation.ts index 9ed21b290728..15fb27f9c803 100644 --- a/packages/core/src/integrations/mcp-server/validation.ts +++ b/packages/core/src/integrations/mcp-server/validation.ts @@ -57,7 +57,10 @@ export function isJsonRpcResponse(message: unknown): message is JsonRpcResponse } /** - * Validates MCP server instance with type checking + * Validates MCP server instance with type checking. + * Accepts both the legacy API (`tool`, `resource`, `prompt`) used in SDK 1.x + * and the newer API (`registerTool`, `registerResource`, `registerPrompt`) introduced + * alongside the legacy API in SDK 1.x and made the only option in SDK 2.x. * @param instance - Object to validate as MCP server instance * @returns True if instance has required MCP server methods */ @@ -65,10 +68,9 @@ export function validateMcpServerInstance(instance: unknown): boolean { if ( typeof instance === 'object' && instance !== null && - 'resource' in instance && - 'tool' in instance && - 'prompt' in instance && - 'connect' in instance + 'connect' in instance && + (('tool' in instance && 'resource' in instance && 'prompt' in instance) || + ('registerTool' in instance && 'registerResource' in instance && 'registerPrompt' in instance)) ) { return true; } diff --git a/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts b/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts index c277162017aa..3fc48a2e0b47 100644 --- a/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as currentScopes from '../../../../src/currentScopes'; import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server'; import * as tracingModule from '../../../../src/tracing'; -import { createMockMcpServer } from './testUtils'; +import { createMockMcpServer, createMockMcpServerWithRegisterApi } from './testUtils'; describe('wrapMcpServerWithSentry', () => { const startSpanSpy = vi.spyOn(tracingModule, 'startSpan'); @@ -45,6 +45,19 @@ describe('wrapMcpServerWithSentry', () => { expect(startInactiveSpanSpy).not.toHaveBeenCalled(); }); + it('should accept a server with only the new register* API (no legacy methods)', () => { + const mockServer = createMockMcpServerWithRegisterApi(); + const result = wrapMcpServerWithSentry(mockServer); + expect(result).toBe(mockServer); + }); + + it('should reject a server with neither legacy nor register* methods', () => { + const invalidServer = { connect: vi.fn() }; + const result = wrapMcpServerWithSentry(invalidServer); + expect(result).toBe(invalidServer); + expect(startSpanSpy).not.toHaveBeenCalled(); + }); + it('should not wrap the same instance twice', () => { const mockMcpServer = createMockMcpServer(); @@ -77,6 +90,19 @@ describe('wrapMcpServerWithSentry', () => { expect(wrappedMcpServer.prompt).not.toBe(originalPrompt); }); + it('should wrap handler methods (registerTool, registerResource, registerPrompt)', () => { + const mockServer = createMockMcpServerWithRegisterApi(); + const originalRegisterTool = mockServer.registerTool; + const originalRegisterResource = mockServer.registerResource; + const originalRegisterPrompt = mockServer.registerPrompt; + + const wrapped = wrapMcpServerWithSentry(mockServer); + + expect(wrapped.registerTool).not.toBe(originalRegisterTool); + expect(wrapped.registerResource).not.toBe(originalRegisterResource); + expect(wrapped.registerPrompt).not.toBe(originalRegisterPrompt); + }); + describe('Handler Wrapping', () => { let mockMcpServer: ReturnType; let wrappedMcpServer: ReturnType; @@ -118,4 +144,38 @@ describe('wrapMcpServerWithSentry', () => { }).not.toThrow(); }); }); + + describe('Handler Wrapping (register* API)', () => { + let mockServer: ReturnType; + let wrappedServer: ReturnType; + + beforeEach(() => { + mockServer = createMockMcpServerWithRegisterApi(); + wrappedServer = wrapMcpServerWithSentry(mockServer); + }); + + it('should register tool handlers via registerTool without throwing errors', () => { + const toolHandler = vi.fn(); + + expect(() => { + wrappedServer.registerTool('test-tool', {}, toolHandler); + }).not.toThrow(); + }); + + it('should register resource handlers via registerResource without throwing errors', () => { + const resourceHandler = vi.fn(); + + expect(() => { + wrappedServer.registerResource('test-resource', 'res://test', {}, resourceHandler); + }).not.toThrow(); + }); + + it('should register prompt handlers via registerPrompt without throwing errors', () => { + const promptHandler = vi.fn(); + + expect(() => { + wrappedServer.registerPrompt('test-prompt', {}, promptHandler); + }).not.toThrow(); + }); + }); }); diff --git a/packages/core/test/lib/integrations/mcp-server/testUtils.ts b/packages/core/test/lib/integrations/mcp-server/testUtils.ts index 782f03a78e35..23b9ee6ff51b 100644 --- a/packages/core/test/lib/integrations/mcp-server/testUtils.ts +++ b/packages/core/test/lib/integrations/mcp-server/testUtils.ts @@ -1,7 +1,7 @@ import { vi } from 'vitest'; /** - * Create a mock MCP server instance for testing + * Create a mock MCP server instance for testing (legacy API: tool/resource/prompt) */ export function createMockMcpServer() { return { @@ -15,6 +15,21 @@ export function createMockMcpServer() { }; } +/** + * Create a mock MCP server instance using the new register* API (SDK >=1.x / 2.x) + */ +export function createMockMcpServerWithRegisterApi() { + return { + registerResource: vi.fn(), + registerTool: vi.fn(), + registerPrompt: vi.fn(), + connect: vi.fn().mockResolvedValue(undefined), + server: { + setRequestHandler: vi.fn(), + }, + }; +} + /** * Create a mock HTTP transport (StreamableHTTPServerTransport) * Uses exact naming pattern from the official SDK