From 31b761478e4102d300e95765b5fcb61038c431f5 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 31 Jul 2026 13:32:49 +0200 Subject: [PATCH] feat(core)!: Use `rpc` span op for MCP and tRPC --- .../cloudflare-mcp-agent/tests/index.test.ts | 4 +- .../cloudflare-mcp/tests/index.test.ts | 8 ++- .../node-express-mcp-v2/tests/mcp.test.ts | 10 +-- .../node-express-streaming/tests/mcp.test.ts | 40 ++++++------ .../node-express-streaming/tests/trpc.test.ts | 12 ++-- .../node-express-v5/tests/mcp.test.ts | 12 ++-- .../node-express-v5/tests/trpc.test.ts | 4 +- .../node-express/tests/mcp.test.ts | 20 +++--- .../node-express/tests/trpc.test.ts | 4 +- .../tsx-express/tests/mcp.test.ts | 16 ++--- .../tsx-express/tests/trpc.test.ts | 4 +- .../mcp-server/attributeExtraction.ts | 13 +++- .../src/integrations/mcp-server/attributes.ts | 24 ++++--- .../core/src/integrations/mcp-server/spans.ts | 28 +++----- packages/core/src/trpc.ts | 14 +++- .../mcp-server/semanticConventions.test.ts | 64 +++++++++++++------ .../transportInstrumentation.test.ts | 18 ++++-- packages/core/test/lib/trpc.test.ts | 6 +- 18 files changed, 175 insertions(+), 126 deletions(-) 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 index d3324d81dee8..3a014ccb8fca 100644 --- 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 @@ -82,12 +82,12 @@ test('sends spans for MCP tool calls via MCPAgent (DurableObject)', async ({ bas trace_id: expect.any(String), parent_span_id: expect.any(String), span_id: expect.any(String), - op: 'mcp.server', + op: 'rpc', origin: 'auto.function.mcp_server', status: 'ok', data: expect.objectContaining({ 'sentry.origin': 'auto.function.mcp_server', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'mcp.method.name': 'tools/call', 'mcp.tool.name': 'my-tool', 'mcp.tool.extra': 'from-mcpagent', diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts index a7bc516e8dae..9e87acfea74e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts @@ -87,13 +87,17 @@ test('sends spans for MCP tool calls', async ({ baseURL }) => { trace_id: expect.any(String), parent_span_id: requestEvent.contexts?.trace?.span_id, span_id: expect.any(String), - op: 'mcp.server', + op: 'rpc', origin: 'auto.function.mcp_server', status: 'ok', data: { 'sentry.origin': 'auto.function.mcp_server', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', + 'rpc.method': 'tools/call', + 'jsonrpc.protocol.version': '2.0', + 'jsonrpc.request.id': '1', 'mcp.transport': 'WorkerTransport', 'network.transport': 'unknown', 'network.protocol.version': '2.0', 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 776725c11cf2..2eb20e5e3490 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 @@ -22,7 +22,7 @@ test('Should record transactions for MCP handlers using @modelcontextprotocol/sd await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-v2'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-V2'); @@ -52,7 +52,7 @@ test('Should record transactions for MCP handlers using @modelcontextprotocol/sd const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); // Proves span was completed with results (span correlation worked end-to-end) @@ -74,7 +74,7 @@ test('Should record transactions for MCP handlers using @modelcontextprotocol/sd const resourceTransaction = await resourceTransactionPromise; expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(resourceTransaction.contexts?.trace?.op).toEqual('rpc'); expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); }); @@ -104,7 +104,7 @@ test('Should record transactions for MCP handlers using @modelcontextprotocol/sd const promptTransaction = await promptTransactionPromise; expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(promptTransaction.contexts?.trace?.op).toEqual('rpc'); expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); }); @@ -121,7 +121,7 @@ test('Should record transactions for MCP handlers using @modelcontextprotocol/sd const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts index 08e1e51b1ca2..e7d296d42626 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts @@ -16,7 +16,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = }); const initializeSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'initialize' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'initialize' && getSpanOp(span) === 'rpc' && span.is_segment; }); await client.connect(transport); @@ -24,7 +24,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = await test.step('initialize handshake', async () => { const initializeSpan = await initializeSpanPromise; expect(initializeSpan).toBeDefined(); - expect(getSpanOp(initializeSpan)).toBe('mcp.server'); + expect(getSpanOp(initializeSpan)).toBe('rpc'); expect(initializeSpan.attributes['mcp.method.name']?.value).toBe('initialize'); expect(initializeSpan.attributes['mcp.client.name']?.value).toBe('test-client'); expect(initializeSpan.attributes['mcp.server.name']?.value).toBe('Echo'); @@ -35,7 +35,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = return span.name === 'POST /messages' && getSpanOp(span) === 'http.server' && span.is_segment; }); const toolSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'tools/call echo' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'tools/call echo' && getSpanOp(span) === 'rpc' && span.is_segment; }); const toolResult = await client.callTool({ @@ -60,7 +60,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = const toolSpan = await toolSpanPromise; expect(toolSpan).toBeDefined(); - expect(getSpanOp(toolSpan)).toBe('mcp.server'); + expect(getSpanOp(toolSpan)).toBe('rpc'); expect(toolSpan.attributes['mcp.method.name']?.value).toBe('tools/call'); }); @@ -69,7 +69,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = return span.name === 'POST /messages' && getSpanOp(span) === 'http.server' && span.is_segment; }); const toolSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'tools/call echo-register' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'tools/call echo-register' && getSpanOp(span) === 'rpc' && span.is_segment; }); const toolResult = await client.callTool({ @@ -94,7 +94,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = const toolSpan = await toolSpanPromise; expect(toolSpan).toBeDefined(); - expect(getSpanOp(toolSpan)).toBe('mcp.server'); + expect(getSpanOp(toolSpan)).toBe('rpc'); expect(toolSpan.attributes['mcp.method.name']?.value).toBe('tools/call'); expect(toolSpan.attributes['mcp.tool.name']?.value).toBe('echo-register'); }); @@ -104,7 +104,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = return span.name === 'POST /messages' && getSpanOp(span) === 'http.server' && span.is_segment; }); const resourceSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'resources/read echo://foobar' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'resources/read echo://foobar' && getSpanOp(span) === 'rpc' && span.is_segment; }); const resourceResult = await client.readResource({ @@ -121,7 +121,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = const resourceSpan = await resourceSpanPromise; expect(resourceSpan).toBeDefined(); - expect(getSpanOp(resourceSpan)).toBe('mcp.server'); + expect(getSpanOp(resourceSpan)).toBe('rpc'); expect(resourceSpan.attributes['mcp.method.name']?.value).toBe('resources/read'); }); @@ -130,7 +130,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = return span.name === 'POST /messages' && getSpanOp(span) === 'http.server' && span.is_segment; }); const promptSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'prompts/get echo' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'prompts/get echo' && getSpanOp(span) === 'rpc' && span.is_segment; }); const promptResult = await client.getPrompt({ @@ -158,13 +158,13 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = const promptSpan = await promptSpanPromise; expect(promptSpan).toBeDefined(); - expect(getSpanOp(promptSpan)).toBe('mcp.server'); + expect(getSpanOp(promptSpan)).toBe('rpc'); expect(promptSpan.attributes['mcp.method.name']?.value).toBe('prompts/get'); }); await test.step('error tool sets span status to error', async () => { const toolSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'tools/call always-error' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'tools/call always-error' && getSpanOp(span) === 'rpc' && span.is_segment; }); try { @@ -175,7 +175,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = const toolSpan = await toolSpanPromise; expect(toolSpan).toBeDefined(); - expect(getSpanOp(toolSpan)).toBe('mcp.server'); + expect(getSpanOp(toolSpan)).toBe('rpc'); expect(toolSpan.status).toBe('error'); }); }); @@ -191,7 +191,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const initializeSpanPromise = waitForStreamedSpan('node-express-streaming', span => { return ( span.name === 'initialize' && - getSpanOp(span) === 'mcp.server' && + getSpanOp(span) === 'rpc' && span.attributes['mcp.server.name']?.value === 'Echo-Streamable' ); }); @@ -201,7 +201,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp await test.step('initialize handshake', async () => { const initializeSpan = await initializeSpanPromise; expect(initializeSpan).toBeDefined(); - expect(getSpanOp(initializeSpan)).toBe('mcp.server'); + expect(getSpanOp(initializeSpan)).toBe('rpc'); expect(initializeSpan.attributes['mcp.method.name']?.value).toBe('initialize'); expect(initializeSpan.attributes['mcp.client.name']?.value).toBe('test-client-streamable'); expect(initializeSpan.attributes['mcp.server.name']?.value).toBe('Echo-Streamable'); @@ -212,7 +212,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const toolSpanPromise = waitForStreamedSpan('node-express-streaming', span => { return ( span.name === 'tools/call echo' && - getSpanOp(span) === 'mcp.server' && + getSpanOp(span) === 'rpc' && String(span.attributes['mcp.transport']?.value).includes('StreamableHTTPServerTransport') ); }); @@ -235,7 +235,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const toolSpan = await toolSpanPromise; expect(toolSpan).toBeDefined(); - expect(getSpanOp(toolSpan)).toBe('mcp.server'); + expect(getSpanOp(toolSpan)).toBe('rpc'); expect(toolSpan.attributes['mcp.method.name']?.value).toBe('tools/call'); expect(toolSpan.attributes['mcp.tool.name']?.value).toBe('echo'); expect(toolSpan.attributes['mcp.tool.result.content_count']?.value).toBe(1); @@ -245,7 +245,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const resourceSpanPromise = waitForStreamedSpan('node-express-streaming', span => { return ( span.name === 'resources/read echo://streamable-test' && - getSpanOp(span) === 'mcp.server' && + getSpanOp(span) === 'rpc' && String(span.attributes['mcp.transport']?.value).includes('StreamableHTTPServerTransport') ); }); @@ -260,7 +260,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const resourceSpan = await resourceSpanPromise; expect(resourceSpan).toBeDefined(); - expect(getSpanOp(resourceSpan)).toBe('mcp.server'); + expect(getSpanOp(resourceSpan)).toBe('rpc'); expect(resourceSpan.attributes['mcp.method.name']?.value).toBe('resources/read'); }); @@ -268,7 +268,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const promptSpanPromise = waitForStreamedSpan('node-express-streaming', span => { return ( span.name === 'prompts/get echo' && - getSpanOp(span) === 'mcp.server' && + getSpanOp(span) === 'rpc' && String(span.attributes['mcp.transport']?.value).includes('StreamableHTTPServerTransport') ); }); @@ -294,7 +294,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp const promptSpan = await promptSpanPromise; expect(promptSpan).toBeDefined(); - expect(getSpanOp(promptSpan)).toBe('mcp.server'); + expect(getSpanOp(promptSpan)).toBe('rpc'); expect(promptSpan.attributes['mcp.method.name']?.value).toBe('prompts/get'); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/trpc.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/trpc.test.ts index 1253a66627bc..0824d6142389 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/trpc.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/trpc.test.ts @@ -5,7 +5,7 @@ import type { AppRouter } from '../src/app'; test('Should record streamed span for trpc query', async ({ baseURL }) => { const trpcSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'trpc/getSomething' && getSpanOp(span) === 'rpc.server'; + return span.name === 'trpc/getSomething' && getSpanOp(span) === 'rpc'; }); const trpcClient = createTRPCProxyClient({ @@ -21,13 +21,13 @@ test('Should record streamed span for trpc query', async ({ baseURL }) => { const trpcSpan = await trpcSpanPromise; expect(trpcSpan).toBeDefined(); expect(trpcSpan.name).toBe('trpc/getSomething'); - expect(getSpanOp(trpcSpan)).toBe('rpc.server'); + expect(getSpanOp(trpcSpan)).toBe('rpc'); expect(trpcSpan.attributes['sentry.origin']?.value).toBe('auto.rpc.trpc'); }); test('Should record streamed span for trpc mutation', async ({ baseURL }) => { const trpcSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'trpc/createSomething' && getSpanOp(span) === 'rpc.server'; + return span.name === 'trpc/createSomething' && getSpanOp(span) === 'rpc'; }); const trpcClient = createTRPCProxyClient({ @@ -43,13 +43,13 @@ test('Should record streamed span for trpc mutation', async ({ baseURL }) => { const trpcSpan = await trpcSpanPromise; expect(trpcSpan).toBeDefined(); expect(trpcSpan.name).toBe('trpc/createSomething'); - expect(getSpanOp(trpcSpan)).toBe('rpc.server'); + expect(getSpanOp(trpcSpan)).toBe('rpc'); expect(trpcSpan.attributes['sentry.origin']?.value).toBe('auto.rpc.trpc'); }); test('Should record streamed span and error for a crashing trpc handler', async ({ baseURL }) => { const trpcSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'trpc/crashSomething' && getSpanOp(span) === 'rpc.server'; + return span.name === 'trpc/crashSomething' && getSpanOp(span) === 'rpc'; }); const errorEventPromise = waitForError('node-express-streaming', errorEvent => { @@ -83,7 +83,7 @@ test('Should record streamed span and error for a crashing trpc handler', async test('Should record streamed span and error for a trpc handler that returns a status code', async ({ baseURL }) => { const trpcSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'trpc/badRequest' && getSpanOp(span) === 'rpc.server'; + return span.name === 'trpc/badRequest' && getSpanOp(span) === 'rpc'; }); const errorEventPromise = waitForError('node-express-streaming', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts index c943ebfd4ab1..bf2312528a62 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts @@ -21,7 +21,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo'); @@ -166,7 +166,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); }); }); @@ -202,7 +202,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-streamable'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-Streamable'); @@ -236,7 +236,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); // This attribute proves the span was completed with results (sessionId correlation worked) @@ -262,7 +262,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const resourceTransaction = await resourceTransactionPromise; expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(resourceTransaction.contexts?.trace?.op).toEqual('rpc'); expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); }); @@ -295,7 +295,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const promptTransaction = await promptTransactionPromise; expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(promptTransaction.contexts?.trace?.op).toEqual('rpc'); expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts index 1618313ff444..33852add4123 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts @@ -27,7 +27,7 @@ test('Should record span for trpc query', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/getSomething`, @@ -59,7 +59,7 @@ test('Should record transaction for trpc mutation', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/createSomething`, diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts index 504bfaffcd27..c791e595d10d 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts @@ -21,7 +21,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo'); @@ -57,7 +57,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); // 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 }); @@ -92,7 +92,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); }); @@ -119,7 +119,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const resourceTransaction = await resourceTransactionPromise; expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(resourceTransaction.contexts?.trace?.op).toEqual('rpc'); expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); // 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 }); @@ -157,7 +157,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const promptTransaction = await promptTransactionPromise; expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(promptTransaction.contexts?.trace?.op).toEqual('rpc'); expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); // 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 }); @@ -175,7 +175,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); }); }); @@ -211,7 +211,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-streamable'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-Streamable'); @@ -245,7 +245,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); // This attribute proves the span was completed with results (sessionId correlation worked) @@ -271,7 +271,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const resourceTransaction = await resourceTransactionPromise; expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(resourceTransaction.contexts?.trace?.op).toEqual('rpc'); expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); }); @@ -304,7 +304,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const promptTransaction = await promptTransactionPromise; expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(promptTransaction.contexts?.trace?.op).toEqual('rpc'); expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/trpc.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/trpc.test.ts index e27789b7e4c5..a7a67ae08a6e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/trpc.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/trpc.test.ts @@ -27,7 +27,7 @@ test('Should record span for trpc query', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/getSomething`, @@ -59,7 +59,7 @@ test('Should record transaction for trpc mutation', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/createSomething`, diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts index f85eedf74b99..caa7b20ed428 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts @@ -21,7 +21,7 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => { await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo'); @@ -57,7 +57,7 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); // 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 @@ -92,7 +92,7 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); }); @@ -171,7 +171,7 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => { const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); }); }); @@ -207,7 +207,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor await test.step('initialize handshake', async () => { const initializeTransaction = await initializeTransactionPromise; expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(initializeTransaction.contexts?.trace?.op).toEqual('rpc'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-streamable'); expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-Streamable'); @@ -241,7 +241,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const toolTransaction = await toolTransactionPromise; expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(toolTransaction.contexts?.trace?.op).toEqual('rpc'); expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); // This attribute proves the span was completed with results (sessionId correlation worked) @@ -267,7 +267,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const resourceTransaction = await resourceTransactionPromise; expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(resourceTransaction.contexts?.trace?.op).toEqual('rpc'); expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); }); @@ -300,7 +300,7 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor const promptTransaction = await promptTransactionPromise; expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); + expect(promptTransaction.contexts?.trace?.op).toEqual('rpc'); expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); }); diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/trpc.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/trpc.test.ts index 6429b1255b85..cd0bb425540c 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/trpc.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/trpc.test.ts @@ -27,7 +27,7 @@ test('Records span for trpc query', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/getSomething`, @@ -59,7 +59,7 @@ test('Records transaction for trpc mutation', async ({ baseURL }) => { expect(transaction.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'sentry.op': 'rpc.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', }), description: `trpc/createSomething`, diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 75449f43ccc9..5e50fac9cabf 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -2,6 +2,7 @@ * Core attribute extraction and building functions for MCP server instrumentation */ +import { JSONRPC_PROTOCOL_VERSION, JSONRPC_REQUEST_ID, RPC_METHOD } from '@sentry/conventions/attributes'; import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url'; import { MCP_LOGGING_DATA_TYPE_ATTRIBUTE, @@ -117,13 +118,21 @@ export function buildTypeSpecificAttributes( const targetInfo = extractTargetInfo(request.method, params || {}); return { - ...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }), + [RPC_METHOD]: request.method, + [JSONRPC_PROTOCOL_VERSION]: request.jsonrpc, + ...(request.id !== undefined && { + [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id), + [JSONRPC_REQUEST_ID]: String(request.id), + }), ...targetInfo.attributes, ...(recordInputs ? getRequestArguments(request.method, params || {}) : {}), }; } - return getNotificationAttributes(message.method, params || {}, recordInputs); + return { + [RPC_METHOD]: message.method, + ...getNotificationAttributes(message.method, params || {}, recordInputs), + }; } // Re-export buildTransportAttributes for spans.ts diff --git a/packages/core/src/integrations/mcp-server/attributes.ts b/packages/core/src/integrations/mcp-server/attributes.ts index 273bdbdb9560..215e9d03e353 100644 --- a/packages/core/src/integrations/mcp-server/attributes.ts +++ b/packages/core/src/integrations/mcp-server/attributes.ts @@ -141,26 +141,24 @@ export const CLIENT_PORT_ATTRIBUTE = 'client.port'; // SENTRY-SPECIFIC MCP ATTRIBUTE VALUES // ============================================================================= -/** Sentry operation value for MCP server spans */ -export const MCP_SERVER_OP_VALUE = 'mcp.server'; - /** - * Sentry operation value for client-to-server notifications - * Following OpenTelemetry MCP semantic conventions + * Sentry operation value for all MCP spans (requests and notifications). + * MCP is JSON-RPC, so it shares the cross-protocol `rpc` op; the request/ + * notification distinction and direction live in attributes instead. */ -export const MCP_NOTIFICATION_CLIENT_TO_SERVER_OP_VALUE = 'mcp.notification.client_to_server'; +export const MCP_RPC_OP_VALUE = 'rpc'; -/** - * Sentry operation value for server-to-client notifications - * Following OpenTelemetry MCP semantic conventions - */ -export const MCP_NOTIFICATION_SERVER_TO_CLIENT_OP_VALUE = 'mcp.notification.server_to_client'; +/** `rpc.system` value identifying MCP as the RPC protocol */ +export const MCP_RPC_SYSTEM_VALUE = 'mcp'; /** Sentry origin value for MCP function spans */ export const MCP_FUNCTION_ORIGIN_VALUE = 'auto.function.mcp_server'; -/** Sentry origin value for MCP notification spans */ -export const MCP_NOTIFICATION_ORIGIN_VALUE = 'auto.mcp.notification'; +/** Sentry origin value for incoming (client→server) MCP notification spans */ +export const MCP_NOTIFICATION_INCOMING_ORIGIN_VALUE = 'auto.mcp.notification.client_to_server'; + +/** Sentry origin value for outgoing (server→client) MCP notification spans */ +export const MCP_NOTIFICATION_OUTGOING_ORIGIN_VALUE = 'auto.mcp.notification.server_to_client'; /** Sentry source value for MCP route spans */ export const MCP_ROUTE_SOURCE_VALUE = 'route'; diff --git a/packages/core/src/integrations/mcp-server/spans.ts b/packages/core/src/integrations/mcp-server/spans.ts index f7e1b42325d5..aaaea5fae38e 100644 --- a/packages/core/src/integrations/mcp-server/spans.ts +++ b/packages/core/src/integrations/mcp-server/spans.ts @@ -5,22 +5,19 @@ * Handles both request and notification spans with attribute extraction. */ +import { RPC_SYSTEM_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; import { getClient } from '../../currentScopes'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, -} from '../../semanticAttributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../semanticAttributes'; import { startSpan } from '../../tracing'; import { buildTransportAttributes, buildTypeSpecificAttributes } from './attributeExtraction'; import { MCP_FUNCTION_ORIGIN_VALUE, MCP_METHOD_NAME_ATTRIBUTE, - MCP_NOTIFICATION_CLIENT_TO_SERVER_OP_VALUE, - MCP_NOTIFICATION_ORIGIN_VALUE, - MCP_NOTIFICATION_SERVER_TO_CLIENT_OP_VALUE, + MCP_NOTIFICATION_INCOMING_ORIGIN_VALUE, + MCP_NOTIFICATION_OUTGOING_ORIGIN_VALUE, MCP_ROUTE_SOURCE_VALUE, - MCP_SERVER_OP_VALUE, + MCP_RPC_OP_VALUE, + MCP_RPC_SYSTEM_VALUE, } from './attributes'; import { extractTargetInfo } from './methodConfig'; import { filterMcpPiiFromSpanData } from './piiFiltering'; @@ -51,28 +48,25 @@ function createSpanName(method: string, target?: string): string { * @returns Sentry-specific attributes */ function buildSentryAttributes(type: McpSpanConfig['type']): Record { - let op: string; let origin: string; switch (type) { case 'request': - op = MCP_SERVER_OP_VALUE; origin = MCP_FUNCTION_ORIGIN_VALUE; break; case 'notification-incoming': - op = MCP_NOTIFICATION_CLIENT_TO_SERVER_OP_VALUE; - origin = MCP_NOTIFICATION_ORIGIN_VALUE; + origin = MCP_NOTIFICATION_INCOMING_ORIGIN_VALUE; break; case 'notification-outgoing': - op = MCP_NOTIFICATION_SERVER_TO_CLIENT_OP_VALUE; - origin = MCP_NOTIFICATION_ORIGIN_VALUE; + origin = MCP_NOTIFICATION_OUTGOING_ORIGIN_VALUE; break; } return { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_OP]: MCP_RPC_OP_VALUE, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: MCP_ROUTE_SOURCE_VALUE, + [RPC_SYSTEM_NAME]: MCP_RPC_SYSTEM_VALUE, }; } @@ -182,7 +176,6 @@ export function buildMcpServerSpanConfig( options?: ResolvedMcpOptions, ): { name: string; - op: string; forceTransaction: boolean; attributes: Record; } { @@ -205,7 +198,6 @@ export function buildMcpServerSpanConfig( return { name: spanName, - op: MCP_SERVER_OP_VALUE, forceTransaction: true, attributes, }; diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 53c55bca3c32..8fe3137ff4a9 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -1,3 +1,11 @@ +import { + RPC_METHOD, + RPC_SYSTEM_NAME, + SENTRY_OP, + TRPC_PROCEDURE_PATH, + TRPC_PROCEDURE_TYPE, +} from '@sentry/conventions/attributes'; +import { WEB_SERVER_RPC_SPAN_OP } from '@sentry/conventions/op'; import { getClient, withIsolationScope } from './currentScopes'; import { captureException } from './exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes'; @@ -85,10 +93,14 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return startSpanManual( { name: `trpc/${path}`, - op: 'rpc.server', attributes: { + [SENTRY_OP]: WEB_SERVER_RPC_SPAN_OP, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc', + [RPC_SYSTEM_NAME]: 'trpc', + [RPC_METHOD]: String(path), + [TRPC_PROCEDURE_PATH]: String(path), + [TRPC_PROCEDURE_TYPE]: String(type), }, forceTransaction: !!options.forceTransaction, }, diff --git a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts index 06b3cef96592..eaf6794e4418 100644 --- a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts @@ -47,12 +47,14 @@ describe('MCP Server Semantic Conventions', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith({ name: 'tools/call get-weather', - op: 'mcp.server', forceTransaction: true, attributes: { 'mcp.method.name': 'tools/call', + 'rpc.method': 'tools/call', + 'jsonrpc.protocol.version': '2.0', 'mcp.tool.name': 'get-weather', 'mcp.request.id': 'req-1', + 'jsonrpc.request.id': 'req-1', 'mcp.session.id': 'test-session-123', 'client.address': '192.168.1.100', 'client.port': 54321, @@ -60,9 +62,10 @@ describe('MCP Server Semantic Conventions', () => { 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.location': '"Seattle, WA"', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }); }); @@ -81,20 +84,23 @@ describe('MCP Server Semantic Conventions', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith({ name: 'resources/read file:///docs/api.md', - op: 'mcp.server', forceTransaction: true, attributes: { 'mcp.method.name': 'resources/read', + 'rpc.method': 'resources/read', + 'jsonrpc.protocol.version': '2.0', 'mcp.resource.uri': 'file:///docs/api.md', 'mcp.request.id': 'req-2', + 'jsonrpc.request.id': 'req-2', 'mcp.session.id': 'test-session-123', 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.uri': '"file:///docs/api.md"', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }); }); @@ -113,20 +119,23 @@ describe('MCP Server Semantic Conventions', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith({ name: 'prompts/get analyze-code', - op: 'mcp.server', forceTransaction: true, attributes: { 'mcp.method.name': 'prompts/get', + 'rpc.method': 'prompts/get', + 'jsonrpc.protocol.version': '2.0', 'mcp.prompt.name': 'analyze-code', 'mcp.request.id': 'req-3', + 'jsonrpc.request.id': 'req-3', 'mcp.session.id': 'test-session-123', 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.name': '"analyze-code"', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }); }); @@ -148,13 +157,15 @@ describe('MCP Server Semantic Conventions', () => { forceTransaction: true, attributes: { 'mcp.method.name': 'notifications/tools/list_changed', + 'rpc.method': 'notifications/tools/list_changed', 'mcp.session.id': 'test-session-123', 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', - 'sentry.op': 'mcp.notification.client_to_server', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.client_to_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }, expect.any(Function), @@ -185,16 +196,19 @@ describe('MCP Server Semantic Conventions', () => { forceTransaction: true, attributes: expect.objectContaining({ 'mcp.method.name': 'tools/list', + 'rpc.method': 'tools/list', 'mcp.request.id': 'req-4', + 'jsonrpc.request.id': 'req-4', 'mcp.session.id': 'test-session-123', // Transport attributes 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', // Sentry-specific - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }), ); @@ -221,6 +235,7 @@ describe('MCP Server Semantic Conventions', () => { forceTransaction: true, attributes: { 'mcp.method.name': 'notifications/message', + 'rpc.method': 'notifications/message', 'mcp.session.id': 'test-session-123', 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', @@ -229,9 +244,10 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.logging.logger': 'math-service', 'mcp.logging.data_type': 'string', 'mcp.logging.message': 'Addition completed: 2 + 5 = 7', - 'sentry.op': 'mcp.notification.client_to_server', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.client_to_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }, expect.any(Function), @@ -260,9 +276,10 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.method.name': 'notifications/cancelled', 'mcp.cancelled.request_id': 'req-123', 'mcp.cancelled.reason': 'user_requested', - 'sentry.op': 'mcp.notification.client_to_server', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.client_to_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }), expect.any(Function), @@ -294,9 +311,10 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.progress.total': 100, 'mcp.progress.percentage': 75, 'mcp.progress.message': 'Processing files...', - 'sentry.op': 'mcp.notification.client_to_server', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.client_to_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }), expect.any(Function), @@ -322,9 +340,10 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.method.name': 'notifications/resources/updated', 'mcp.resource.uri': 'file:///tmp/data.json', 'mcp.resource.protocol': 'file', - 'sentry.op': 'mcp.notification.client_to_server', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.client_to_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }), expect.any(Function), @@ -346,9 +365,10 @@ describe('MCP Server Semantic Conventions', () => { name: 'notifications/tools/list_changed', attributes: expect.objectContaining({ 'mcp.method.name': 'notifications/tools/list_changed', - 'sentry.op': 'mcp.notification.server_to_client', - 'sentry.origin': 'auto.mcp.notification', + 'sentry.op': 'rpc', + 'sentry.origin': 'auto.mcp.notification.server_to_client', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }), expect.any(Function), @@ -387,12 +407,13 @@ describe('MCP Server Semantic Conventions', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'tools/call weather-lookup', - op: 'mcp.server', forceTransaction: true, attributes: expect.objectContaining({ 'mcp.method.name': 'tools/call', 'mcp.tool.name': 'weather-lookup', 'mcp.request.id': 'req-tool-result', + 'sentry.op': 'rpc', + 'rpc.system.name': 'mcp', }), }), ); @@ -460,12 +481,13 @@ describe('MCP Server Semantic Conventions', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'prompts/get code-review', - op: 'mcp.server', forceTransaction: true, attributes: expect.objectContaining({ 'mcp.method.name': 'prompts/get', 'mcp.prompt.name': 'code-review', 'mcp.request.id': 'req-prompt-result', + 'sentry.op': 'rpc', + 'rpc.system.name': 'mcp', }), }), ); diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index a7b00bb3b1c6..b3087bd55211 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -269,20 +269,23 @@ describe('MCP Server Transport Instrumentation', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith({ name: 'tools/call process-file', - op: 'mcp.server', forceTransaction: true, attributes: { 'mcp.method.name': 'tools/call', + 'rpc.method': 'tools/call', + 'jsonrpc.protocol.version': '2.0', 'mcp.tool.name': 'process-file', 'mcp.request.id': 'req-stdio-1', + 'jsonrpc.request.id': 'req-stdio-1', 'mcp.session.id': 'stdio-session-456', 'mcp.transport': 'StdioServerTransport', 'network.transport': 'pipe', // Should be pipe, not tcp 'network.protocol.version': '2.0', 'mcp.request.argument.path': '"/tmp/data.txt"', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }, }); }); @@ -419,12 +422,14 @@ describe('MCP Server Transport Instrumentation', () => { expect(config).toEqual({ name: 'tools/call test-tool', - op: 'mcp.server', forceTransaction: true, attributes: expect.objectContaining({ 'mcp.method.name': 'tools/call', + 'rpc.method': 'tools/call', + 'jsonrpc.protocol.version': '2.0', 'mcp.tool.name': 'test-tool', 'mcp.request.id': 'req-direct-test', + 'jsonrpc.request.id': 'req-direct-test', 'mcp.session.id': 'test-session-direct', 'client.address': '127.0.0.1', 'client.port': 8080, @@ -432,9 +437,10 @@ describe('MCP Server Transport Instrumentation', () => { 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.input': '"test"', - 'sentry.op': 'mcp.server', + 'sentry.op': 'rpc', 'sentry.origin': 'auto.function.mcp_server', 'sentry.source': 'route', + 'rpc.system.name': 'mcp', }), }); }); @@ -906,7 +912,9 @@ describe('MCP Server Transport Instrumentation', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'tools/call test-tool', - op: 'mcp.server', + attributes: expect.objectContaining({ + 'sentry.op': 'rpc', + }), }), ); diff --git a/packages/core/test/lib/trpc.test.ts b/packages/core/test/lib/trpc.test.ts index 587d531b5d00..de355c82cba4 100644 --- a/packages/core/test/lib/trpc.test.ts +++ b/packages/core/test/lib/trpc.test.ts @@ -57,10 +57,14 @@ describe('trpcMiddleware', () => { expect(tracing.startSpanManual).toHaveBeenCalledWith( { name: 'trpc/test.procedure', - op: 'rpc.server', attributes: { + 'sentry.op': 'rpc', 'sentry.origin': 'auto.rpc.trpc', 'sentry.source': 'route', + 'rpc.system.name': 'trpc', + 'rpc.method': 'test.procedure', + 'trpc.procedure_path': 'test.procedure', + 'trpc.procedure_type': 'query', }, forceTransaction: false, },