From 73b2bd6cc5cfac4ac75493e048bc68188aef57a9 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 5 Oct 2026 15:49:37 +0200 Subject: [PATCH 1/3] ref(core): use shared MCP attribute conventions --- .../mcp-server/attributeExtraction.ts | 60 +++++--- .../src/integrations/mcp-server/attributes.ts | 137 ------------------ .../integrations/mcp-server/correlation.ts | 13 +- .../integrations/mcp-server/methodConfig.ts | 32 ++-- .../integrations/mcp-server/piiFiltering.ts | 4 +- .../mcp-server/resultExtraction.ts | 24 +-- .../mcp-server/sessionExtraction.ts | 64 ++++---- .../core/src/integrations/mcp-server/spans.ts | 15 +- .../src/integrations/mcp-server/transport.ts | 4 +- 9 files changed, 119 insertions(+), 234 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 75449f43ccc9..494d500bdafd 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -2,15 +2,25 @@ * Core attribute extraction and building functions for MCP server instrumentation */ -import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url'; import { - MCP_LOGGING_DATA_TYPE_ATTRIBUTE, - MCP_LOGGING_LEVEL_ATTRIBUTE, - MCP_LOGGING_LOGGER_ATTRIBUTE, - MCP_LOGGING_MESSAGE_ATTRIBUTE, - MCP_REQUEST_ID_ATTRIBUTE, - MCP_RESOURCE_URI_ATTRIBUTE, -} from './attributes'; + MCP_CANCELLED_REASON, + MCP_CANCELLED_REQUEST_ID, + MCP_LIFECYCLE_PHASE, + MCP_LOGGING_DATA_TYPE, + MCP_LOGGING_LEVEL, + MCP_LOGGING_LOGGER, + MCP_LOGGING_MESSAGE, + MCP_PROGRESS_CURRENT, + MCP_PROGRESS_MESSAGE, + MCP_PROGRESS_PERCENTAGE, + MCP_PROGRESS_TOKEN, + MCP_PROGRESS_TOTAL, + MCP_PROTOCOL_READY, + MCP_REQUEST_ID, + MCP_RESOURCE_PROTOCOL, + MCP_RESOURCE_URI, +} from '@sentry/conventions/attributes'; +import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url'; import { extractTargetInfo, getRequestArguments } from './methodConfig'; import type { JsonRpcNotification, JsonRpcRequest, McpSpanType } from './types'; @@ -39,59 +49,60 @@ export function getNotificationAttributes( switch (method) { case 'notifications/cancelled': if (params?.requestId) { - attributes['mcp.cancelled.request_id'] = String(params.requestId); + attributes[MCP_CANCELLED_REQUEST_ID] = String(params.requestId); } if (params?.reason) { - attributes['mcp.cancelled.reason'] = String(params.reason); + attributes[MCP_CANCELLED_REASON] = String(params.reason); } break; case 'notifications/message': if (params?.level) { - attributes[MCP_LOGGING_LEVEL_ATTRIBUTE] = String(params.level); + attributes[MCP_LOGGING_LEVEL] = String(params.level); } if (params?.logger) { - attributes[MCP_LOGGING_LOGGER_ATTRIBUTE] = String(params.logger); + attributes[MCP_LOGGING_LOGGER] = String(params.logger); } if (params?.data !== undefined) { - attributes[MCP_LOGGING_DATA_TYPE_ATTRIBUTE] = typeof params.data; + attributes[MCP_LOGGING_DATA_TYPE] = typeof params.data; if (recordInputs) { - attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = formatLoggingData(params.data); + attributes[MCP_LOGGING_MESSAGE] = formatLoggingData(params.data); } } break; case 'notifications/progress': if (params?.progressToken) { - attributes['mcp.progress.token'] = String(params.progressToken); + attributes[MCP_PROGRESS_TOKEN] = String(params.progressToken); } if (typeof params?.progress === 'number') { - attributes['mcp.progress.current'] = params.progress; + attributes[MCP_PROGRESS_CURRENT] = params.progress; } if (typeof params?.total === 'number') { - attributes['mcp.progress.total'] = params.total; + attributes[MCP_PROGRESS_TOTAL] = params.total; if (typeof params?.progress === 'number') { - attributes['mcp.progress.percentage'] = (params.progress / params.total) * 100; + attributes[MCP_PROGRESS_PERCENTAGE] = (params.progress / params.total) * 100; } } if (params?.message) { - attributes['mcp.progress.message'] = String(params.message); + attributes[MCP_PROGRESS_MESSAGE] = String(params.message); } break; case 'notifications/resources/updated': if (params?.uri) { - attributes[MCP_RESOURCE_URI_ATTRIBUTE] = String(params.uri); + attributes[MCP_RESOURCE_URI] = String(params.uri); const urlObject = parseStringToURLObject(String(params.uri)); if (urlObject && !isURLObjectRelative(urlObject)) { - attributes['mcp.resource.protocol'] = urlObject.protocol.replace(':', ''); + // oxlint-disable-next-line typescript/no-deprecated -- Keep the resource URI scheme distinct from the network protocol. + attributes[MCP_RESOURCE_PROTOCOL] = urlObject.protocol.replace(':', ''); } } break; case 'notifications/initialized': - attributes['mcp.lifecycle.phase'] = 'initialization_complete'; - attributes['mcp.protocol.ready'] = 1; + attributes[MCP_LIFECYCLE_PHASE] = 'initialization_complete'; + attributes[MCP_PROTOCOL_READY] = 1; break; } @@ -117,7 +128,8 @@ export function buildTypeSpecificAttributes( const targetInfo = extractTargetInfo(request.method, params || {}); return { - ...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }), + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy request ID attribute for existing consumers. + ...(request.id !== undefined && { [MCP_REQUEST_ID]: String(request.id) }), ...targetInfo.attributes, ...(recordInputs ? getRequestArguments(request.method, params || {}) : {}), }; diff --git a/packages/core/src/integrations/mcp-server/attributes.ts b/packages/core/src/integrations/mcp-server/attributes.ts index 2baa446418d7..06b818d0e2d1 100644 --- a/packages/core/src/integrations/mcp-server/attributes.ts +++ b/packages/core/src/integrations/mcp-server/attributes.ts @@ -1,146 +1,9 @@ -/** - * Essential MCP attribute constants for Sentry instrumentation - * - * Based on OpenTelemetry MCP semantic conventions - * @see https://github.com/open-telemetry/semantic-conventions/blob/3097fb0af5b9492b0e3f55dc5f6c21a3dc2be8df/docs/gen-ai/mcp.md - */ - -// ============================================================================= -// CORE MCP ATTRIBUTES -// ============================================================================= - -/** The name of the request or notification method */ -export const MCP_METHOD_NAME_ATTRIBUTE = 'mcp.method.name'; - -/** JSON-RPC request identifier for the request. Unique within the MCP session. */ -export const MCP_REQUEST_ID_ATTRIBUTE = 'mcp.request.id'; - -/** Identifies the MCP session */ -export const MCP_SESSION_ID_ATTRIBUTE = 'mcp.session.id'; - -/** Transport method used for MCP communication */ -export const MCP_TRANSPORT_ATTRIBUTE = 'mcp.transport'; - -// ============================================================================= -// CLIENT ATTRIBUTES -// ============================================================================= - -/** Name of the MCP client application */ -export const MCP_CLIENT_NAME_ATTRIBUTE = 'mcp.client.name'; - -/** Display title of the MCP client application */ -export const MCP_CLIENT_TITLE_ATTRIBUTE = 'mcp.client.title'; - -/** Version of the MCP client application */ -export const MCP_CLIENT_VERSION_ATTRIBUTE = 'mcp.client.version'; - -// ============================================================================= -// SERVER ATTRIBUTES -// ============================================================================= - -/** Name of the MCP server application */ -export const MCP_SERVER_NAME_ATTRIBUTE = 'mcp.server.name'; - -/** Display title of the MCP server application */ -export const MCP_SERVER_TITLE_ATTRIBUTE = 'mcp.server.title'; - -/** Version of the MCP server application */ -export const MCP_SERVER_VERSION_ATTRIBUTE = 'mcp.server.version'; - -/** MCP protocol version used in the session */ -export const MCP_PROTOCOL_VERSION_ATTRIBUTE = 'mcp.protocol.version'; - -// ============================================================================= -// METHOD-SPECIFIC ATTRIBUTES -// ============================================================================= - -/** Name of the tool being called */ -export const MCP_TOOL_NAME_ATTRIBUTE = 'mcp.tool.name'; - -/** The resource URI being accessed */ -export const MCP_RESOURCE_URI_ATTRIBUTE = 'mcp.resource.uri'; - -/** Name of the prompt template */ -export const MCP_PROMPT_NAME_ATTRIBUTE = 'mcp.prompt.name'; - -// ============================================================================= -// TOOL RESULT ATTRIBUTES -// ============================================================================= - -/** Whether a tool execution resulted in an error */ -export const MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE = 'mcp.tool.result.is_error'; - -/** Number of content items in the tool result */ -export const MCP_TOOL_RESULT_CONTENT_COUNT_ATTRIBUTE = 'mcp.tool.result.content_count'; - -/** Serialized content of the tool result */ -export const MCP_TOOL_RESULT_CONTENT_ATTRIBUTE = 'mcp.tool.result.content'; - /** Prefix for tool result attributes that contain sensitive content */ export const MCP_TOOL_RESULT_PREFIX = 'mcp.tool.result'; -// ============================================================================= -// PROMPT RESULT ATTRIBUTES -// ============================================================================= - -/** Description of the prompt result */ -export const MCP_PROMPT_RESULT_DESCRIPTION_ATTRIBUTE = 'mcp.prompt.result.description'; - -/** Number of messages in the prompt result */ -export const MCP_PROMPT_RESULT_MESSAGE_COUNT_ATTRIBUTE = 'mcp.prompt.result.message_count'; - -/** Role of the message in the prompt result (for single message results) */ -export const MCP_PROMPT_RESULT_MESSAGE_ROLE_ATTRIBUTE = 'mcp.prompt.result.message_role'; - -/** Content of the message in the prompt result (for single message results) */ -export const MCP_PROMPT_RESULT_MESSAGE_CONTENT_ATTRIBUTE = 'mcp.prompt.result.message_content'; - /** Prefix for prompt result attributes that contain sensitive content */ export const MCP_PROMPT_RESULT_PREFIX = 'mcp.prompt.result'; -// ============================================================================= -// REQUEST ARGUMENT ATTRIBUTES -// ============================================================================= - -/** Prefix for MCP request argument prefix for each argument */ -export const MCP_REQUEST_ARGUMENT = 'mcp.request.argument'; - -// ============================================================================= -// LOGGING ATTRIBUTES -// ============================================================================= - -/** Log level for MCP logging operations */ -export const MCP_LOGGING_LEVEL_ATTRIBUTE = 'mcp.logging.level'; - -/** Logger name for MCP logging operations */ -export const MCP_LOGGING_LOGGER_ATTRIBUTE = 'mcp.logging.logger'; - -/** Data type of the logged message */ -export const MCP_LOGGING_DATA_TYPE_ATTRIBUTE = 'mcp.logging.data_type'; - -/** Log message content */ -export const MCP_LOGGING_MESSAGE_ATTRIBUTE = 'mcp.logging.message'; - -// ============================================================================= -// NETWORK ATTRIBUTES (OpenTelemetry Standard) -// ============================================================================= - -/** OSI transport layer protocol */ -export const NETWORK_TRANSPORT_ATTRIBUTE = 'network.transport'; - -/** The version of JSON RPC protocol used */ -export const NETWORK_PROTOCOL_VERSION_ATTRIBUTE = 'network.protocol.version'; - -/** Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name */ -export const CLIENT_ADDRESS_ATTRIBUTE = 'client.address'; - -/** Client port number */ -export const CLIENT_PORT_ATTRIBUTE = 'client.port'; - -// ============================================================================= -// SENTRY-SPECIFIC MCP ATTRIBUTE VALUES -// ============================================================================= - /** Sentry origin value for MCP function spans */ export const MCP_FUNCTION_ORIGIN_VALUE = 'auto.function.mcp_server'; diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts index 3b0e8e9a5d0e..4ca42839787d 100644 --- a/packages/core/src/integrations/mcp-server/correlation.ts +++ b/packages/core/src/integrations/mcp-server/correlation.ts @@ -10,10 +10,14 @@ * Falls back to WeakMap by transport instance for stateless transports (no sessionId). */ -import { ERROR_TYPE, RPC_RESPONSE_STATUS_CODE } from '@sentry/conventions/attributes'; +import { + ERROR_TYPE, + MCP_PROTOCOL_VERSION, + MCP_TOOL_RESULT_IS_ERROR, + RPC_RESPONSE_STATUS_CODE, +} from '@sentry/conventions/attributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types/span'; -import { MCP_PROTOCOL_VERSION_ATTRIBUTE, MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE } from './attributes'; import { isJsonRpcServerError } from './errorCapture'; import { extractPromptResultAttributes, extractToolResultAttributes } from './resultExtraction'; import { @@ -118,7 +122,7 @@ export function completeSpanWithResults( ...buildServerAttributesFromInfo(responseSessionData.serverInfo), }; if (responseSessionData.protocolVersion) { - responseAttributes[MCP_PROTOCOL_VERSION_ATTRIBUTE] = responseSessionData.protocolVersion; + responseAttributes[MCP_PROTOCOL_VERSION] = responseSessionData.protocolVersion; } if (Object.keys(responseAttributes).length > 0) { span.setAttributes(responseAttributes); @@ -133,7 +137,8 @@ export function completeSpanWithResults( } else if (method === 'tools/call') { const toolAttributes = extractToolResultAttributes(result, spanData.capturePolicy.recordOutputs); span.setAttributes(toolAttributes); - if (toolAttributes[MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE] === true) { + // oxlint-disable-next-line typescript/no-deprecated -- Preserve classification based on the existing tool result attribute. + if (toolAttributes[MCP_TOOL_RESULT_IS_ERROR] === true) { span.setAttributes({ [ERROR_TYPE]: 'tool_error' }); span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } diff --git a/packages/core/src/integrations/mcp-server/methodConfig.ts b/packages/core/src/integrations/mcp-server/methodConfig.ts index 0153adf49bbc..1e885187b83e 100644 --- a/packages/core/src/integrations/mcp-server/methodConfig.ts +++ b/packages/core/src/integrations/mcp-server/methodConfig.ts @@ -2,13 +2,15 @@ * Method configuration and request processing for MCP server instrumentation */ -import { isObjectLike } from '../../utils/is'; import { - MCP_PROMPT_NAME_ATTRIBUTE, - MCP_REQUEST_ARGUMENT, - MCP_RESOURCE_URI_ATTRIBUTE, - MCP_TOOL_NAME_ATTRIBUTE, -} from './attributes'; + MCP_PROMPT_NAME, + MCP_REQUEST_ARGUMENT_KEY_BASE, + MCP_REQUEST_ARGUMENT_NAME, + MCP_REQUEST_ARGUMENT_URI, + MCP_RESOURCE_URI, + MCP_TOOL_NAME, +} from '@sentry/conventions/attributes'; +import { isObjectLike } from '../../utils/is'; import type { MethodConfig } from './types'; /** @@ -18,27 +20,29 @@ import type { MethodConfig } from './types'; const METHOD_CONFIGS: Record = { 'tools/call': { targetField: 'name', - targetAttribute: MCP_TOOL_NAME_ATTRIBUTE, + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy tool name attribute for existing consumers. + targetAttribute: MCP_TOOL_NAME, targetIsLowCardinality: true, captureArguments: true, argumentsField: 'arguments', }, 'resources/read': { targetField: 'uri', - targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE, + targetAttribute: MCP_RESOURCE_URI, captureUri: true, }, 'resources/subscribe': { targetField: 'uri', - targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE, + targetAttribute: MCP_RESOURCE_URI, }, 'resources/unsubscribe': { targetField: 'uri', - targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE, + targetAttribute: MCP_RESOURCE_URI, }, 'prompts/get': { targetField: 'name', - targetAttribute: MCP_PROMPT_NAME_ATTRIBUTE, + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy prompt name attribute for existing consumers. + targetAttribute: MCP_PROMPT_NAME, targetIsLowCardinality: true, captureName: true, captureArguments: true, @@ -95,17 +99,17 @@ export function getRequestArguments(method: string, params: Record)) { - args[`${MCP_REQUEST_ARGUMENT}.${key.toLowerCase()}`] = JSON.stringify(value); + args[`${MCP_REQUEST_ARGUMENT_KEY_BASE}.${key.toLowerCase()}`] = JSON.stringify(value); } } } if (config.captureUri && params?.uri) { - args[`${MCP_REQUEST_ARGUMENT}.uri`] = JSON.stringify(params.uri); + args[MCP_REQUEST_ARGUMENT_URI] = JSON.stringify(params.uri); } if (config.captureName && params?.name) { - args[`${MCP_REQUEST_ARGUMENT}.name`] = JSON.stringify(params.name); + args[MCP_REQUEST_ARGUMENT_NAME] = JSON.stringify(params.name); } return args; diff --git a/packages/core/src/integrations/mcp-server/piiFiltering.ts b/packages/core/src/integrations/mcp-server/piiFiltering.ts index 8dc139f2bbf4..fe9b91744cfb 100644 --- a/packages/core/src/integrations/mcp-server/piiFiltering.ts +++ b/packages/core/src/integrations/mcp-server/piiFiltering.ts @@ -5,14 +5,14 @@ * Input/output data (request arguments, tool/prompt results) is controlled * separately via recordInputs/recordOutputs options. */ +import { CLIENT_ADDRESS, CLIENT_PORT, MCP_RESOURCE_URI } from '@sentry/conventions/attributes'; import type { SpanAttributeValue } from '../../types/span'; -import { CLIENT_ADDRESS_ATTRIBUTE, CLIENT_PORT_ATTRIBUTE, MCP_RESOURCE_URI_ATTRIBUTE } from './attributes'; /** * Network PII attributes that should be removed when dataCollection.userInfo is false * @internal */ -const NETWORK_PII_ATTRIBUTES = new Set([CLIENT_ADDRESS_ATTRIBUTE, CLIENT_PORT_ATTRIBUTE, MCP_RESOURCE_URI_ATTRIBUTE]); +const NETWORK_PII_ATTRIBUTES = new Set([CLIENT_ADDRESS, CLIENT_PORT, MCP_RESOURCE_URI]); /** * Checks if an attribute key should be considered network PII. diff --git a/packages/core/src/integrations/mcp-server/resultExtraction.ts b/packages/core/src/integrations/mcp-server/resultExtraction.ts index 58f9ad860083..df45b92ac8d0 100644 --- a/packages/core/src/integrations/mcp-server/resultExtraction.ts +++ b/packages/core/src/integrations/mcp-server/resultExtraction.ts @@ -5,11 +5,12 @@ */ import { - MCP_PROMPT_RESULT_DESCRIPTION_ATTRIBUTE, - MCP_PROMPT_RESULT_MESSAGE_COUNT_ATTRIBUTE, - MCP_TOOL_RESULT_CONTENT_COUNT_ATTRIBUTE, - MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE, -} from './attributes'; + MCP_PROMPT_RESULT_DESCRIPTION, + MCP_PROMPT_RESULT_MESSAGE_COUNT, + MCP_TOOL_RESULT_CONTENT_COUNT, + MCP_TOOL_RESULT_IS_ERROR, +} from '@sentry/conventions/attributes'; +import { MCP_TOOL_RESULT_PREFIX, MCP_PROMPT_RESULT_PREFIX } from './attributes'; import { isValidContentItem } from './validation'; /** @@ -23,7 +24,7 @@ function buildAllContentItemAttributes( includeContent: boolean, ): Record { const attributes: Record = { - [MCP_TOOL_RESULT_CONTENT_COUNT_ATTRIBUTE]: content.length, + [MCP_TOOL_RESULT_CONTENT_COUNT]: content.length, }; for (const [i, item] of content.entries()) { @@ -31,7 +32,7 @@ function buildAllContentItemAttributes( continue; } - const prefix = content.length === 1 ? 'mcp.tool.result' : `mcp.tool.result.${i}`; + const prefix = content.length === 1 ? MCP_TOOL_RESULT_PREFIX : `${MCP_TOOL_RESULT_PREFIX}.${i}`; if (typeof item.type === 'string') { attributes[`${prefix}.content_type`] = item.type; @@ -84,7 +85,8 @@ export function extractToolResultAttributes( const attributes = Array.isArray(result.content) ? buildAllContentItemAttributes(result.content, recordOutputs) : {}; if (typeof result.isError === 'boolean') { - attributes[MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE] = result.isError; + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy tool result attribute for existing consumers. + attributes[MCP_TOOL_RESULT_IS_ERROR] = result.isError; } return attributes; @@ -106,11 +108,11 @@ export function extractPromptResultAttributes( } if (recordOutputs && typeof result.description === 'string') { - attributes[MCP_PROMPT_RESULT_DESCRIPTION_ATTRIBUTE] = result.description; + attributes[MCP_PROMPT_RESULT_DESCRIPTION] = result.description; } if (Array.isArray(result.messages)) { - attributes[MCP_PROMPT_RESULT_MESSAGE_COUNT_ATTRIBUTE] = result.messages.length; + attributes[MCP_PROMPT_RESULT_MESSAGE_COUNT] = result.messages.length; if (recordOutputs) { const messages = result.messages; @@ -119,7 +121,7 @@ export function extractPromptResultAttributes( continue; } - const prefix = messages.length === 1 ? 'mcp.prompt.result' : `mcp.prompt.result.${i}`; + const prefix = messages.length === 1 ? MCP_PROMPT_RESULT_PREFIX : `${MCP_PROMPT_RESULT_PREFIX}.${i}`; const safeSet = (key: string, value: unknown): void => { if (typeof value === 'string') { diff --git a/packages/core/src/integrations/mcp-server/sessionExtraction.ts b/packages/core/src/integrations/mcp-server/sessionExtraction.ts index a4de982bf1b4..daaf4c068253 100644 --- a/packages/core/src/integrations/mcp-server/sessionExtraction.ts +++ b/packages/core/src/integrations/mcp-server/sessionExtraction.ts @@ -5,17 +5,20 @@ */ import { - CLIENT_ADDRESS_ATTRIBUTE, - CLIENT_PORT_ATTRIBUTE, - MCP_PROTOCOL_VERSION_ATTRIBUTE, - MCP_SERVER_NAME_ATTRIBUTE, - MCP_SERVER_TITLE_ATTRIBUTE, - MCP_SERVER_VERSION_ATTRIBUTE, - MCP_SESSION_ID_ATTRIBUTE, - MCP_TRANSPORT_ATTRIBUTE, - NETWORK_PROTOCOL_VERSION_ATTRIBUTE, - NETWORK_TRANSPORT_ATTRIBUTE, -} from './attributes'; + CLIENT_ADDRESS, + CLIENT_PORT, + MCP_CLIENT_NAME, + MCP_CLIENT_TITLE, + MCP_CLIENT_VERSION, + MCP_PROTOCOL_VERSION, + MCP_SERVER_NAME, + MCP_SERVER_TITLE, + MCP_SERVER_VERSION, + MCP_SESSION_ID, + MCP_TRANSPORT, + NETWORK_PROTOCOL_VERSION, + NETWORK_TRANSPORT, +} from '@sentry/conventions/attributes'; import { getClientInfoForTransport, getProtocolVersionForTransport, @@ -143,13 +146,13 @@ export function getClientAttributes(transport: MCPTransport): Record = {}; if (clientInfo?.name) { - attributes['mcp.client.name'] = clientInfo.name; + attributes[MCP_CLIENT_NAME] = clientInfo.name; } if (clientInfo?.title) { - attributes['mcp.client.title'] = clientInfo.title; + attributes[MCP_CLIENT_TITLE] = clientInfo.title; } if (clientInfo?.version) { - attributes['mcp.client.version'] = clientInfo.version; + attributes[MCP_CLIENT_VERSION] = clientInfo.version; } return attributes; @@ -164,13 +167,13 @@ export function buildClientAttributesFromInfo(clientInfo?: PartyInfo): Record = {}; if (clientInfo?.name) { - attributes['mcp.client.name'] = clientInfo.name; + attributes[MCP_CLIENT_NAME] = clientInfo.name; } if (clientInfo?.title) { - attributes['mcp.client.title'] = clientInfo.title; + attributes[MCP_CLIENT_TITLE] = clientInfo.title; } if (clientInfo?.version) { - attributes['mcp.client.version'] = clientInfo.version; + attributes[MCP_CLIENT_VERSION] = clientInfo.version; } return attributes; @@ -186,13 +189,13 @@ export function getServerAttributes(transport: MCPTransport): Record = {}; if (serverInfo?.name) { - attributes[MCP_SERVER_NAME_ATTRIBUTE] = serverInfo.name; + attributes[MCP_SERVER_NAME] = serverInfo.name; } if (serverInfo?.title) { - attributes[MCP_SERVER_TITLE_ATTRIBUTE] = serverInfo.title; + attributes[MCP_SERVER_TITLE] = serverInfo.title; } if (serverInfo?.version) { - attributes[MCP_SERVER_VERSION_ATTRIBUTE] = serverInfo.version; + attributes[MCP_SERVER_VERSION] = serverInfo.version; } return attributes; @@ -207,13 +210,13 @@ export function buildServerAttributesFromInfo(serverInfo?: PartyInfo): Record = {}; if (serverInfo?.name) { - attributes[MCP_SERVER_NAME_ATTRIBUTE] = serverInfo.name; + attributes[MCP_SERVER_NAME] = serverInfo.name; } if (serverInfo?.title) { - attributes[MCP_SERVER_TITLE_ATTRIBUTE] = serverInfo.title; + attributes[MCP_SERVER_TITLE] = serverInfo.title; } if (serverInfo?.version) { - attributes[MCP_SERVER_VERSION_ATTRIBUTE] = serverInfo.version; + attributes[MCP_SERVER_VERSION] = serverInfo.version; } return attributes; @@ -288,13 +291,14 @@ export function buildTransportAttributes( const protocolVersion = hasRequestMetadata ? messageData?.protocolVersion : getProtocolVersionForTransport(transport); const attributes = { - ...(sessionId && { [MCP_SESSION_ID_ATTRIBUTE]: sessionId }), - ...(clientInfo.address && { [CLIENT_ADDRESS_ATTRIBUTE]: clientInfo.address }), - ...(clientInfo.port && { [CLIENT_PORT_ATTRIBUTE]: clientInfo.port }), - [MCP_TRANSPORT_ATTRIBUTE]: mcpTransport, - [NETWORK_TRANSPORT_ATTRIBUTE]: networkTransport, - [NETWORK_PROTOCOL_VERSION_ATTRIBUTE]: '2.0', - ...(protocolVersion && { [MCP_PROTOCOL_VERSION_ATTRIBUTE]: protocolVersion }), + ...(sessionId && { [MCP_SESSION_ID]: sessionId }), + ...(clientInfo.address && { [CLIENT_ADDRESS]: clientInfo.address }), + ...(clientInfo.port && { [CLIENT_PORT]: clientInfo.port }), + // oxlint-disable-next-line typescript/no-deprecated -- Keep the transport implementation name distinct from the network transport. + [MCP_TRANSPORT]: mcpTransport, + [NETWORK_TRANSPORT]: networkTransport, + [NETWORK_PROTOCOL_VERSION]: '2.0', + ...(protocolVersion && { [MCP_PROTOCOL_VERSION]: protocolVersion }), ...clientAttributes, ...serverAttributes, }; diff --git a/packages/core/src/integrations/mcp-server/spans.ts b/packages/core/src/integrations/mcp-server/spans.ts index b380da55e5a8..9f370a5d1b98 100644 --- a/packages/core/src/integrations/mcp-server/spans.ts +++ b/packages/core/src/integrations/mcp-server/spans.ts @@ -5,23 +5,18 @@ * Handles both request and notification spans with attribute extraction. */ -import { getClient } from '../../currentScopes'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { MCP_METHOD_NAME, SENTRY_OP, SENTRY_ORIGIN, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; import { MCP_NOTIFICATION_CLIENT_TO_SERVER, MCP_NOTIFICATION_SERVER_TO_CLIENT, MCP_SERVER, } from '@sentry/conventions/op'; +import { getClient } from '../../currentScopes'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK, MCP_SERVER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; import { startSpan } from '../../tracing/trace'; import { buildTransportAttributes, buildTypeSpecificAttributes } from './attributeExtraction'; -import { - MCP_FUNCTION_ORIGIN_VALUE, - MCP_METHOD_NAME_ATTRIBUTE, - MCP_NOTIFICATION_ORIGIN_VALUE, - MCP_ROUTE_SOURCE_VALUE, -} from './attributes'; +import { MCP_FUNCTION_ORIGIN_VALUE, MCP_NOTIFICATION_ORIGIN_VALUE, MCP_ROUTE_SOURCE_VALUE } from './attributes'; import { extractTargetInfo } from './methodConfig'; import { filterMcpPiiFromSpanData } from './piiFiltering'; import type { @@ -103,7 +98,7 @@ function createMcpSpan(config: McpSpanConfig): unknown { const rawAttributes: Record = { ...buildTransportAttributes(transport, extra, message), - [MCP_METHOD_NAME_ATTRIBUTE]: method, + [MCP_METHOD_NAME]: method, ...buildTypeSpecificAttributes(type, message, params, options?.recordInputs), ...buildSentryAttributes(type), }; @@ -201,7 +196,7 @@ export function buildMcpServerSpanConfig( const rawAttributes: Record = { ...buildTransportAttributes(transport, extra, jsonRpcMessage), - [MCP_METHOD_NAME_ATTRIBUTE]: method, + [MCP_METHOD_NAME]: method, ...buildTypeSpecificAttributes('request', jsonRpcMessage, params, options?.recordInputs), ...buildSentryAttributes('request'), }; diff --git a/packages/core/src/integrations/mcp-server/transport.ts b/packages/core/src/integrations/mcp-server/transport.ts index 599457b01cfd..92788b275f49 100644 --- a/packages/core/src/integrations/mcp-server/transport.ts +++ b/packages/core/src/integrations/mcp-server/transport.ts @@ -5,12 +5,12 @@ * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/transports */ +import { MCP_PROTOCOL_VERSION } from '@sentry/conventions/attributes'; import { getClient, getIsolationScope, withIsolationScope } from '../../currentScopes'; import { withActiveSpan } from '../../tracing'; import { startInactiveSpan } from '../../tracing/trace'; import { isObjectLike } from '../../utils/is'; import { fill } from '../../utils/object'; -import { MCP_PROTOCOL_VERSION_ATTRIBUTE } from './attributes'; import { cleanupPendingSpansForTransport, completeSpanWithResults, storeSpanForRequest } from './correlation'; import { captureError, isJsonRpcServerError } from './errorCapture'; import { buildClientAttributesFromInfo, extractSessionDataFromInitializeRequest } from './sessionExtraction'; @@ -73,7 +73,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: McpServ span.setAttributes({ ...buildClientAttributesFromInfo(messageSessionData.clientInfo), ...(messageSessionData.protocolVersion && { - [MCP_PROTOCOL_VERSION_ATTRIBUTE]: messageSessionData.protocolVersion, + [MCP_PROTOCOL_VERSION]: messageSessionData.protocolVersion, }), }); } From a19fe12883b2afd33c8c359fd2f0600a513ae43d Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 8 Oct 2026 13:24:22 +0200 Subject: [PATCH 2/3] ref(core): reuse registered MCP result constants --- .../mcp-server/resultExtraction.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/resultExtraction.ts b/packages/core/src/integrations/mcp-server/resultExtraction.ts index df45b92ac8d0..d554f4d3a6b2 100644 --- a/packages/core/src/integrations/mcp-server/resultExtraction.ts +++ b/packages/core/src/integrations/mcp-server/resultExtraction.ts @@ -6,7 +6,10 @@ import { MCP_PROMPT_RESULT_DESCRIPTION, + MCP_PROMPT_RESULT_MESSAGE_CONTENT, MCP_PROMPT_RESULT_MESSAGE_COUNT, + MCP_PROMPT_RESULT_MESSAGE_ROLE, + MCP_TOOL_RESULT_CONTENT, MCP_TOOL_RESULT_CONTENT_COUNT, MCP_TOOL_RESULT_IS_ERROR, } from '@sentry/conventions/attributes'; @@ -50,7 +53,9 @@ function buildAllContentItemAttributes( safeSet('name', item.name); if (typeof item.text === 'string') { - attributes[`${prefix}.content`] = item.text; + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy content attribute for existing consumers. + const attrName = content.length === 1 ? MCP_TOOL_RESULT_CONTENT : `${prefix}.content`; + attributes[attrName] = item.text; } if (typeof item.data === 'string') { @@ -123,19 +128,15 @@ export function extractPromptResultAttributes( const prefix = messages.length === 1 ? MCP_PROMPT_RESULT_PREFIX : `${MCP_PROMPT_RESULT_PREFIX}.${i}`; - const safeSet = (key: string, value: unknown): void => { - if (typeof value === 'string') { - const attrName = messages.length === 1 ? `${prefix}.message_${key}` : `${prefix}.${key}`; - attributes[attrName] = value; - } - }; - - safeSet('role', message.role); + if (typeof message.role === 'string') { + const attrName = messages.length === 1 ? MCP_PROMPT_RESULT_MESSAGE_ROLE : `${prefix}.role`; + attributes[attrName] = message.role; + } if (isValidContentItem(message.content)) { const content = message.content; if (typeof content.text === 'string') { - const attrName = messages.length === 1 ? `${prefix}.message_content` : `${prefix}.content`; + const attrName = messages.length === 1 ? MCP_PROMPT_RESULT_MESSAGE_CONTENT : `${prefix}.content`; attributes[attrName] = content.text; } } From 1afb158802906eb9c22720ee9ebf1bd264b533ca Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 13 Oct 2026 10:44:37 +0200 Subject: [PATCH 3/3] ref(core): adopt published MCP result conventions --- .../mcp-server/attributeExtraction.ts | 1 - .../src/integrations/mcp-server/attributes.ts | 6 -- .../mcp-server/resultExtraction.ts | 65 ++++++++++++++----- .../mcp-server/sessionExtraction.ts | 1 - 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 494d500bdafd..fff8d76dc46f 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -94,7 +94,6 @@ export function getNotificationAttributes( attributes[MCP_RESOURCE_URI] = String(params.uri); const urlObject = parseStringToURLObject(String(params.uri)); if (urlObject && !isURLObjectRelative(urlObject)) { - // oxlint-disable-next-line typescript/no-deprecated -- Keep the resource URI scheme distinct from the network protocol. attributes[MCP_RESOURCE_PROTOCOL] = urlObject.protocol.replace(':', ''); } } diff --git a/packages/core/src/integrations/mcp-server/attributes.ts b/packages/core/src/integrations/mcp-server/attributes.ts index 06b818d0e2d1..b707336b2b95 100644 --- a/packages/core/src/integrations/mcp-server/attributes.ts +++ b/packages/core/src/integrations/mcp-server/attributes.ts @@ -1,9 +1,3 @@ -/** Prefix for tool result attributes that contain sensitive content */ -export const MCP_TOOL_RESULT_PREFIX = 'mcp.tool.result'; - -/** Prefix for prompt result attributes that contain sensitive content */ -export const MCP_PROMPT_RESULT_PREFIX = 'mcp.prompt.result'; - /** Sentry origin value for MCP function spans */ export const MCP_FUNCTION_ORIGIN_VALUE = 'auto.function.mcp_server'; diff --git a/packages/core/src/integrations/mcp-server/resultExtraction.ts b/packages/core/src/integrations/mcp-server/resultExtraction.ts index d554f4d3a6b2..42fd9d8740c1 100644 --- a/packages/core/src/integrations/mcp-server/resultExtraction.ts +++ b/packages/core/src/integrations/mcp-server/resultExtraction.ts @@ -6,16 +6,33 @@ import { MCP_PROMPT_RESULT_DESCRIPTION, + MCP_PROMPT_RESULT_KEY_CONTENT, + MCP_PROMPT_RESULT_KEY_ROLE, MCP_PROMPT_RESULT_MESSAGE_CONTENT, MCP_PROMPT_RESULT_MESSAGE_COUNT, MCP_PROMPT_RESULT_MESSAGE_ROLE, MCP_TOOL_RESULT_CONTENT, MCP_TOOL_RESULT_CONTENT_COUNT, + MCP_TOOL_RESULT_CONTENT_TYPE, + MCP_TOOL_RESULT_DATA_SIZE, MCP_TOOL_RESULT_IS_ERROR, + MCP_TOOL_RESULT_KEY_CONTENT, + MCP_TOOL_RESULT_KEY_CONTENT_TYPE, + MCP_TOOL_RESULT_KEY_DATA_SIZE, + MCP_TOOL_RESULT_KEY_MIME_TYPE, + MCP_TOOL_RESULT_KEY_NAME, + MCP_TOOL_RESULT_KEY_RESOURCE_MIME_TYPE, + MCP_TOOL_RESULT_KEY_RESOURCE_URI, + MCP_TOOL_RESULT_KEY_URI, + MCP_TOOL_RESULT_MIME_TYPE, + MCP_TOOL_RESULT_NAME, + MCP_TOOL_RESULT_RESOURCE_MIME_TYPE, + MCP_TOOL_RESULT_RESOURCE_URI, + MCP_TOOL_RESULT_URI, } from '@sentry/conventions/attributes'; -import { MCP_TOOL_RESULT_PREFIX, MCP_PROMPT_RESULT_PREFIX } from './attributes'; import { isValidContentItem } from './validation'; +/* oxlint-disable typescript/no-deprecated -- Preserve the legacy result attributes for existing consumers. */ /** * Build attributes for tool result content items * @param content - Array of content items from tool result @@ -35,43 +52,55 @@ function buildAllContentItemAttributes( continue; } - const prefix = content.length === 1 ? MCP_TOOL_RESULT_PREFIX : `${MCP_TOOL_RESULT_PREFIX}.${i}`; + const single = content.length === 1; + const index = String(i); if (typeof item.type === 'string') { - attributes[`${prefix}.content_type`] = item.type; + const key = single ? MCP_TOOL_RESULT_CONTENT_TYPE : MCP_TOOL_RESULT_KEY_CONTENT_TYPE.replace('', index); + attributes[key] = item.type; } if (includeContent) { const safeSet = (key: string, value: unknown): void => { if (typeof value === 'string') { - attributes[`${prefix}.${key}`] = value; + attributes[key] = value; } }; - safeSet('mime_type', item.mimeType); - safeSet('uri', item.uri); - safeSet('name', item.name); + safeSet( + single ? MCP_TOOL_RESULT_MIME_TYPE : MCP_TOOL_RESULT_KEY_MIME_TYPE.replace('', index), + item.mimeType, + ); + safeSet(single ? MCP_TOOL_RESULT_URI : MCP_TOOL_RESULT_KEY_URI.replace('', index), item.uri); + safeSet(single ? MCP_TOOL_RESULT_NAME : MCP_TOOL_RESULT_KEY_NAME.replace('', index), item.name); if (typeof item.text === 'string') { - // oxlint-disable-next-line typescript/no-deprecated -- Preserve the legacy content attribute for existing consumers. - const attrName = content.length === 1 ? MCP_TOOL_RESULT_CONTENT : `${prefix}.content`; + const attrName = single ? MCP_TOOL_RESULT_CONTENT : MCP_TOOL_RESULT_KEY_CONTENT.replace('', index); attributes[attrName] = item.text; } if (typeof item.data === 'string') { - attributes[`${prefix}.data_size`] = item.data.length; + const key = single ? MCP_TOOL_RESULT_DATA_SIZE : MCP_TOOL_RESULT_KEY_DATA_SIZE.replace('', index); + attributes[key] = item.data.length; } const resource = item.resource; if (isValidContentItem(resource)) { - safeSet('resource_uri', resource.uri); - safeSet('resource_mime_type', resource.mimeType); + safeSet( + single ? MCP_TOOL_RESULT_RESOURCE_URI : MCP_TOOL_RESULT_KEY_RESOURCE_URI.replace('', index), + resource.uri, + ); + safeSet( + single ? MCP_TOOL_RESULT_RESOURCE_MIME_TYPE : MCP_TOOL_RESULT_KEY_RESOURCE_MIME_TYPE.replace('', index), + resource.mimeType, + ); } } } return attributes; } +/* oxlint-enable typescript/no-deprecated */ /** * Extract tool result attributes for span instrumentation @@ -126,17 +155,21 @@ export function extractPromptResultAttributes( continue; } - const prefix = messages.length === 1 ? MCP_PROMPT_RESULT_PREFIX : `${MCP_PROMPT_RESULT_PREFIX}.${i}`; - if (typeof message.role === 'string') { - const attrName = messages.length === 1 ? MCP_PROMPT_RESULT_MESSAGE_ROLE : `${prefix}.role`; + const attrName = + messages.length === 1 + ? MCP_PROMPT_RESULT_MESSAGE_ROLE + : MCP_PROMPT_RESULT_KEY_ROLE.replace('', String(i)); attributes[attrName] = message.role; } if (isValidContentItem(message.content)) { const content = message.content; if (typeof content.text === 'string') { - const attrName = messages.length === 1 ? MCP_PROMPT_RESULT_MESSAGE_CONTENT : `${prefix}.content`; + const attrName = + messages.length === 1 + ? MCP_PROMPT_RESULT_MESSAGE_CONTENT + : MCP_PROMPT_RESULT_KEY_CONTENT.replace('', String(i)); attributes[attrName] = content.text; } } diff --git a/packages/core/src/integrations/mcp-server/sessionExtraction.ts b/packages/core/src/integrations/mcp-server/sessionExtraction.ts index daaf4c068253..492674ec16c3 100644 --- a/packages/core/src/integrations/mcp-server/sessionExtraction.ts +++ b/packages/core/src/integrations/mcp-server/sessionExtraction.ts @@ -294,7 +294,6 @@ export function buildTransportAttributes( ...(sessionId && { [MCP_SESSION_ID]: sessionId }), ...(clientInfo.address && { [CLIENT_ADDRESS]: clientInfo.address }), ...(clientInfo.port && { [CLIENT_PORT]: clientInfo.port }), - // oxlint-disable-next-line typescript/no-deprecated -- Keep the transport implementation name distinct from the network transport. [MCP_TRANSPORT]: mcpTransport, [NETWORK_TRANSPORT]: networkTransport, [NETWORK_PROTOCOL_VERSION]: '2.0',