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
59 changes: 35 additions & 24 deletions packages/core/src/integrations/mcp-server/attributeExtraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -39,59 +49,59 @@ 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(':', '');
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;
}

Expand All @@ -117,7 +127,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 || {}) : {}),
};
Expand Down
143 changes: 0 additions & 143 deletions packages/core/src/integrations/mcp-server/attributes.ts
Original file line number Diff line number Diff line change
@@ -1,146 +1,3 @@
/**
* 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';

Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/integrations/mcp-server/correlation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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' });
}
Expand Down
32 changes: 18 additions & 14 deletions packages/core/src/integrations/mcp-server/methodConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -18,27 +20,29 @@ import type { MethodConfig } from './types';
const METHOD_CONFIGS: Record<string, MethodConfig> = {
'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,
Expand Down Expand Up @@ -95,17 +99,17 @@ export function getRequestArguments(method: string, params: Record<string, unkno
const argumentsObj = params[config.argumentsField];
if (isObjectLike(argumentsObj)) {
for (const [key, value] of Object.entries(argumentsObj as Record<string, unknown>)) {
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;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/integrations/mcp-server/piiFiltering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>([CLIENT_ADDRESS, CLIENT_PORT, MCP_RESOURCE_URI]);

/**
* Checks if an attribute key should be considered network PII.
Expand Down
Loading
Loading