Skip to content

Commit 4973a60

Browse files
committed
Implement MCP server attribute extraction and correlation system. New files for attribute extraction, correlation, and handler wrapping, removing deprecated utilities and configuration files. Improve transport instrumentation for better telemetry and span handling.
1 parent aa0a9fd commit 4973a60

File tree

14 files changed

+1037
-982
lines changed

14 files changed

+1037
-982
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/**
2+
* Attribute extraction and building functions for MCP server instrumentation
3+
*/
4+
5+
import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url';
6+
import {
7+
CLIENT_ADDRESS_ATTRIBUTE,
8+
CLIENT_PORT_ATTRIBUTE,
9+
MCP_PROMPT_NAME_ATTRIBUTE,
10+
MCP_REQUEST_ID_ATTRIBUTE,
11+
MCP_RESOURCE_URI_ATTRIBUTE,
12+
MCP_SESSION_ID_ATTRIBUTE,
13+
MCP_TOOL_NAME_ATTRIBUTE,
14+
MCP_TRANSPORT_ATTRIBUTE,
15+
NETWORK_PROTOCOL_VERSION_ATTRIBUTE,
16+
NETWORK_TRANSPORT_ATTRIBUTE,
17+
} from './attributes';
18+
import type { ExtraHandlerData, JsonRpcNotification, JsonRpcRequest, McpSpanType,MCPTransport, MethodConfig } from './types';
19+
20+
/** Configuration for MCP methods to extract targets and arguments */
21+
const METHOD_CONFIGS: Record<string, MethodConfig> = {
22+
'tools/call': {
23+
targetField: 'name',
24+
targetAttribute: MCP_TOOL_NAME_ATTRIBUTE,
25+
captureArguments: true,
26+
argumentsField: 'arguments',
27+
},
28+
'resources/read': {
29+
targetField: 'uri',
30+
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
31+
captureUri: true,
32+
},
33+
'resources/subscribe': {
34+
targetField: 'uri',
35+
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
36+
},
37+
'resources/unsubscribe': {
38+
targetField: 'uri',
39+
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
40+
},
41+
'prompts/get': {
42+
targetField: 'name',
43+
targetAttribute: MCP_PROMPT_NAME_ATTRIBUTE,
44+
captureName: true,
45+
captureArguments: true,
46+
argumentsField: 'arguments',
47+
},
48+
};
49+
50+
/** Extracts target info from method and params based on method type */
51+
export function extractTargetInfo(method: string, params: Record<string, unknown>): {
52+
target?: string;
53+
attributes: Record<string, string>
54+
} {
55+
const config = METHOD_CONFIGS[method as keyof typeof METHOD_CONFIGS];
56+
if (!config) {
57+
return { attributes: {} };
58+
}
59+
60+
const target = config.targetField && typeof params?.[config.targetField] === 'string'
61+
? params[config.targetField] as string
62+
: undefined;
63+
64+
return {
65+
target,
66+
attributes: target && config.targetAttribute ? { [config.targetAttribute]: target } : {}
67+
};
68+
}
69+
70+
/** Extracts request arguments based on method type */
71+
export function getRequestArguments(method: string, params: Record<string, unknown>): Record<string, string> {
72+
const args: Record<string, string> = {};
73+
const config = METHOD_CONFIGS[method as keyof typeof METHOD_CONFIGS];
74+
75+
if (!config) {
76+
return args;
77+
}
78+
79+
// Capture arguments from the configured field
80+
if (config.captureArguments && config.argumentsField && params?.[config.argumentsField]) {
81+
const argumentsObj = params[config.argumentsField];
82+
if (typeof argumentsObj === 'object' && argumentsObj !== null) {
83+
for (const [key, value] of Object.entries(argumentsObj as Record<string, unknown>)) {
84+
args[`mcp.request.argument.${key.toLowerCase()}`] = JSON.stringify(value);
85+
}
86+
}
87+
}
88+
89+
// Capture specific fields as arguments
90+
if (config.captureUri && params?.uri) {
91+
args['mcp.request.argument.uri'] = JSON.stringify(params.uri);
92+
}
93+
94+
if (config.captureName && params?.name) {
95+
args['mcp.request.argument.name'] = JSON.stringify(params.name);
96+
}
97+
98+
return args;
99+
}
100+
101+
/** Extracts transport types based on transport constructor name */
102+
export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } {
103+
const transportName = transport.constructor?.name?.toLowerCase() || '';
104+
105+
// Standard MCP transports per specification
106+
if (transportName.includes('stdio')) {
107+
return { mcpTransport: 'stdio', networkTransport: 'pipe' };
108+
}
109+
110+
// Streamable HTTP is the standard HTTP-based transport
111+
if (transportName.includes('streamablehttp') || transportName.includes('streamable')) {
112+
return { mcpTransport: 'http', networkTransport: 'tcp' };
113+
}
114+
115+
// SSE is deprecated (backwards compatibility)
116+
if (transportName.includes('sse')) {
117+
return { mcpTransport: 'sse', networkTransport: 'tcp' };
118+
}
119+
120+
// For custom transports, mark as unknown
121+
return { mcpTransport: 'unknown', networkTransport: 'unknown' };
122+
}
123+
124+
/** Extracts additional attributes for specific notification types */
125+
export function getNotificationAttributes(
126+
method: string,
127+
params: Record<string, unknown>,
128+
): Record<string, string | number> {
129+
const attributes: Record<string, string | number> = {};
130+
131+
switch (method) {
132+
case 'notifications/cancelled':
133+
if (params?.requestId) {
134+
attributes['mcp.cancelled.request_id'] = String(params.requestId);
135+
}
136+
if (params?.reason) {
137+
attributes['mcp.cancelled.reason'] = String(params.reason);
138+
}
139+
break;
140+
141+
case 'notifications/message':
142+
if (params?.level) {
143+
attributes['mcp.logging.level'] = String(params.level);
144+
}
145+
if (params?.logger) {
146+
attributes['mcp.logging.logger'] = String(params.logger);
147+
}
148+
if (params?.data !== undefined) {
149+
attributes['mcp.logging.data_type'] = typeof params.data;
150+
// Store the actual message content
151+
if (typeof params.data === 'string') {
152+
attributes['mcp.logging.message'] = params.data;
153+
} else {
154+
attributes['mcp.logging.message'] = JSON.stringify(params.data);
155+
}
156+
}
157+
break;
158+
159+
case 'notifications/progress':
160+
if (params?.progressToken) {
161+
attributes['mcp.progress.token'] = String(params.progressToken);
162+
}
163+
if (typeof params?.progress === 'number') {
164+
attributes['mcp.progress.current'] = params.progress;
165+
}
166+
if (typeof params?.total === 'number') {
167+
attributes['mcp.progress.total'] = params.total;
168+
if (typeof params?.progress === 'number') {
169+
attributes['mcp.progress.percentage'] = (params.progress / params.total) * 100;
170+
}
171+
}
172+
if (params?.message) {
173+
attributes['mcp.progress.message'] = String(params.message);
174+
}
175+
break;
176+
177+
case 'notifications/resources/updated':
178+
if (params?.uri) {
179+
attributes['mcp.resource.uri'] = String(params.uri);
180+
// Extract protocol from URI
181+
const urlObject = parseStringToURLObject(String(params.uri));
182+
if (urlObject && !isURLObjectRelative(urlObject)) {
183+
attributes['mcp.resource.protocol'] = urlObject.protocol.replace(':', '');
184+
}
185+
}
186+
break;
187+
188+
case 'notifications/initialized':
189+
attributes['mcp.lifecycle.phase'] = 'initialization_complete';
190+
attributes['mcp.protocol.ready'] = 1;
191+
break;
192+
}
193+
194+
return attributes;
195+
}
196+
197+
/** Extracts client connection info from extra handler data */
198+
export function extractClientInfo(extra: ExtraHandlerData): {
199+
address?: string;
200+
port?: number
201+
} {
202+
return {
203+
address: extra?.requestInfo?.remoteAddress ||
204+
extra?.clientAddress ||
205+
extra?.request?.ip ||
206+
extra?.request?.connection?.remoteAddress,
207+
port: extra?.requestInfo?.remotePort ||
208+
extra?.clientPort ||
209+
extra?.request?.connection?.remotePort
210+
};
211+
}
212+
213+
/** Build transport and network attributes */
214+
export function buildTransportAttributes(
215+
transport: MCPTransport,
216+
extra?: ExtraHandlerData,
217+
): Record<string, string | number> {
218+
const sessionId = transport.sessionId;
219+
const clientInfo = extra ? extractClientInfo(extra) : {};
220+
const { mcpTransport, networkTransport } = getTransportTypes(transport);
221+
222+
return {
223+
...(sessionId && { [MCP_SESSION_ID_ATTRIBUTE]: sessionId }),
224+
...(clientInfo.address && { [CLIENT_ADDRESS_ATTRIBUTE]: clientInfo.address }),
225+
...(clientInfo.port && { [CLIENT_PORT_ATTRIBUTE]: clientInfo.port }),
226+
[MCP_TRANSPORT_ATTRIBUTE]: mcpTransport,
227+
[NETWORK_TRANSPORT_ATTRIBUTE]: networkTransport,
228+
[NETWORK_PROTOCOL_VERSION_ATTRIBUTE]: '2.0',
229+
};
230+
}
231+
232+
/** Build type-specific attributes based on message type */
233+
export function buildTypeSpecificAttributes(
234+
type: McpSpanType,
235+
message: JsonRpcRequest | JsonRpcNotification,
236+
params?: Record<string, unknown>,
237+
): Record<string, string | number> {
238+
if (type === 'request') {
239+
const request = message as JsonRpcRequest;
240+
const targetInfo = extractTargetInfo(request.method, params || {});
241+
242+
return {
243+
...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }),
244+
...targetInfo.attributes,
245+
...getRequestArguments(request.method, params || {}),
246+
};
247+
}
248+
249+
// For notifications, only include notification-specific attributes
250+
return getNotificationAttributes(message.method, params || {});
251+
}
252+
253+
/** Simplified tool result attribute extraction */
254+
export function extractSimpleToolAttributes(result: unknown): Record<string, string | number | boolean> {
255+
const attributes: Record<string, string | number | boolean> = {};
256+
257+
if (typeof result === 'object' && result !== null) {
258+
const resultObj = result as Record<string, unknown>;
259+
260+
// Check if this is an error result
261+
if (typeof resultObj.isError === 'boolean') {
262+
attributes['mcp.tool.result.is_error'] = resultObj.isError;
263+
}
264+
265+
// Extract basic content info
266+
if (Array.isArray(resultObj.content)) {
267+
attributes['mcp.tool.result.content_count'] = resultObj.content.length;
268+
269+
// Extract info from all content items
270+
for (let i = 0; i < resultObj.content.length; i++) {
271+
const item = resultObj.content[i];
272+
if (item && typeof item === 'object' && item !== null) {
273+
const contentItem = item as Record<string, unknown>;
274+
const prefix = resultObj.content.length === 1 ? 'mcp.tool.result' : `mcp.tool.result.${i}`;
275+
276+
// Always capture the content type
277+
if (typeof contentItem.type === 'string') {
278+
attributes[`${prefix}.content_type`] = contentItem.type;
279+
}
280+
281+
// Extract common fields generically
282+
if (typeof contentItem.text === 'string') {
283+
const text = contentItem.text;
284+
attributes[`${prefix}.content`] = text.length > 500 ? `${text.substring(0, 497)}...` : text;
285+
}
286+
287+
if (typeof contentItem.mimeType === 'string') {
288+
attributes[`${prefix}.mime_type`] = contentItem.mimeType;
289+
}
290+
291+
if (typeof contentItem.uri === 'string') {
292+
attributes[`${prefix}.uri`] = contentItem.uri;
293+
}
294+
295+
if (typeof contentItem.name === 'string') {
296+
attributes[`${prefix}.name`] = contentItem.name;
297+
}
298+
299+
if (typeof contentItem.data === 'string') {
300+
attributes[`${prefix}.data_size`] = contentItem.data.length;
301+
}
302+
303+
// For embedded resources, check the nested resource object
304+
if (contentItem.resource && typeof contentItem.resource === 'object') {
305+
const resource = contentItem.resource as Record<string, unknown>;
306+
if (typeof resource.uri === 'string') {
307+
attributes[`${prefix}.resource_uri`] = resource.uri;
308+
}
309+
if (typeof resource.mimeType === 'string') {
310+
attributes[`${prefix}.resource_mime_type`] = resource.mimeType;
311+
}
312+
}
313+
}
314+
}
315+
}
316+
}
317+
318+
return attributes;
319+
}

packages/core/src/integrations/mcp-server/attributes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ export const MCP_NOTIFICATION_ORIGIN_VALUE = 'auto.mcp.notification';
117117
/**
118118
* Sentry source value for MCP route spans
119119
*/
120-
export const MCP_ROUTE_SOURCE_VALUE = 'route';
120+
export const MCP_ROUTE_SOURCE_VALUE = 'route';

packages/core/src/integrations/mcp-server/config.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)