UNPKG

@sentry/core

Version:
351 lines (305 loc) 12.2 kB
import { parseStringToURLObject, isURLObjectRelative } from '../../utils/url.js'; import { MCP_REQUEST_ID_ATTRIBUTE, MCP_PROTOCOL_VERSION_ATTRIBUTE, CLIENT_PORT_ATTRIBUTE, CLIENT_ADDRESS_ATTRIBUTE, MCP_SESSION_ID_ATTRIBUTE, MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE, NETWORK_PROTOCOL_VERSION_ATTRIBUTE, NETWORK_TRANSPORT_ATTRIBUTE, MCP_TRANSPORT_ATTRIBUTE, MCP_TOOL_RESULT_CONTENT_COUNT_ATTRIBUTE, MCP_SERVER_NAME_ATTRIBUTE, MCP_SERVER_TITLE_ATTRIBUTE, MCP_SERVER_VERSION_ATTRIBUTE, MCP_RESOURCE_URI_ATTRIBUTE, MCP_LOGGING_LEVEL_ATTRIBUTE, MCP_LOGGING_LOGGER_ATTRIBUTE, MCP_LOGGING_DATA_TYPE_ATTRIBUTE, MCP_LOGGING_MESSAGE_ATTRIBUTE } from './attributes.js'; import { extractTargetInfo, getRequestArguments } from './methodConfig.js'; import { getProtocolVersionForTransport, getClientInfoForTransport, getSessionDataForTransport } from './sessionManagement.js'; /** * Attribute extraction and building functions for MCP server instrumentation */ /** * Extracts transport types based on transport constructor name * @param transport - MCP transport instance * @returns Transport type mapping for span attributes */ function getTransportTypes(transport) { const transportName = transport.constructor?.name?.toLowerCase() || ''; if (transportName.includes('stdio')) { return { mcpTransport: 'stdio', networkTransport: 'pipe' }; } if (transportName.includes('streamablehttp') || transportName.includes('streamable')) { return { mcpTransport: 'http', networkTransport: 'tcp' }; } if (transportName.includes('sse')) { return { mcpTransport: 'sse', networkTransport: 'tcp' }; } return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } /** * Extracts additional attributes for specific notification types * @param method - Notification method name * @param params - Notification parameters * @returns Method-specific attributes for span instrumentation */ function getNotificationAttributes( method, params, ) { const attributes = {}; switch (method) { case 'notifications/cancelled': if (params?.requestId) { attributes['mcp.cancelled.request_id'] = String(params.requestId); } if (params?.reason) { attributes['mcp.cancelled.reason'] = String(params.reason); } break; case 'notifications/message': if (params?.level) { attributes[MCP_LOGGING_LEVEL_ATTRIBUTE] = String(params.level); } if (params?.logger) { attributes[MCP_LOGGING_LOGGER_ATTRIBUTE] = String(params.logger); } if (params?.data !== undefined) { attributes[MCP_LOGGING_DATA_TYPE_ATTRIBUTE] = typeof params.data; if (typeof params.data === 'string') { attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = params.data; } else { attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = JSON.stringify(params.data); } } break; case 'notifications/progress': if (params?.progressToken) { attributes['mcp.progress.token'] = String(params.progressToken); } if (typeof params?.progress === 'number') { attributes['mcp.progress.current'] = params.progress; } if (typeof params?.total === 'number') { attributes['mcp.progress.total'] = params.total; if (typeof params?.progress === 'number') { attributes['mcp.progress.percentage'] = (params.progress / params.total) * 100; } } if (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); const urlObject = parseStringToURLObject(String(params.uri)); if (urlObject && !isURLObjectRelative(urlObject)) { attributes['mcp.resource.protocol'] = urlObject.protocol.replace(':', ''); } } break; case 'notifications/initialized': attributes['mcp.lifecycle.phase'] = 'initialization_complete'; attributes['mcp.protocol.ready'] = 1; break; } return attributes; } /** * Extracts and validates PartyInfo from an unknown object * @param obj - Unknown object that might contain party info * @returns Validated PartyInfo object with only string properties */ function extractPartyInfo(obj) { const partyInfo = {}; if (obj && typeof obj === 'object' && obj !== null) { const source = obj ; if (typeof source.name === 'string') partyInfo.name = source.name; if (typeof source.title === 'string') partyInfo.title = source.title; if (typeof source.version === 'string') partyInfo.version = source.version; } return partyInfo; } /** * Extracts session data from "initialize" requests * @param request - JSON-RPC "initialize" request containing client info and protocol version * @returns Session data extracted from request parameters including protocol version and client info */ function extractSessionDataFromInitializeRequest(request) { const sessionData = {}; if (request.params && typeof request.params === 'object' && request.params !== null) { const params = request.params ; if (typeof params.protocolVersion === 'string') { sessionData.protocolVersion = params.protocolVersion; } if (params.clientInfo) { sessionData.clientInfo = extractPartyInfo(params.clientInfo); } } return sessionData; } /** * Extracts session data from "initialize" response * @param result - "initialize" response result containing server info and protocol version * @returns Partial session data extracted from response including protocol version and server info */ function extractSessionDataFromInitializeResponse(result) { const sessionData = {}; if (result && typeof result === 'object') { const resultObj = result ; if (typeof resultObj.protocolVersion === 'string') sessionData.protocolVersion = resultObj.protocolVersion; if (resultObj.serverInfo) { sessionData.serverInfo = extractPartyInfo(resultObj.serverInfo); } } return sessionData; } /** * Build client attributes from stored client info * @param transport - MCP transport instance * @returns Client attributes for span instrumentation */ function getClientAttributes(transport) { const clientInfo = getClientInfoForTransport(transport); const attributes = {}; if (clientInfo?.name) { attributes['mcp.client.name'] = clientInfo.name; } if (clientInfo?.title) { attributes['mcp.client.title'] = clientInfo.title; } if (clientInfo?.version) { attributes['mcp.client.version'] = clientInfo.version; } return attributes; } /** * Build server attributes from stored server info * @param transport - MCP transport instance * @returns Server attributes for span instrumentation */ function getServerAttributes(transport) { const serverInfo = getSessionDataForTransport(transport)?.serverInfo; const attributes = {}; if (serverInfo?.name) { attributes[MCP_SERVER_NAME_ATTRIBUTE] = serverInfo.name; } if (serverInfo?.title) { attributes[MCP_SERVER_TITLE_ATTRIBUTE] = serverInfo.title; } if (serverInfo?.version) { attributes[MCP_SERVER_VERSION_ATTRIBUTE] = serverInfo.version; } return attributes; } /** * Extracts client connection info from extra handler data * @param extra - Extra handler data containing connection info * @returns Client address and port information */ function extractClientInfo(extra) { return { address: extra?.requestInfo?.remoteAddress || extra?.clientAddress || extra?.request?.ip || extra?.request?.connection?.remoteAddress, port: extra?.requestInfo?.remotePort || extra?.clientPort || extra?.request?.connection?.remotePort, }; } /** * Build transport and network attributes * @param transport - MCP transport instance * @param extra - Optional extra handler data * @returns Transport attributes for span instrumentation */ function buildTransportAttributes( transport, extra, ) { const sessionId = transport.sessionId; const clientInfo = extra ? extractClientInfo(extra) : {}; const { mcpTransport, networkTransport } = getTransportTypes(transport); const clientAttributes = getClientAttributes(transport); const serverAttributes = getServerAttributes(transport); const 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 }), ...clientAttributes, ...serverAttributes, }; return attributes; } /** * Build type-specific attributes based on message type * @param type - Span type (request or notification) * @param message - JSON-RPC message * @param params - Optional parameters for attribute extraction * @returns Type-specific attributes for span instrumentation */ function buildTypeSpecificAttributes( type, message, params, ) { if (type === 'request') { const request = message ; const targetInfo = extractTargetInfo(request.method, params || {}); return { ...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }), ...targetInfo.attributes, ...getRequestArguments(request.method, params || {}), }; } return getNotificationAttributes(message.method, params || {}); } /** * Build attributes for tool result content items * @param content - Array of content items from tool result * @returns Attributes extracted from each content item including type, text, mime type, URI, and resource info */ function buildAllContentItemAttributes(content) { const attributes = { [MCP_TOOL_RESULT_CONTENT_COUNT_ATTRIBUTE]: content.length, }; for (const [i, item] of content.entries()) { if (typeof item !== 'object' || item === null) continue; const contentItem = item ; const prefix = content.length === 1 ? 'mcp.tool.result' : `mcp.tool.result.${i}`; const safeSet = (key, value) => { if (typeof value === 'string') attributes[`${prefix}.${key}`] = value; }; safeSet('content_type', contentItem.type); safeSet('mime_type', contentItem.mimeType); safeSet('uri', contentItem.uri); safeSet('name', contentItem.name); if (typeof contentItem.text === 'string') { const text = contentItem.text; const maxLength = 500; attributes[`${prefix}.content`] = text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text; } if (typeof contentItem.data === 'string') { attributes[`${prefix}.data_size`] = contentItem.data.length; } const resource = contentItem.resource; if (typeof resource === 'object' && resource !== null) { const res = resource ; safeSet('resource_uri', res.uri); safeSet('resource_mime_type', res.mimeType); } } return attributes; } /** * Extract tool result attributes for span instrumentation * @param result - Tool execution result * @returns Attributes extracted from tool result content */ function extractToolResultAttributes(result) { let attributes = {}; if (typeof result !== 'object' || result === null) return attributes; const resultObj = result ; if (typeof resultObj.isError === 'boolean') { attributes[MCP_TOOL_RESULT_IS_ERROR_ATTRIBUTE] = resultObj.isError; } if (Array.isArray(resultObj.content)) { attributes = { ...attributes, ...buildAllContentItemAttributes(resultObj.content) }; } return attributes; } export { buildTransportAttributes, buildTypeSpecificAttributes, extractClientInfo, extractSessionDataFromInitializeRequest, extractSessionDataFromInitializeResponse, extractToolResultAttributes, getClientAttributes, getNotificationAttributes, getServerAttributes, getTransportTypes }; //# sourceMappingURL=attributeExtraction.js.map