Skip to content

Commit ae8e59e

Browse files
authored
feat(core): Associate resource/tool/prompt invocations with request span instead of response span (#16126)
1 parent f0c9458 commit ae8e59e

File tree

7 files changed

+416
-47
lines changed

7 files changed

+416
-47
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"test:assert": "pnpm test"
1212
},
1313
"dependencies": {
14+
"@modelcontextprotocol/sdk": "^1.10.2",
1415
"@sentry/core": "latest || *",
1516
"@sentry/node": "latest || *",
1617
"@trpc/server": "10.45.2",
@@ -19,7 +20,7 @@
1920
"@types/node": "^18.19.1",
2021
"express": "^4.21.2",
2122
"typescript": "~5.0.0",
22-
"zod": "~3.22.4"
23+
"zod": "~3.24.3"
2324
},
2425
"devDependencies": {
2526
"@playwright/test": "~1.50.0",

dev-packages/e2e-tests/test-applications/node-express/src/app.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import { TRPCError, initTRPC } from '@trpc/server';
1919
import * as trpcExpress from '@trpc/server/adapters/express';
2020
import express from 'express';
2121
import { z } from 'zod';
22+
import { mcpRouter } from './mcp';
2223

2324
const app = express();
2425
const port = 3030;
2526

27+
app.use(mcpRouter);
28+
2629
app.get('/test-success', function (req, res) {
2730
res.send({ version: 'v1' });
2831
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import express from 'express';
2+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
4+
import { z } from 'zod';
5+
import { wrapMcpServerWithSentry } from '@sentry/core';
6+
7+
const mcpRouter = express.Router();
8+
9+
const server = wrapMcpServerWithSentry(
10+
new McpServer({
11+
name: 'Echo',
12+
version: '1.0.0',
13+
}),
14+
);
15+
16+
server.resource('echo', new ResourceTemplate('echo://{message}', { list: undefined }), async (uri, { message }) => ({
17+
contents: [
18+
{
19+
uri: uri.href,
20+
text: `Resource echo: ${message}`,
21+
},
22+
],
23+
}));
24+
25+
server.tool('echo', { message: z.string() }, async ({ message }, rest) => {
26+
return {
27+
content: [{ type: 'text', text: `Tool echo: ${message}` }],
28+
};
29+
});
30+
31+
server.prompt('echo', { message: z.string() }, ({ message }, extra) => ({
32+
messages: [
33+
{
34+
role: 'user',
35+
content: {
36+
type: 'text',
37+
text: `Please process this message: ${message}`,
38+
},
39+
},
40+
],
41+
}));
42+
43+
const transports: Record<string, SSEServerTransport> = {};
44+
45+
mcpRouter.get('/sse', async (_, res) => {
46+
const transport = new SSEServerTransport('/messages', res);
47+
transports[transport.sessionId] = transport;
48+
res.on('close', () => {
49+
delete transports[transport.sessionId];
50+
});
51+
await server.connect(transport);
52+
});
53+
54+
mcpRouter.post('/messages', async (req, res) => {
55+
const sessionId = req.query.sessionId;
56+
const transport = transports[sessionId as string];
57+
if (transport) {
58+
await transport.handlePostMessage(req, res);
59+
} else {
60+
res.status(400).send('No transport found for sessionId');
61+
}
62+
});
63+
64+
export { mcpRouter };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5+
6+
test('Should record transactions for mcp handlers', async ({ baseURL }) => {
7+
const transport = new SSEClientTransport(new URL(`${baseURL}/sse`));
8+
9+
const client = new Client({
10+
name: 'test-client',
11+
version: '1.0.0',
12+
});
13+
14+
await client.connect(transport);
15+
16+
await test.step('tool handler', async () => {
17+
const postTransactionPromise = waitForTransaction('node-express', transactionEvent => {
18+
return transactionEvent.transaction === 'POST /messages';
19+
});
20+
const toolTransactionPromise = waitForTransaction('node-express', transactionEvent => {
21+
return transactionEvent.transaction === 'mcp-server/tool:echo';
22+
});
23+
24+
const toolResult = await client.callTool({
25+
name: 'echo',
26+
arguments: {
27+
message: 'foobar',
28+
},
29+
});
30+
31+
expect(toolResult).toMatchObject({
32+
content: [
33+
{
34+
text: 'Tool echo: foobar',
35+
type: 'text',
36+
},
37+
],
38+
});
39+
40+
const postTransaction = await postTransactionPromise;
41+
expect(postTransaction).toBeDefined();
42+
43+
const toolTransaction = await toolTransactionPromise;
44+
expect(toolTransaction).toBeDefined();
45+
46+
// 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
47+
});
48+
49+
await test.step('resource handler', async () => {
50+
const postTransactionPromise = waitForTransaction('node-express', transactionEvent => {
51+
return transactionEvent.transaction === 'POST /messages';
52+
});
53+
const resourceTransactionPromise = waitForTransaction('node-express', transactionEvent => {
54+
return transactionEvent.transaction === 'mcp-server/resource:echo';
55+
});
56+
57+
const resourceResult = await client.readResource({
58+
uri: 'echo://foobar',
59+
});
60+
61+
expect(resourceResult).toMatchObject({
62+
contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }],
63+
});
64+
65+
const postTransaction = await postTransactionPromise;
66+
expect(postTransaction).toBeDefined();
67+
68+
const resourceTransaction = await resourceTransactionPromise;
69+
expect(resourceTransaction).toBeDefined();
70+
71+
// 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
72+
});
73+
74+
await test.step('prompt handler', async () => {
75+
const postTransactionPromise = waitForTransaction('node-express', transactionEvent => {
76+
return transactionEvent.transaction === 'POST /messages';
77+
});
78+
const promptTransactionPromise = waitForTransaction('node-express', transactionEvent => {
79+
return transactionEvent.transaction === 'mcp-server/prompt:echo';
80+
});
81+
82+
const promptResult = await client.getPrompt({
83+
name: 'echo',
84+
arguments: {
85+
message: 'foobar',
86+
},
87+
});
88+
89+
expect(promptResult).toMatchObject({
90+
messages: [
91+
{
92+
content: {
93+
text: 'Please process this message: foobar',
94+
type: 'text',
95+
},
96+
role: 'user',
97+
},
98+
],
99+
});
100+
101+
const postTransaction = await postTransactionPromise;
102+
expect(postTransaction).toBeDefined();
103+
104+
const promptTransaction = await promptTransactionPromise;
105+
expect(promptTransaction).toBeDefined();
106+
107+
// 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
108+
});
109+
});

dev-packages/e2e-tests/test-applications/node-express/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"esModuleInterop": true,
55
"lib": ["es2020"],
66
"strict": true,
7-
"outDir": "dist"
7+
"outDir": "dist",
8+
"skipLibCheck": true
89
},
910
"include": ["src/**/*.ts"]
1011
}

0 commit comments

Comments
 (0)