Skip to content

Commit f46bc61

Browse files
sergicalcodex
andcommitted
feat(core): support stable MCP SDK v2
Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent fa8babb commit f46bc61

10 files changed

Lines changed: 382 additions & 188 deletions

File tree

dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
"test:dev": "TEST_ENV=development playwright test"
1515
},
1616
"dependencies": {
17-
"@modelcontextprotocol/sdk": "^1.24.0",
17+
"@modelcontextprotocol/server": "2.0.0",
1818
"@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
19-
"agents": "0.3.10",
20-
"zod": "^3.25.76"
19+
"agents": "0.20.1",
20+
"zod": "^4.2.0"
2121
},
2222
"devDependencies": {
2323
"@cloudflare/workers-types": "^4.20240725.0",
@@ -30,11 +30,5 @@
3030
"volta": {
3131
"node": "24.15.0",
3232
"extends": "../../package.json"
33-
},
34-
"pnpm": {
35-
"overrides": {
36-
"strip-literal": "~2.0.0",
37-
"@modelcontextprotocol/sdk": "1.25.2"
38-
}
3933
}
4034
}

dev-packages/e2e-tests/test-applications/cloudflare-mcp/src/index.ts

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,55 @@
1111
* Learn more at https://developers.cloudflare.com/workers/
1212
*/
1313
import * as Sentry from '@sentry/cloudflare';
14-
import { createMcpHandler } from 'agents/mcp';
15-
import * as z from 'zod';
16-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14+
import { McpServer } from '@modelcontextprotocol/server';
15+
import { createMcpHandler } from 'agents/mcp/server';
16+
import { z } from 'zod';
17+
18+
function createServer() {
19+
const server = Sentry.wrapMcpServerWithSentry(
20+
new McpServer({
21+
name: 'cloudflare-mcp',
22+
version: '2.0.0',
23+
}),
24+
);
25+
26+
server.registerTool(
27+
'my-tool',
28+
{
29+
title: 'My Tool',
30+
description: 'My Tool Description',
31+
inputSchema: z.object({
32+
message: z.string(),
33+
}),
34+
},
35+
async ({ message }) => {
36+
const span = Sentry.getActiveSpan();
37+
38+
await new Promise(resolve => setTimeout(resolve, 500));
39+
40+
if (span) {
41+
span.setAttribute('mcp.tool.name', 'my-tool');
42+
span.setAttribute('mcp.tool.extra', 'ƸӜƷ');
43+
span.setAttribute('mcp.tool.input', JSON.stringify({ message }));
44+
}
45+
46+
return {
47+
content: [
48+
{
49+
type: 'text' as const,
50+
text: `Tool my-tool: ${message}`,
51+
},
52+
],
53+
};
54+
},
55+
);
56+
57+
return server;
58+
}
59+
60+
const mcpHandler = createMcpHandler(createServer, {
61+
route: '/mcp',
62+
});
1763

1864
export default Sentry.withSentry(
1965
(env: Env) => ({
@@ -31,54 +77,13 @@ export default Sentry.withSentry(
3177
}),
3278
{
3379
async fetch(request, env, ctx) {
34-
const server = new McpServer({
35-
name: 'cloudflare-mcp',
36-
version: '1.0.0',
37-
});
38-
3980
const span = Sentry.getActiveSpan();
4081

4182
if (span) {
4283
span.setAttribute('mcp.server.extra', ' /|\ ^._.^ /|\ ');
4384
}
4485

45-
server.registerTool(
46-
'my-tool',
47-
{
48-
title: 'My Tool',
49-
description: 'My Tool Description',
50-
inputSchema: {
51-
message: z.string(),
52-
},
53-
},
54-
async ({ message }) => {
55-
const span = Sentry.getActiveSpan();
56-
57-
// simulate a long running tool
58-
await new Promise(resolve => setTimeout(resolve, 500));
59-
60-
if (span) {
61-
span.setAttribute('mcp.tool.name', 'my-tool');
62-
span.setAttribute('mcp.tool.extra', 'ƸӜƷ');
63-
span.setAttribute('mcp.tool.input', JSON.stringify({ message }));
64-
}
65-
66-
return {
67-
content: [
68-
{
69-
type: 'text' as const,
70-
text: `Tool my-tool: ${message}`,
71-
},
72-
],
73-
};
74-
},
75-
);
76-
77-
const handler = createMcpHandler(Sentry.wrapMcpServerWithSentry(server), {
78-
route: '/mcp',
79-
});
80-
81-
return handler(request, env, ctx);
86+
return mcpHandler(request, env, ctx);
8287
},
8388
} satisfies ExportedHandler<Env>,
8489
);
Lines changed: 113 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,146 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForRequest } from '@sentry-internal/test-utils';
33

4-
test('sends spans for MCP tool calls', async ({ baseURL }) => {
5-
const spanRequestWaiter = waitForRequest('cloudflare-mcp', event => {
6-
const transaction = event.envelope[1][0][1];
7-
return typeof transaction !== 'string' && 'transaction' in transaction && transaction.transaction === 'POST /mcp';
8-
});
4+
const APP_NAME = 'cloudflare-mcp';
5+
6+
function getTransaction(eventData: Awaited<ReturnType<typeof waitForRequest>>) {
7+
const event = eventData.envelope[1][0][1];
8+
return typeof event !== 'string' && 'transaction' in event ? event : undefined;
9+
}
10+
11+
function requireTransaction(eventData: Awaited<ReturnType<typeof waitForRequest>>) {
12+
const event = getTransaction(eventData);
13+
if (!event) {
14+
throw new Error('Expected a transaction event');
15+
}
16+
return event;
17+
}
18+
19+
test.describe.configure({ mode: 'serial' });
920

10-
const spanMcpWaiter = waitForRequest('cloudflare-mcp', event => {
11-
const transaction = event.envelope[1][0][1];
21+
test('sends spans for MCP 2026-07-28 tool calls', async ({ baseURL }) => {
22+
const url = `${baseURL}/mcp?protocol=modern`;
23+
const requestWaiter = waitForRequest(APP_NAME, eventData => {
24+
const event = getTransaction(eventData);
25+
return event?.transaction === 'POST /mcp' && event.contexts?.trace?.data?.['url.full'] === url;
26+
});
27+
const mcpWaiter = waitForRequest(APP_NAME, eventData => {
28+
const event = getTransaction(eventData);
1229
return (
13-
typeof transaction !== 'string' &&
14-
'transaction' in transaction &&
15-
transaction.transaction === 'tools/call my-tool'
30+
event?.transaction === 'tools/call my-tool' &&
31+
event.contexts?.trace?.data?.['mcp.protocol.version'] === '2026-07-28'
1632
);
1733
});
1834

19-
const response = await fetch(`${baseURL}/mcp`, {
35+
const response = await fetch(url, {
2036
method: 'POST',
2137
headers: {
2238
'Content-Type': 'application/json',
2339
Accept: 'application/json, text/event-stream',
40+
'MCP-Protocol-Version': '2026-07-28',
41+
'Mcp-Method': 'tools/call',
42+
'Mcp-Name': 'my-tool',
2443
},
2544
body: JSON.stringify({
2645
jsonrpc: '2.0',
27-
id: 1,
46+
id: 'modern-tool-call',
2847
method: 'tools/call',
2948
params: {
49+
_meta: {
50+
'io.modelcontextprotocol/protocolVersion': '2026-07-28',
51+
'io.modelcontextprotocol/clientInfo': {
52+
name: 'cloudflare-modern-client',
53+
version: '2.0.0',
54+
},
55+
'io.modelcontextprotocol/clientCapabilities': {},
56+
},
3057
name: 'my-tool',
3158
arguments: {
32-
message: 'ʕっ•ᴥ•ʔっ',
59+
message: 'modern protocol request',
3360
},
3461
},
3562
}),
3663
});
3764

3865
expect(response.status).toBe(200);
66+
await expect(response.json()).resolves.toMatchObject({
67+
jsonrpc: '2.0',
68+
id: 'modern-tool-call',
69+
result: {
70+
resultType: 'complete',
71+
content: [{ type: 'text', text: 'Tool my-tool: modern protocol request' }],
72+
},
73+
});
3974

40-
const requestData = await spanRequestWaiter;
41-
const mcpData = await spanMcpWaiter;
42-
43-
const requestEvent = requestData.envelope[1][0][1];
44-
const mcpEvent = mcpData.envelope[1][0][1];
45-
46-
// Check that the events have contexts
47-
// this is for TypeScript type safety
48-
if (
49-
typeof mcpEvent === 'string' ||
50-
!('contexts' in mcpEvent) ||
51-
typeof requestEvent === 'string' ||
52-
!('contexts' in requestEvent)
53-
) {
54-
throw new Error("Events don't have contexts");
55-
}
75+
const requestData = await requestWaiter;
76+
const mcpData = await mcpWaiter;
77+
const requestEvent = requireTransaction(requestData);
78+
const mcpEvent = requireTransaction(mcpData);
79+
const requestTrace = requestEvent.contexts?.trace;
80+
const mcpTrace = mcpEvent.contexts?.trace;
5681

57-
expect(mcpEvent.contexts?.trace?.trace_id).toBe((mcpData.envelope[0].trace as any).trace_id);
58-
expect(requestData.envelope[0].event_id).not.toBe(mcpData.envelope[0].event_id);
82+
expect(requestTrace?.op).toBe('http.server');
83+
expect(requestTrace?.data?.['mcp.server.extra']).toBe(' /|\ ^._.^ /|\ ');
84+
expect(mcpTrace?.trace_id).toBe(requestTrace?.trace_id);
85+
expect(mcpTrace?.parent_span_id).toBe(requestTrace?.span_id);
86+
expect(mcpTrace?.op).toBe('mcp.server');
87+
expect(mcpTrace?.origin).toBe('auto.function.mcp_server');
88+
expect(mcpTrace?.status).toBe('ok');
89+
expect(mcpTrace?.data?.['mcp.transport']).toBe('PerRequestHTTPServerTransport');
90+
expect(mcpTrace?.data?.['network.transport']).toBe('tcp');
91+
expect(mcpTrace?.data?.['mcp.protocol.version']).toBe('2026-07-28');
92+
expect(mcpTrace?.data?.['mcp.client.name']).toBe('cloudflare-modern-client');
93+
expect(mcpTrace?.data?.['mcp.client.version']).toBe('2.0.0');
94+
expect(mcpTrace?.data?.['mcp.server.name']).toBe('cloudflare-mcp');
95+
expect(mcpTrace?.data?.['mcp.server.version']).toBe('2.0.0');
96+
expect(mcpTrace?.data?.['mcp.method.name']).toBe('tools/call');
97+
expect(mcpTrace?.data?.['mcp.request.id']).toBe('modern-tool-call');
98+
expect(mcpTrace?.data?.['mcp.tool.name']).toBe('my-tool');
99+
expect(mcpTrace?.data?.['mcp.request.argument.message']).toBe('"modern protocol request"');
100+
expect(mcpTrace?.data?.['mcp.tool.result.content_count']).toBe(1);
101+
expect(mcpTrace?.data?.['mcp.tool.result.content']).toBe('Tool my-tool: modern protocol request');
102+
});
59103

60-
expect(requestEvent.contexts?.trace).toEqual({
61-
span_id: expect.any(String),
62-
trace_id: expect.any(String),
63-
data: expect.objectContaining({
64-
'sentry.origin': 'auto.http.cloudflare',
65-
'sentry.op': 'http.server',
66-
'sentry.source': 'url',
67-
'sentry.sample_rate': 1,
68-
'http.request.method': 'POST',
69-
'url.path': '/mcp',
70-
'url.full': 'http://localhost:38787/mcp',
71-
'url.port': '38787',
72-
'url.scheme': 'http:',
73-
'server.address': 'localhost',
74-
'http.request.body.size': 120,
75-
'user_agent.original': 'node',
76-
'http.request.header.content_type': 'application/json',
77-
'network.protocol.name': 'HTTP/1.1',
78-
'mcp.server.extra': ' /|\ ^._.^ /|\ ',
79-
'http.response.status_code': 200,
80-
}),
81-
op: 'http.server',
82-
status: 'ok',
83-
origin: 'auto.http.cloudflare',
104+
test('keeps sending spans for legacy-compatible MCP tool calls', async ({ baseURL }) => {
105+
const url = `${baseURL}/mcp?protocol=legacy`;
106+
const mcpWaiter = waitForRequest(APP_NAME, eventData => {
107+
const event = getTransaction(eventData);
108+
return (
109+
event?.transaction === 'tools/call my-tool' &&
110+
event.contexts?.trace?.data?.['mcp.request.argument.message'] === '"legacy protocol request"'
111+
);
84112
});
85113

86-
expect(mcpEvent.contexts?.trace).toEqual({
87-
trace_id: expect.any(String),
88-
parent_span_id: requestEvent.contexts?.trace?.span_id,
89-
span_id: expect.any(String),
90-
op: 'mcp.server',
91-
origin: 'auto.function.mcp_server',
92-
status: 'ok',
93-
data: {
94-
'sentry.origin': 'auto.function.mcp_server',
95-
'sentry.op': 'mcp.server',
96-
'sentry.source': 'route',
97-
'mcp.transport': 'WorkerTransport',
98-
'network.transport': 'unknown',
99-
'network.protocol.version': '2.0',
100-
'mcp.method.name': 'tools/call',
101-
'mcp.request.id': '1',
102-
'mcp.tool.name': 'my-tool',
103-
'mcp.request.argument.message': '"ʕっ•ᴥ•ʔっ"',
104-
'mcp.tool.extra': 'ƸӜƷ',
105-
'mcp.tool.input': '{"message":"ʕっ•ᴥ•ʔっ"}',
106-
'mcp.tool.result.content_count': 1,
107-
'mcp.tool.result.content_type': 'text',
108-
'mcp.tool.result.content': 'Tool my-tool: ʕっ•ᴥ•ʔっ',
114+
const response = await fetch(url, {
115+
method: 'POST',
116+
headers: {
117+
'Content-Type': 'application/json',
118+
Accept: 'application/json, text/event-stream',
109119
},
120+
body: JSON.stringify({
121+
jsonrpc: '2.0',
122+
id: 'legacy-tool-call',
123+
method: 'tools/call',
124+
params: {
125+
name: 'my-tool',
126+
arguments: {
127+
message: 'legacy protocol request',
128+
},
129+
},
130+
}),
110131
});
132+
133+
expect(response.status).toBe(200);
134+
135+
const mcpEvent = requireTransaction(await mcpWaiter);
136+
const trace = mcpEvent.contexts?.trace;
137+
138+
expect(trace?.op).toBe('mcp.server');
139+
expect(trace?.status).toBe('ok');
140+
expect(trace?.data?.['mcp.transport']).toBe('WebStandardStreamableHTTPServerTransport');
141+
expect(trace?.data?.['mcp.method.name']).toBe('tools/call');
142+
expect(trace?.data?.['mcp.request.id']).toBe('legacy-tool-call');
143+
expect(trace?.data?.['mcp.tool.name']).toBe('my-tool');
144+
expect(trace?.data?.['mcp.protocol.version']).toBeUndefined();
145+
expect(trace?.data?.['mcp.tool.result.content']).toBe('Tool my-tool: legacy protocol request');
111146
});

dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212
},
1313
"dependencies": {
1414
"@cfworker/json-schema": "^4.0.0",
15-
"@modelcontextprotocol/server": "2.0.0-alpha.2",
16-
"@modelcontextprotocol/node": "2.0.0-alpha.2",
15+
"@modelcontextprotocol/node": "2.0.0",
16+
"@modelcontextprotocol/server": "2.0.0",
1717
"@sentry/node": "file:../../packed/sentry-node-packed.tgz",
1818
"@types/express": "^4.17.21",
1919
"@types/node": "^18.19.1",
2020
"express": "^4.21.2",
2121
"typescript": "~5.0.0",
22-
"zod": "^4.0.0"
22+
"zod": "^4.2.0"
2323
},
2424
"devDependencies": {
25-
"@modelcontextprotocol/client": "2.0.0-alpha.2",
25+
"@modelcontextprotocol/client": "2.0.0",
2626
"@playwright/test": "~1.56.0",
2727
"@sentry-internal/test-utils": "link:../../../test-utils",
2828
"@sentry/core": "file:../../packed/sentry-core-packed.tgz"

dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
33
import { Client } from '@modelcontextprotocol/client';
44
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
55

6-
test('Should record transactions for MCP handlers using @modelcontextprotocol/sdk v2 (register* API)', async ({
7-
baseURL,
8-
}) => {
6+
test('records transactions for stable MCP SDK v2 handlers using the register API', async ({ baseURL }) => {
97
const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`));
108

119
const client = new Client({

0 commit comments

Comments
 (0)