Skip to content
Merged
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
@@ -0,0 +1 @@
.wrangler
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface Env {
E2E_TEST_DSN: string;
MCP_AGENT: DurableObjectNamespace;
}
Original file line number Diff line number Diff line change
@@ -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<Env, unknown, Record<string, unknown>> {
#mcpServer = new McpServer({
name: 'cloudflare-mcp-agent',
version: '1.0.0',
});

get server() {
return Sentry.wrapMcpServerWithSentry(this.#mcpServer);
}

async init(): Promise<void> {
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' }),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'cloudflare-mcp-agent',
});
Original file line number Diff line number Diff line change
@@ -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"}',
}),
});
});
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
@@ -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"],
},
],
}
Loading