Skip to content

Commit 3d58496

Browse files
authored
feat(core): Capture initialize attributes on MCP servers (#18531)
With this PR we get this attributes from the initialization request and response into the `initialize` span: - `mcp.client.name`, `mcp.client.version`, `mcp.client.title` (from request) - `mcp.server.name`, `mcp.server.version`, `mcp.server.title` (from response) - `mcp.protocol.version` (from both request and response) Changes: - Extract client info and protocol version from the `initialize` request and set them on the span after creation in `transport.ts` - Extract server info and protocol version from the `initialize` response in `completeSpanWithResults` and add them to the span - Add two new helper functions `buildClientAttributesFromInfo` and `buildServerAttributesFromInfo` to build attributes directly from `PartyInfo` objects Closes #18532 (added automatically)
1 parent 2c45b73 commit 3d58496

File tree

7 files changed

+193
-9
lines changed

7 files changed

+193
-9
lines changed

dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,21 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => {
1111
version: '1.0.0',
1212
});
1313

14+
const initializeTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => {
15+
return transactionEvent.transaction === 'initialize';
16+
});
17+
1418
await client.connect(transport);
1519

20+
await test.step('initialize handshake', async () => {
21+
const initializeTransaction = await initializeTransactionPromise;
22+
expect(initializeTransaction).toBeDefined();
23+
expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server');
24+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize');
25+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client');
26+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo');
27+
});
28+
1629
await test.step('tool handler', async () => {
1730
const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => {
1831
return transactionEvent.transaction === 'POST /messages';

dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,21 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => {
1111
version: '1.0.0',
1212
});
1313

14+
const initializeTransactionPromise = waitForTransaction('node-express', transactionEvent => {
15+
return transactionEvent.transaction === 'initialize';
16+
});
17+
1418
await client.connect(transport);
1519

20+
await test.step('initialize handshake', async () => {
21+
const initializeTransaction = await initializeTransactionPromise;
22+
expect(initializeTransaction).toBeDefined();
23+
expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server');
24+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize');
25+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client');
26+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo');
27+
});
28+
1629
await test.step('tool handler', async () => {
1730
const postTransactionPromise = waitForTransaction('node-express', transactionEvent => {
1831
return transactionEvent.transaction === 'POST /messages';

dev-packages/e2e-tests/test-applications/tsx-express/tests/mcp.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,21 @@ test('Records transactions for mcp handlers', async ({ baseURL }) => {
1111
version: '1.0.0',
1212
});
1313

14+
const initializeTransactionPromise = waitForTransaction('tsx-express', transactionEvent => {
15+
return transactionEvent.transaction === 'initialize';
16+
});
17+
1418
await client.connect(transport);
1519

20+
await test.step('initialize handshake', async () => {
21+
const initializeTransaction = await initializeTransactionPromise;
22+
expect(initializeTransaction).toBeDefined();
23+
expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server');
24+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize');
25+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client');
26+
expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo');
27+
});
28+
1629
await test.step('tool handler', async () => {
1730
const postTransactionPromise = waitForTransaction('tsx-express', transactionEvent => {
1831
return transactionEvent.transaction === 'POST /messages';

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import { getClient } from '../../currentScopes';
1010
import { SPAN_STATUS_ERROR } from '../../tracing';
1111
import type { Span } from '../../types-hoist/span';
12+
import { MCP_PROTOCOL_VERSION_ATTRIBUTE } from './attributes';
1213
import { filterMcpPiiFromSpanData } from './piiFiltering';
1314
import { extractPromptResultAttributes, extractToolResultAttributes } from './resultExtraction';
15+
import { buildServerAttributesFromInfo, extractSessionDataFromInitializeResponse } from './sessionExtraction';
1416
import type { MCPTransport, RequestId, RequestSpanMapValue } from './types';
1517

1618
/**
@@ -51,18 +53,30 @@ export function storeSpanForRequest(transport: MCPTransport, requestId: RequestI
5153
}
5254

5355
/**
54-
* Completes span with tool results and cleans up correlation
56+
* Completes span with results and cleans up correlation
5557
* @param transport - MCP transport instance
5658
* @param requestId - Request identifier
57-
* @param result - Tool execution result for attribute extraction
59+
* @param result - Execution result for attribute extraction
5860
*/
5961
export function completeSpanWithResults(transport: MCPTransport, requestId: RequestId, result: unknown): void {
6062
const spanMap = getOrCreateSpanMap(transport);
6163
const spanData = spanMap.get(requestId);
6264
if (spanData) {
6365
const { span, method } = spanData;
6466

65-
if (method === 'tools/call') {
67+
if (method === 'initialize') {
68+
const sessionData = extractSessionDataFromInitializeResponse(result);
69+
const serverAttributes = buildServerAttributesFromInfo(sessionData.serverInfo);
70+
71+
const initAttributes: Record<string, string | number> = {
72+
...serverAttributes,
73+
};
74+
if (sessionData.protocolVersion) {
75+
initAttributes[MCP_PROTOCOL_VERSION_ATTRIBUTE] = sessionData.protocolVersion;
76+
}
77+
78+
span.setAttributes(initAttributes);
79+
} else if (method === 'tools/call') {
6680
const rawToolAttributes = extractToolResultAttributes(result);
6781
const client = getClient();
6882
const sendDefaultPii = Boolean(client?.getOptions().sendDefaultPii);

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ export function getClientAttributes(transport: MCPTransport): Record<string, str
105105
return attributes;
106106
}
107107

108+
/**
109+
* Build client attributes from PartyInfo directly
110+
* @param clientInfo - Client party info
111+
* @returns Client attributes for span instrumentation
112+
*/
113+
export function buildClientAttributesFromInfo(clientInfo?: PartyInfo): Record<string, string> {
114+
const attributes: Record<string, string> = {};
115+
116+
if (clientInfo?.name) {
117+
attributes['mcp.client.name'] = clientInfo.name;
118+
}
119+
if (clientInfo?.title) {
120+
attributes['mcp.client.title'] = clientInfo.title;
121+
}
122+
if (clientInfo?.version) {
123+
attributes['mcp.client.version'] = clientInfo.version;
124+
}
125+
126+
return attributes;
127+
}
128+
108129
/**
109130
* Build server attributes from stored server info
110131
* @param transport - MCP transport instance
@@ -127,6 +148,27 @@ export function getServerAttributes(transport: MCPTransport): Record<string, str
127148
return attributes;
128149
}
129150

151+
/**
152+
* Build server attributes from PartyInfo directly
153+
* @param serverInfo - Server party info
154+
* @returns Server attributes for span instrumentation
155+
*/
156+
export function buildServerAttributesFromInfo(serverInfo?: PartyInfo): Record<string, string> {
157+
const attributes: Record<string, string> = {};
158+
159+
if (serverInfo?.name) {
160+
attributes[MCP_SERVER_NAME_ATTRIBUTE] = serverInfo.name;
161+
}
162+
if (serverInfo?.title) {
163+
attributes[MCP_SERVER_TITLE_ATTRIBUTE] = serverInfo.title;
164+
}
165+
if (serverInfo?.version) {
166+
attributes[MCP_SERVER_VERSION_ATTRIBUTE] = serverInfo.version;
167+
}
168+
169+
return attributes;
170+
}
171+
130172
/**
131173
* Extracts client connection info from extra handler data
132174
* @param extra - Extra handler data containing connection info

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
import { getIsolationScope, withIsolationScope } from '../../currentScopes';
99
import { startInactiveSpan, withActiveSpan } from '../../tracing';
1010
import { fill } from '../../utils/object';
11+
import { MCP_PROTOCOL_VERSION_ATTRIBUTE } from './attributes';
1112
import { cleanupPendingSpansForTransport, completeSpanWithResults, storeSpanForRequest } from './correlation';
1213
import { captureError } from './errorCapture';
13-
import { extractSessionDataFromInitializeRequest, extractSessionDataFromInitializeResponse } from './sessionExtraction';
14+
import {
15+
buildClientAttributesFromInfo,
16+
extractSessionDataFromInitializeRequest,
17+
extractSessionDataFromInitializeResponse,
18+
} from './sessionExtraction';
1419
import {
1520
cleanupSessionDataForTransport,
1621
storeSessionDataForTransport,
1722
updateSessionDataForTransport,
1823
} from './sessionManagement';
1924
import { buildMcpServerSpanConfig, createMcpNotificationSpan, createMcpOutgoingNotificationSpan } from './spans';
20-
import type { ExtraHandlerData, MCPTransport } from './types';
25+
import type { ExtraHandlerData, MCPTransport, SessionData } from './types';
2126
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, isValidContentItem } from './validation';
2227

2328
/**
@@ -31,10 +36,13 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
3136
fill(transport, 'onmessage', originalOnMessage => {
3237
return function (this: MCPTransport, message: unknown, extra?: unknown) {
3338
if (isJsonRpcRequest(message)) {
34-
if (message.method === 'initialize') {
39+
const isInitialize = message.method === 'initialize';
40+
let initSessionData: SessionData | undefined;
41+
42+
if (isInitialize) {
3543
try {
36-
const sessionData = extractSessionDataFromInitializeRequest(message);
37-
storeSessionDataForTransport(this, sessionData);
44+
initSessionData = extractSessionDataFromInitializeRequest(message);
45+
storeSessionDataForTransport(this, initSessionData);
3846
} catch {
3947
// noop
4048
}
@@ -46,6 +54,16 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
4654
const spanConfig = buildMcpServerSpanConfig(message, this, extra as ExtraHandlerData);
4755
const span = startInactiveSpan(spanConfig);
4856

57+
// For initialize requests, add client info directly to span (works even for stateless transports)
58+
if (isInitialize && initSessionData) {
59+
span.setAttributes({
60+
...buildClientAttributesFromInfo(initSessionData.clientInfo),
61+
...(initSessionData.protocolVersion && {
62+
[MCP_PROTOCOL_VERSION_ATTRIBUTE]: initSessionData.protocolVersion,
63+
}),
64+
});
65+
}
66+
4967
storeSpanForRequest(this, message.id, span, message.method);
5068

5169
return withActiveSpan(span, () => {

packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ describe('MCP Server Transport Instrumentation', () => {
571571

572572
it('excludes sessionId when undefined', () => {
573573
const transport = createMockTransport();
574-
transport.sessionId = undefined;
574+
transport.sessionId = '';
575575
const attributes = buildTransportAttributes(transport);
576576

577577
expect(attributes['mcp.session.id']).toBeUndefined();
@@ -584,4 +584,75 @@ describe('MCP Server Transport Instrumentation', () => {
584584
expect(attributes['mcp.session.id']).toBeUndefined();
585585
});
586586
});
587+
588+
describe('Initialize Span Attributes', () => {
589+
it('should add client info to initialize span on request', async () => {
590+
const mockMcpServer = createMockMcpServer();
591+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
592+
const transport = createMockTransport();
593+
transport.sessionId = '';
594+
595+
await wrappedMcpServer.connect(transport);
596+
597+
const mockSpan = { setAttributes: vi.fn(), end: vi.fn() };
598+
startInactiveSpanSpy.mockReturnValue(mockSpan);
599+
600+
transport.onmessage?.(
601+
{
602+
jsonrpc: '2.0',
603+
method: 'initialize',
604+
id: 'init-1',
605+
params: { protocolVersion: '2025-06-18', clientInfo: { name: 'test-client', version: '1.0.0' } },
606+
},
607+
{},
608+
);
609+
610+
expect(mockSpan.setAttributes).toHaveBeenCalledWith(
611+
expect.objectContaining({
612+
'mcp.client.name': 'test-client',
613+
'mcp.client.version': '1.0.0',
614+
'mcp.protocol.version': '2025-06-18',
615+
}),
616+
);
617+
});
618+
619+
it('should add server info to initialize span on response', async () => {
620+
const mockMcpServer = createMockMcpServer();
621+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
622+
const transport = createMockTransport();
623+
624+
await wrappedMcpServer.connect(transport);
625+
626+
const mockSpan = { setAttributes: vi.fn(), end: vi.fn() };
627+
startInactiveSpanSpy.mockReturnValue(mockSpan as any);
628+
629+
transport.onmessage?.(
630+
{
631+
jsonrpc: '2.0',
632+
method: 'initialize',
633+
id: 'init-1',
634+
params: { protocolVersion: '2025-06-18', clientInfo: { name: 'test-client', version: '1.0.0' } },
635+
},
636+
{},
637+
);
638+
639+
await transport.send?.({
640+
jsonrpc: '2.0',
641+
id: 'init-1',
642+
result: {
643+
protocolVersion: '2025-06-18',
644+
serverInfo: { name: 'test-server', version: '2.0.0' },
645+
capabilities: {},
646+
},
647+
});
648+
649+
expect(mockSpan.setAttributes).toHaveBeenCalledWith(
650+
expect.objectContaining({
651+
'mcp.server.name': 'test-server',
652+
'mcp.server.version': '2.0.0',
653+
}),
654+
);
655+
expect(mockSpan.end).toHaveBeenCalled();
656+
});
657+
});
587658
});

0 commit comments

Comments
 (0)