Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
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
50 changes: 43 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,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<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
Loading
Loading