From 855161c5f4051fbbb85007f16251d3d6fedf7c43 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 21 Sep 2026 10:19:18 +0200 Subject: [PATCH 1/2] test(e2e): Add MCP auto-instrumentation e2e apps (node, v1 + v2) Add two Express e2e apps that construct an `McpServer` WITHOUT calling `wrapMcpServerWithSentry`, exercising the `mcpServer` integration's auto-wrapping over a real streamable-HTTP transport: - `node-express-mcp-v1-auto` (`@modelcontextprotocol/sdk` v1) - `node-express-mcp-v2-auto` (`@modelcontextprotocol/server` v2) Both assert the expected `mcp.server` spans (initialize, tool call, resource read, error status) still appear without a manual wrap. Bun/Deno/Cloudflare variants are intentionally left as a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../node-express-mcp-v1-auto/instrument.mjs | 9 ++ .../node-express-mcp-v1-auto/package.json | 34 +++++ .../playwright.config.mjs | 7 + .../node-express-mcp-v1-auto/src/app.ts | 17 +++ .../node-express-mcp-v1-auto/src/mcp.ts | 120 ++++++++++++++++ .../start-event-proxy.mjs | 6 + .../tests/mcp.test.ts | 100 +++++++++++++ .../node-express-mcp-v1-auto/tsconfig.json | 13 ++ .../node-express-mcp-v2-auto/instrument.mjs | 9 ++ .../node-express-mcp-v2-auto/package.json | 37 +++++ .../playwright.config.mjs | 7 + .../node-express-mcp-v2-auto/src/app.ts | 17 +++ .../node-express-mcp-v2-auto/src/mcp.ts | 124 ++++++++++++++++ .../start-event-proxy.mjs | 6 + .../tests/mcp.test.ts | 134 ++++++++++++++++++ .../node-express-mcp-v2-auto/tsconfig.json | 13 ++ 16 files changed, 653 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs new file mode 100644 index 000000000000..f3dd95215d03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json new file mode 100644 index 000000000000..29ddfa6a60fd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json @@ -0,0 +1,34 @@ +{ + "name": "node-express-mcp-v1-auto-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node --import ./instrument.mjs dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@types/express": "^4.17.21", + "@types/node": "^18.19.1", + "express": "^4.21.2", + "typescript": "~5.0.0", + "zod": "~3.25.0" + }, + "devDependencies": { + "@playwright/test": "~1.63.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/core": "file:../../packed/sentry-core-packed.tgz" + }, + "type": "module", + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts new file mode 100644 index 000000000000..3ed8df4a040a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; +import { mcpRouter } from './mcp.js'; + +const app = express(); +const port = 3030; + +app.use(express.json()); +app.use(mcpRouter); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts new file mode 100644 index 000000000000..004cf2f89287 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts @@ -0,0 +1,120 @@ +import { randomUUID } from 'node:crypto'; +import express from 'express'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { z } from 'zod'; + +const mcpRouter = express.Router(); + +// Intentionally NOT wrapped with `wrapMcpServerWithSentry`: the `mcpServer` integration +// auto-instruments the `McpServer` constructor, so spans must be produced anyway. +const server = new McpServer({ + name: 'Echo-V1-Auto', + version: '1.0.0', +}); + +server.registerResource( + 'echo', + new ResourceTemplate('echo://{message}', { list: undefined }), + { title: 'Echo Resource' }, + async (uri, { message }) => ({ + contents: [ + { + uri: uri.href, + text: `Resource echo: ${message}`, + }, + ], + }), +); + +server.registerTool( + 'echo', + { description: 'Echo tool', inputSchema: { message: z.string() } }, + async ({ message }) => ({ + content: [{ type: 'text', text: `Tool echo: ${message}` }], + }), +); + +server.registerPrompt('echo', { description: 'Echo prompt', argsSchema: { message: z.string() } }, ({ message }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please process this message: ${message}`, + }, + }, + ], +})); + +server.registerTool('always-error', {}, async () => { + throw new Error('intentional error for span status testing'); +}); + +const transports: Record = {}; + +mcpRouter.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + try { + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + } else if (!sessionId && req.body?.method === 'initialize') { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: sid => { + transports[sid] = transport; + }, + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid && transports[sid]) { + delete transports[sid]; + } + }; + + await server.connect(transport); + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, + id: null, + }); + return; + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { code: -32603, message: 'Internal server error' }, + id: null, + }); + } + } +}); + +mcpRouter.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); +}); + +mcpRouter.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); +}); + +export { mcpRouter }; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs new file mode 100644 index 000000000000..79a2c31587f2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-mcp-v1-auto', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts new file mode 100644 index 000000000000..27d280e5d072 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from '@playwright/test'; +import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +// The server in this app is never wrapped with `wrapMcpServerWithSentry` — spans are produced +// solely by the auto-instrumenting `mcpServer` integration against the legacy v1 SDK. +test('auto-instruments a legacy MCP SDK v1 server (no manual wrap)', async ({ baseURL }) => { + const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); + + const client = new Client({ + name: 'test-client-v1-auto', + version: '1.0.0', + }); + + const initializeSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v1-auto', + segment => segment.is_segment && segment.name === 'initialize', + ); + + await client.connect(transport); + + await test.step('initialize handshake', async () => { + const initializeSegment = await initializeSegmentPromise; + expect(initializeSegment).toBeDefined(); + expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); + expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v1-auto'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V1-Auto'); + }); + + await test.step('tool call', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v1-auto', + segment => segment.is_segment && segment.name === 'tools/call echo', + ); + + const toolResult = await client.callTool({ + name: 'echo', + arguments: { + message: 'foobar', + }, + }); + + expect(toolResult).toMatchObject({ + content: [ + { + text: 'Tool echo: foobar', + type: 'text', + }, + ], + }); + + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment.attributes?.['mcp.method.name']?.value).toEqual('tools/call'); + expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo'); + }); + + await test.step('resource read', async () => { + const resourceSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v1-auto', + segment => segment.is_segment && segment.name === 'resources/read', + ); + + const resourceResult = await client.readResource({ + uri: 'echo://foobar', + }); + + expect(resourceResult).toMatchObject({ + contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }], + }); + + const resourceSegment = await resourceSegmentPromise; + expect(resourceSegment).toBeDefined(); + expect(getSpanOp(resourceSegment)).toEqual('mcp.server'); + expect(resourceSegment.attributes?.['mcp.method.name']?.value).toEqual('resources/read'); + }); + + await test.step('error tool sets span status to error', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v1-auto', + segment => segment.is_segment && segment.name === 'tools/call always-error', + ); + + try { + await client.callTool({ name: 'always-error', arguments: {} }); + } catch { + // Expected: MCP SDK throws when the tool returns a JSON-RPC error + } + + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment?.status).toEqual('error'); + }); + + await client.close(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json new file mode 100644 index 000000000000..21ecf1357722 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2020"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs new file mode 100644 index 000000000000..f3dd95215d03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json new file mode 100644 index 000000000000..9593b764a32c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json @@ -0,0 +1,37 @@ +{ + "name": "node-express-mcp-v2-auto-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node --import ./instrument.mjs dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@cfworker/json-schema": "^4.0.0", + "@modelcontextprotocol/node": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@types/express": "^4.17.21", + "@types/node": "^18.19.1", + "express": "^4.21.2", + "typescript": "~5.0.0", + "zod": "^4.2.0" + }, + "devDependencies": { + "@modelcontextprotocol/client": "^2.0.0", + "@playwright/test": "~1.63.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/core": "file:../../packed/sentry-core-packed.tgz" + }, + "type": "module", + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts new file mode 100644 index 000000000000..3ed8df4a040a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; +import { mcpRouter } from './mcp.js'; + +const app = express(); +const port = 3030; + +app.use(express.json()); +app.use(mcpRouter); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts new file mode 100644 index 000000000000..eba162d8c151 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts @@ -0,0 +1,124 @@ +import { randomUUID } from 'node:crypto'; +import express from 'express'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { z } from 'zod'; + +const mcpRouter = express.Router(); + +// Intentionally NOT wrapped with `wrapMcpServerWithSentry`: the `mcpServer` integration +// auto-instruments the `McpServer` constructor, so spans must be produced anyway. +const server = new McpServer({ + name: 'Echo-V2-Auto', + version: '2.0.0', +}); + +server.registerResource( + 'echo', + new ResourceTemplate('echo://{message}', { list: undefined }), + { title: 'Echo Resource' }, + async (uri, { message }) => ({ + contents: [ + { + uri: uri.href, + text: `Resource echo: ${message}`, + }, + ], + }), +); + +server.registerTool( + 'echo', + { description: 'Echo tool', inputSchema: z.object({ message: z.string() }) }, + async ({ message }) => ({ + content: [{ type: 'text', text: `Tool echo: ${message}` }], + }), +); + +server.registerPrompt( + 'echo', + { description: 'Echo prompt', argsSchema: z.object({ message: z.string() }) }, + ({ message }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please process this message: ${message}`, + }, + }, + ], + }), +); + +server.registerTool('always-error', {}, async () => { + throw new Error('intentional error for span status testing'); +}); + +const transports: Record = {}; + +mcpRouter.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + try { + let transport: NodeStreamableHTTPServerTransport; + + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + } else if (!sessionId && req.body?.method === 'initialize') { + transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: sid => { + transports[sid] = transport; + }, + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid && transports[sid]) { + delete transports[sid]; + } + }; + + await server.connect(transport); + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, + id: null, + }); + return; + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { code: -32603, message: 'Internal server error' }, + id: null, + }); + } + } +}); + +mcpRouter.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); +}); + +mcpRouter.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); +}); + +export { mcpRouter }; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs new file mode 100644 index 000000000000..4866aaed167e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-mcp-v2-auto', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts new file mode 100644 index 000000000000..4ddfd69cffde --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts @@ -0,0 +1,134 @@ +import { expect, test } from '@playwright/test'; +import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; +import { Client } from '@modelcontextprotocol/client'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; + +// The server in this app is never wrapped with `wrapMcpServerWithSentry` — spans are produced +// solely by the auto-instrumenting `mcpServer` integration. +test('auto-instruments a stable MCP SDK v2 server (no manual wrap)', async ({ baseURL }) => { + const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); + + const client = new Client({ + name: 'test-client-v2-auto', + version: '1.0.0', + }); + + const initializeSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2-auto', + segment => segment.is_segment && segment.name === 'initialize', + ); + + await client.connect(transport); + + await test.step('initialize handshake', async () => { + const initializeSegment = await initializeSegmentPromise; + expect(initializeSegment).toBeDefined(); + expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); + expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v2-auto'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V2-Auto'); + expect(initializeSegment.attributes?.['mcp.transport']?.value).toMatch(/StreamableHTTPServerTransport/); + }); + + await test.step('registerTool handler', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2-auto', + segment => segment.is_segment && segment.name === 'tools/call echo', + ); + + const toolResult = await client.callTool({ + name: 'echo', + arguments: { + message: 'foobar', + }, + }); + + expect(toolResult).toMatchObject({ + content: [ + { + text: 'Tool echo: foobar', + type: 'text', + }, + ], + }); + + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment.attributes?.['mcp.method.name']?.value).toEqual('tools/call'); + expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo'); + // Proves span was completed with results (span correlation worked end-to-end) + expect(toolSegment.attributes?.['mcp.tool.result.content_count']?.value).toEqual(1); + }); + + await test.step('registerResource handler', async () => { + const resourceSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2-auto', + segment => segment.is_segment && segment.name === 'resources/read', + ); + + const resourceResult = await client.readResource({ + uri: 'echo://foobar', + }); + + expect(resourceResult).toMatchObject({ + contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }], + }); + + const resourceSegment = await resourceSegmentPromise; + expect(resourceSegment).toBeDefined(); + expect(getSpanOp(resourceSegment)).toEqual('mcp.server'); + expect(resourceSegment.attributes?.['mcp.method.name']?.value).toEqual('resources/read'); + }); + + await test.step('registerPrompt handler', async () => { + const promptSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2-auto', + segment => segment.is_segment && segment.name === 'prompts/get echo', + ); + + const promptResult = await client.getPrompt({ + name: 'echo', + arguments: { + message: 'foobar', + }, + }); + + expect(promptResult).toMatchObject({ + messages: [ + { + content: { + text: 'Please process this message: foobar', + type: 'text', + }, + role: 'user', + }, + ], + }); + + const promptSegment = await promptSegmentPromise; + expect(promptSegment).toBeDefined(); + expect(getSpanOp(promptSegment)).toEqual('mcp.server'); + expect(promptSegment.attributes?.['mcp.method.name']?.value).toEqual('prompts/get'); + }); + + await test.step('error tool sets span status to error', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2-auto', + segment => segment.is_segment && segment.name === 'tools/call always-error', + ); + + try { + await client.callTool({ name: 'always-error', arguments: {} }); + } catch { + // Expected: MCP SDK throws when the tool returns a JSON-RPC error + } + + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment?.status).toEqual('error'); + }); + + await client.close(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json new file mode 100644 index 000000000000..21ecf1357722 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2020"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} From 59cc5287f544a10e081dff5d9396e2481de116f9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 21 Sep 2026 10:29:11 +0200 Subject: [PATCH 2/2] test(e2e): Rename MCP auto apps; replace manual node-express-mcp-v2 Drop the `-auto` suffix now that auto-instrumentation is the default path: - `node-express-mcp-v1-auto` -> `node-express-mcp-v1` - `node-express-mcp-v2-auto` -> `node-express-mcp-v2`, replacing the previous manual-wrap app of that name (same assertions, now exercising the default auto path). Manual-wrap coverage remains via the cloudflare-mcp apps and the v1 `node-express*` apps. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../instrument.mjs | 0 .../package.json | 2 +- .../playwright.config.mjs | 0 .../src/app.ts | 0 .../src/mcp.ts | 2 +- .../start-event-proxy.mjs | 2 +- .../tests/mcp.test.ts | 14 +- .../tsconfig.json | 0 .../node-express-mcp-v2-auto/instrument.mjs | 9 -- .../node-express-mcp-v2-auto/package.json | 37 ----- .../playwright.config.mjs | 7 - .../node-express-mcp-v2-auto/src/app.ts | 17 --- .../node-express-mcp-v2-auto/src/mcp.ts | 124 ---------------- .../start-event-proxy.mjs | 6 - .../tests/mcp.test.ts | 134 ------------------ .../node-express-mcp-v2-auto/tsconfig.json | 13 -- .../node-express-mcp-v2/src/mcp.ts | 13 +- .../node-express-mcp-v2/tests/mcp.test.ts | 4 +- 18 files changed, 19 insertions(+), 365 deletions(-) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/instrument.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/package.json (95%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/playwright.config.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/src/app.ts (100%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/src/mcp.ts (99%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/start-event-proxy.mjs (70%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/tests/mcp.test.ts (93%) rename dev-packages/e2e-tests/test-applications/{node-express-mcp-v1-auto => node-express-mcp-v1}/tsconfig.json (100%) delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/instrument.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/instrument.mjs rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/instrument.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json similarity index 95% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json index 29ddfa6a60fd..d82b94ec77f4 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json @@ -1,5 +1,5 @@ { - "name": "node-express-mcp-v1-auto-app", + "name": "node-express-mcp-v1-app", "version": "1.0.0", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/src/app.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/app.ts rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/src/app.ts diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/src/mcp.ts similarity index 99% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/src/mcp.ts index 004cf2f89287..c1a9dfea3622 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/src/mcp.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/src/mcp.ts @@ -9,7 +9,7 @@ const mcpRouter = express.Router(); // Intentionally NOT wrapped with `wrapMcpServerWithSentry`: the `mcpServer` integration // auto-instruments the `McpServer` constructor, so spans must be produced anyway. const server = new McpServer({ - name: 'Echo-V1-Auto', + name: 'Echo-V1', version: '1.0.0', }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/start-event-proxy.mjs similarity index 70% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/start-event-proxy.mjs index 79a2c31587f2..69bb0c645c0a 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'node-express-mcp-v1-auto', + proxyServerName: 'node-express-mcp-v1', }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/tests/mcp.test.ts similarity index 93% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/tests/mcp.test.ts index 27d280e5d072..0e34722b1243 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/tests/mcp.test.ts @@ -9,12 +9,12 @@ test('auto-instruments a legacy MCP SDK v1 server (no manual wrap)', async ({ ba const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); const client = new Client({ - name: 'test-client-v1-auto', + name: 'test-client-v1', version: '1.0.0', }); const initializeSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v1-auto', + 'node-express-mcp-v1', segment => segment.is_segment && segment.name === 'initialize', ); @@ -25,13 +25,13 @@ test('auto-instruments a legacy MCP SDK v1 server (no manual wrap)', async ({ ba expect(initializeSegment).toBeDefined(); expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); - expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v1-auto'); - expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V1-Auto'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v1'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V1'); }); await test.step('tool call', async () => { const toolSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v1-auto', + 'node-express-mcp-v1', segment => segment.is_segment && segment.name === 'tools/call echo', ); @@ -60,7 +60,7 @@ test('auto-instruments a legacy MCP SDK v1 server (no manual wrap)', async ({ ba await test.step('resource read', async () => { const resourceSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v1-auto', + 'node-express-mcp-v1', segment => segment.is_segment && segment.name === 'resources/read', ); @@ -80,7 +80,7 @@ test('auto-instruments a legacy MCP SDK v1 server (no manual wrap)', async ({ ba await test.step('error tool sets span status to error', async () => { const toolSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v1-auto', + 'node-express-mcp-v1', segment => segment.is_segment && segment.name === 'tools/call always-error', ); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-express-mcp-v1-auto/tsconfig.json rename to dev-packages/e2e-tests/test-applications/node-express-mcp-v1/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs deleted file mode 100644 index f3dd95215d03..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/instrument.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/node'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - debug: !!process.env.DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1, -}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json deleted file mode 100644 index 9593b764a32c..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "node-express-mcp-v2-auto-app", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "tsc", - "start": "node --import ./instrument.mjs dist/app.js", - "test": "playwright test", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test" - }, - "dependencies": { - "@cfworker/json-schema": "^4.0.0", - "@modelcontextprotocol/node": "^2.0.0", - "@modelcontextprotocol/server": "^2.0.0", - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@types/express": "^4.17.21", - "@types/node": "^18.19.1", - "express": "^4.21.2", - "typescript": "~5.0.0", - "zod": "^4.2.0" - }, - "devDependencies": { - "@modelcontextprotocol/client": "^2.0.0", - "@playwright/test": "~1.63.0", - "@sentry-internal/test-utils": "link:../../../test-utils", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz" - }, - "type": "module", - "volta": { - "extends": "../../package.json" - }, - "sentryTest": { - "optional": true - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs deleted file mode 100644 index 31f2b913b58b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/playwright.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig({ - startCommand: `pnpm start`, -}); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts deleted file mode 100644 index 3ed8df4a040a..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/app.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from '@sentry/node'; -import express from 'express'; -import { mcpRouter } from './mcp.js'; - -const app = express(); -const port = 3030; - -app.use(express.json()); -app.use(mcpRouter); - -app.get('/test-success', function (_req, res) { - res.send({ version: 'v1' }); -}); - -app.listen(port, () => { - console.log(`Example app listening on port ${port}`); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts deleted file mode 100644 index eba162d8c151..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/src/mcp.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import express from 'express'; -import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; -import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; -import { z } from 'zod'; - -const mcpRouter = express.Router(); - -// Intentionally NOT wrapped with `wrapMcpServerWithSentry`: the `mcpServer` integration -// auto-instruments the `McpServer` constructor, so spans must be produced anyway. -const server = new McpServer({ - name: 'Echo-V2-Auto', - version: '2.0.0', -}); - -server.registerResource( - 'echo', - new ResourceTemplate('echo://{message}', { list: undefined }), - { title: 'Echo Resource' }, - async (uri, { message }) => ({ - contents: [ - { - uri: uri.href, - text: `Resource echo: ${message}`, - }, - ], - }), -); - -server.registerTool( - 'echo', - { description: 'Echo tool', inputSchema: z.object({ message: z.string() }) }, - async ({ message }) => ({ - content: [{ type: 'text', text: `Tool echo: ${message}` }], - }), -); - -server.registerPrompt( - 'echo', - { description: 'Echo prompt', argsSchema: z.object({ message: z.string() }) }, - ({ message }) => ({ - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Please process this message: ${message}`, - }, - }, - ], - }), -); - -server.registerTool('always-error', {}, async () => { - throw new Error('intentional error for span status testing'); -}); - -const transports: Record = {}; - -mcpRouter.post('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - - try { - let transport: NodeStreamableHTTPServerTransport; - - if (sessionId && transports[sessionId]) { - transport = transports[sessionId]; - } else if (!sessionId && req.body?.method === 'initialize') { - transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: sid => { - transports[sid] = transport; - }, - }); - - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - delete transports[sid]; - } - }; - - await server.connect(transport); - } else { - res.status(400).json({ - jsonrpc: '2.0', - error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, - id: null, - }); - return; - } - - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { code: -32603, message: 'Internal server error' }, - id: null, - }); - } - } -}); - -mcpRouter.get('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - await transports[sessionId].handleRequest(req, res); -}); - -mcpRouter.delete('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - await transports[sessionId].handleRequest(req, res); -}); - -export { mcpRouter }; diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs deleted file mode 100644 index 4866aaed167e..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'node-express-mcp-v2-auto', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts deleted file mode 100644 index 4ddfd69cffde..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tests/mcp.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; -import { Client } from '@modelcontextprotocol/client'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; - -// The server in this app is never wrapped with `wrapMcpServerWithSentry` — spans are produced -// solely by the auto-instrumenting `mcpServer` integration. -test('auto-instruments a stable MCP SDK v2 server (no manual wrap)', async ({ baseURL }) => { - const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); - - const client = new Client({ - name: 'test-client-v2-auto', - version: '1.0.0', - }); - - const initializeSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v2-auto', - segment => segment.is_segment && segment.name === 'initialize', - ); - - await client.connect(transport); - - await test.step('initialize handshake', async () => { - const initializeSegment = await initializeSegmentPromise; - expect(initializeSegment).toBeDefined(); - expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); - expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); - expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v2-auto'); - expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V2-Auto'); - expect(initializeSegment.attributes?.['mcp.transport']?.value).toMatch(/StreamableHTTPServerTransport/); - }); - - await test.step('registerTool handler', async () => { - const toolSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v2-auto', - segment => segment.is_segment && segment.name === 'tools/call echo', - ); - - const toolResult = await client.callTool({ - name: 'echo', - arguments: { - message: 'foobar', - }, - }); - - expect(toolResult).toMatchObject({ - content: [ - { - text: 'Tool echo: foobar', - type: 'text', - }, - ], - }); - - const toolSegment = await toolSegmentPromise; - expect(toolSegment).toBeDefined(); - expect(getSpanOp(toolSegment)).toEqual('mcp.server'); - expect(toolSegment.attributes?.['mcp.method.name']?.value).toEqual('tools/call'); - expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo'); - // Proves span was completed with results (span correlation worked end-to-end) - expect(toolSegment.attributes?.['mcp.tool.result.content_count']?.value).toEqual(1); - }); - - await test.step('registerResource handler', async () => { - const resourceSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v2-auto', - segment => segment.is_segment && segment.name === 'resources/read', - ); - - const resourceResult = await client.readResource({ - uri: 'echo://foobar', - }); - - expect(resourceResult).toMatchObject({ - contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }], - }); - - const resourceSegment = await resourceSegmentPromise; - expect(resourceSegment).toBeDefined(); - expect(getSpanOp(resourceSegment)).toEqual('mcp.server'); - expect(resourceSegment.attributes?.['mcp.method.name']?.value).toEqual('resources/read'); - }); - - await test.step('registerPrompt handler', async () => { - const promptSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v2-auto', - segment => segment.is_segment && segment.name === 'prompts/get echo', - ); - - const promptResult = await client.getPrompt({ - name: 'echo', - arguments: { - message: 'foobar', - }, - }); - - expect(promptResult).toMatchObject({ - messages: [ - { - content: { - text: 'Please process this message: foobar', - type: 'text', - }, - role: 'user', - }, - ], - }); - - const promptSegment = await promptSegmentPromise; - expect(promptSegment).toBeDefined(); - expect(getSpanOp(promptSegment)).toEqual('mcp.server'); - expect(promptSegment.attributes?.['mcp.method.name']?.value).toEqual('prompts/get'); - }); - - await test.step('error tool sets span status to error', async () => { - const toolSegmentPromise = waitForStreamedSpan( - 'node-express-mcp-v2-auto', - segment => segment.is_segment && segment.name === 'tools/call always-error', - ); - - try { - await client.callTool({ name: 'always-error', arguments: {} }); - } catch { - // Expected: MCP SDK throws when the tool returns a JSON-RPC error - } - - const toolSegment = await toolSegmentPromise; - expect(toolSegment).toBeDefined(); - expect(getSpanOp(toolSegment)).toEqual('mcp.server'); - expect(toolSegment?.status).toEqual('error'); - }); - - await client.close(); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json deleted file mode 100644 index 21ecf1357722..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2-auto/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "types": ["node"], - "esModuleInterop": true, - "lib": ["es2020"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -} diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/mcp.ts index 6034032b46df..86076f743d62 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/mcp.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/mcp.ts @@ -3,16 +3,15 @@ import express from 'express'; import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import { z } from 'zod'; -import { wrapMcpServerWithSentry } from '@sentry/node'; const mcpRouter = express.Router(); -const server = wrapMcpServerWithSentry( - new McpServer({ - name: 'Echo-V2', - version: '2.0.0', - }), -); +// Intentionally NOT wrapped with `wrapMcpServerWithSentry`: the `mcpServer` integration +// auto-instruments the `McpServer` constructor, so spans must be produced anyway. +const server = new McpServer({ + name: 'Echo-V2', + version: '2.0.0', +}); server.registerResource( 'echo', diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts index 747ebda30062..c7e7e2dae2e2 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts @@ -3,7 +3,9 @@ import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; import { Client } from '@modelcontextprotocol/client'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; -test('records spans for stable MCP SDK v2 handlers using the register API', async ({ baseURL }) => { +// The server in this app is never wrapped with `wrapMcpServerWithSentry` — spans are produced +// solely by the auto-instrumenting `mcpServer` integration. +test('auto-instruments a stable MCP SDK v2 server (no manual wrap)', async ({ baseURL }) => { const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); const client = new Client({