Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand Down
16 changes: 16 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand Down
16 changes: 16 additions & 0 deletions dev-packages/e2e-tests/test-applications/tsx-express/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand Down
28 changes: 18 additions & 10 deletions packages/core/src/integrations/mcp-server/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function captureHandlerError(error: Error, methodName: keyof MCPServerInstance,
try {
const extraData: Record<string, unknown> = {};

if (methodName === 'tool') {
if (methodName === 'tool' || methodName === 'registerTool') {
extraData.tool_name = handlerName;

if (
Expand All @@ -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);
}
Expand All @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/integrations/mcp-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/integrations/mcp-server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,29 @@ 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 (legacy API) */
resource?: (name: string, ...args: unknown[]) => void;

/** Register a tool handler */
tool: (name: string, ...args: unknown[]) => void;
/** Register a tool handler (legacy API) */
tool?: (name: string, ...args: unknown[]) => void;

/** Register a prompt handler */
prompt: (name: string, ...args: unknown[]) => void;
/** Register a prompt handler (legacy API) */
prompt?: (name: string, ...args: unknown[]) => void;

/** Register a resource handler (new API, SDK >=1.x / 2.x) */
registerResource?: (name: string, ...args: unknown[]) => void;

/** Register a tool handler (new API, SDK >=1.x / 2.x) */
registerTool?: (name: string, ...args: unknown[]) => void;

/** Register a prompt handler (new API, SDK >=1.x / 2.x) */
registerPrompt?: (name: string, ...args: unknown[]) => void;

/** Connect the server to a transport */
connect(transport: MCPTransport): Promise<void>;
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/integrations/mcp-server/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,20 @@ 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
*/
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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<typeof createMockMcpServer>;
let wrappedMcpServer: ReturnType<typeof createMockMcpServer>;
Expand Down Expand Up @@ -118,4 +144,38 @@ describe('wrapMcpServerWithSentry', () => {
}).not.toThrow();
});
});

describe('Handler Wrapping (register* API)', () => {
let mockServer: ReturnType<typeof createMockMcpServerWithRegisterApi>;
let wrappedServer: ReturnType<typeof createMockMcpServerWithRegisterApi>;

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();
});
});
});
17 changes: 16 additions & 1 deletion packages/core/test/lib/integrations/mcp-server/testUtils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down
Loading