From 0306960237acd7d9f2691b20948c9a563b02146d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 29 Apr 2026 11:06:23 +0200 Subject: [PATCH 1/2] test(cloudflare): Add e2e test for MCPAgent with DurableObject instrumentation This test ensures that the Sentry SDK properly instruments MCPAgent (which extends DurableObject) from the Cloudflare agents package. It verifies that MCP tool call spans are correctly created and linked. Ref: https://github.com/getsentry/sentry-javascript/issues/17598 Co-Authored-By: Claude Opus 4.5 --- .../cloudflare-mcp-agent/.gitignore | 1 + .../cloudflare-mcp-agent/package.json | 32 +++++++ .../cloudflare-mcp-agent/playwright.config.ts | 15 +++ .../cloudflare-mcp-agent/src/env.d.ts | 4 + .../cloudflare-mcp-agent/src/index.ts | 78 +++++++++++++++ .../start-event-proxy.mjs | 6 ++ .../cloudflare-mcp-agent/tests/index.test.ts | 96 +++++++++++++++++++ .../cloudflare-mcp-agent/tsconfig.json | 21 ++++ .../cloudflare-mcp-agent/vitest.config.mts | 11 +++ .../cloudflare-mcp-agent/wrangler.jsonc | 21 ++++ 10 files changed, 285 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/package.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tests/index.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/wrangler.jsonc diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/.gitignore new file mode 100644 index 000000000000..e71378008bf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/.gitignore @@ -0,0 +1 @@ +.wrangler diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/package.json new file mode 100644 index 000000000000..190582ec8d71 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/package.json @@ -0,0 +1,32 @@ +{ + "name": "cloudflare-mcp-agent", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\"", + "build": "wrangler deploy --dry-run", + "typecheck": "tsc --noEmit", + "cf-typegen": "wrangler types", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm typecheck && pnpm test:dev && pnpm test:prod", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "agents": "0.11.9", + "zod": "^4.3.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260426.0", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "typescript": "^6.0.3", + "wrangler": "^4.86.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/playwright.config.ts new file mode 100644 index 000000000000..5f22d56bb19c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/playwright.config.ts @@ -0,0 +1,15 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const APP_PORT = 38788; + +const config = getPlaywrightConfig({ + startCommand: `pnpm dev --port ${APP_PORT}`, + port: APP_PORT, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/env.d.ts new file mode 100644 index 000000000000..a936f6586952 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/env.d.ts @@ -0,0 +1,4 @@ +interface Env { + E2E_TEST_DSN: string; + MCP_AGENT: DurableObjectNamespace; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/index.ts new file mode 100644 index 000000000000..964ab22cce55 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/src/index.ts @@ -0,0 +1,78 @@ +import * as Sentry from '@sentry/cloudflare'; +import { McpAgent } from 'agents/mcp'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import * as z from 'zod'; + +class MyMCPAgentBase extends McpAgent> { + #mcpServer = new McpServer({ + name: 'cloudflare-mcp-agent', + version: '1.0.0', + }); + + get server() { + return Sentry.wrapMcpServerWithSentry(this.#mcpServer); + } + + async init(): Promise { + this.#mcpServer.registerTool( + 'my-tool', + { + title: 'My Tool', + description: 'My Tool Description', + inputSchema: { + message: z.string(), + }, + }, + async ({ message }) => { + const span = Sentry.getActiveSpan(); + + await new Promise(resolve => setTimeout(resolve, 500)); + + if (span) { + span.setAttribute('mcp.tool.name', 'my-tool'); + span.setAttribute('mcp.tool.extra', 'from-mcpagent'); + span.setAttribute('mcp.tool.input', JSON.stringify({ message })); + } + + return { + content: [ + { + type: 'text' as const, + text: `Tool my-tool: ${message}`, + }, + ], + }; + }, + ); + } +} + +export const MyMCPAgent = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1.0, + sendDefaultPii: true, + debug: true, + transportOptions: { + bufferSize: 1000, + }, + }), + MyMCPAgentBase, +); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1.0, + sendDefaultPii: true, + debug: true, + transportOptions: { + bufferSize: 1000, + }, + }), + MyMCPAgent.serve('/mcp', { binding: 'MCP_AGENT' }), +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/start-event-proxy.mjs new file mode 100644 index 000000000000..946988f3fdc7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'cloudflare-mcp-agent', +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tests/index.test.ts new file mode 100644 index 000000000000..cde74a76aa27 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tests/index.test.ts @@ -0,0 +1,96 @@ +import { expect, test } from '@playwright/test'; +import { waitForRequest } from '@sentry-internal/test-utils'; + +test('sends spans for MCP tool calls via MCPAgent (DurableObject)', async ({ baseURL }) => { + const mcpToolWaiter = waitForRequest('cloudflare-mcp-agent', event => { + const transaction = event.envelope[1][0][1]; + return ( + typeof transaction !== 'string' && + 'transaction' in transaction && + transaction.transaction === 'tools/call my-tool' + ); + }); + + // Step 1: Initialize the MCP session + const initResponse = await fetch(`${baseURL}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 0, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + }), + }); + + expect(initResponse.status).toBe(200); + const sessionId = initResponse.headers.get('Mcp-Session-Id'); + expect(sessionId).toBeTruthy(); + + // Step 2: Send initialized notification + await fetch(`${baseURL}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'Mcp-Session-Id': sessionId!, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'notifications/initialized', + }), + }); + + // Step 3: Call the tool with the session ID + const response = await fetch(`${baseURL}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'Mcp-Session-Id': sessionId!, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'my-tool', + arguments: { + message: 'hello from MCPAgent test', + }, + }, + }), + }); + + expect(response.status).toBe(200); + + const mcpData = await mcpToolWaiter; + const mcpEvent = mcpData.envelope[1][0][1]; + + expect(mcpEvent.contexts?.trace?.trace_id).toBe(mcpData.envelope[0].trace.trace_id); + expect(mcpEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + parent_span_id: expect.any(String), + span_id: expect.any(String), + op: 'mcp.server', + origin: 'auto.function.mcp_server', + data: expect.objectContaining({ + 'sentry.origin': 'auto.function.mcp_server', + 'sentry.op': 'mcp.server', + 'mcp.method.name': 'tools/call', + 'mcp.tool.name': 'my-tool', + 'mcp.tool.extra': 'from-mcpagent', + 'mcp.tool.input': '{"message":"hello from MCPAgent test"}', + }), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tsconfig.json new file mode 100644 index 000000000000..2e9384f1b328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "jsx": "react-jsx", + "module": "es2022", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "types": ["@cloudflare/workers-types/experimental"] + }, + "exclude": ["test"], + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts new file mode 100644 index 000000000000..931e5113e0c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: './wrangler.toml' }, + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/wrangler.jsonc new file mode 100644 index 000000000000..a29277811225 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/wrangler.jsonc @@ -0,0 +1,21 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cloudflare-mcp-agent", + "main": "src/index.ts", + "compatibility_date": "2025-03-21", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [ + { + "name": "MCP_AGENT", + "class_name": "MyMCPAgent", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MyMCPAgent"], + }, + ], +} From 1175c5227e4dbaf0632a9c53f77cf1b8c402af23 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 30 Apr 2026 11:01:17 +0200 Subject: [PATCH 2/2] fixup! test(cloudflare): Add e2e test for MCPAgent with DurableObject instrumentation --- .../cloudflare-mcp-agent/vitest.config.mts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts deleted file mode 100644 index 931e5113e0c2..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp-agent/vitest.config.mts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; - -export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { configPath: './wrangler.toml' }, - }, - }, - }, -});