Skip to content

Commit 5a97d69

Browse files
committed
test(mcp-server): Add tests for span creation and semantic conventions in MCP server
1 parent 1480b78 commit 5a97d69

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed

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

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,190 @@ describe('wrapMcpServerWithSentry', () => {
169169
expect(() => mockTransport.onclose?.()).not.toThrow();
170170
});
171171
});
172+
173+
describe('Span Creation & Semantic Conventions', () => {
174+
let mockMcpServer: ReturnType<typeof createMockMcpServer>;
175+
let wrappedMcpServer: ReturnType<typeof createMockMcpServer>;
176+
let mockTransport: ReturnType<typeof createMockTransport>;
177+
178+
beforeEach(async () => {
179+
mockMcpServer = createMockMcpServer();
180+
wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
181+
mockTransport = createMockTransport();
182+
mockTransport.sessionId = 'test-session-123';
183+
184+
await wrappedMcpServer.connect(mockTransport);
185+
});
186+
187+
it('should create spans with correct MCP server semantic attributes for tool operations', () => {
188+
const jsonRpcRequest = {
189+
jsonrpc: '2.0',
190+
method: 'tools/call',
191+
id: 'req-1',
192+
params: {
193+
name: 'get-weather',
194+
arguments: {
195+
location: 'Seattle, WA',
196+
units: 'metric'
197+
}
198+
}
199+
};
200+
201+
const extraWithClientInfo = {
202+
requestInfo: {
203+
remoteAddress: '192.168.1.100',
204+
remotePort: 54321
205+
}
206+
};
207+
208+
mockTransport.onmessage?.(jsonRpcRequest, extraWithClientInfo);
209+
210+
expect(tracingModule.startSpan).toHaveBeenCalledWith(
211+
expect.objectContaining({
212+
name: 'tools/call get-weather',
213+
op: 'mcp.server',
214+
forceTransaction: true,
215+
attributes: expect.objectContaining({
216+
// Required
217+
'mcp.method.name': 'tools/call',
218+
// Conditionally Required (tool operation)
219+
'mcp.tool.name': 'get-weather',
220+
'mcp.request.id': 'req-1',
221+
// Recommended
222+
'mcp.session.id': 'test-session-123',
223+
'client.address': '192.168.1.100',
224+
'client.port': 54321,
225+
// Transport attributes
226+
'mcp.transport': 'http',
227+
'network.transport': 'tcp',
228+
'network.protocol.version': '2.0',
229+
// Tool arguments (JSON-stringified)
230+
'mcp.request.argument.location': '"Seattle, WA"',
231+
'mcp.request.argument.units': '"metric"',
232+
// Sentry-specific
233+
'sentry.origin': 'auto.function.mcp_server',
234+
}),
235+
}),
236+
expect.any(Function)
237+
);
238+
});
239+
240+
it('should create spans with correct attributes for resource operations', () => {
241+
const jsonRpcRequest = {
242+
jsonrpc: '2.0',
243+
method: 'resources/read',
244+
id: 'req-2',
245+
params: { uri: 'file:///docs/api.md' }
246+
};
247+
248+
mockTransport.onmessage?.(jsonRpcRequest, {});
249+
250+
expect(tracingModule.startSpan).toHaveBeenCalledWith(
251+
expect.objectContaining({
252+
name: 'resources/read file:///docs/api.md',
253+
op: 'mcp.server',
254+
attributes: expect.objectContaining({
255+
// Required
256+
'mcp.method.name': 'resources/read',
257+
// Conditionally Required (resource operation)
258+
'mcp.resource.uri': 'file:///docs/api.md',
259+
'mcp.request.id': 'req-2',
260+
// Recommended
261+
'mcp.session.id': 'test-session-123',
262+
}),
263+
}),
264+
expect.any(Function)
265+
);
266+
});
267+
268+
it('should create spans with correct attributes for prompt operations', () => {
269+
const jsonRpcRequest = {
270+
jsonrpc: '2.0',
271+
method: 'prompts/get',
272+
id: 'req-3',
273+
params: { name: 'analyze-code' }
274+
};
275+
276+
mockTransport.onmessage?.(jsonRpcRequest, {});
277+
278+
expect(tracingModule.startSpan).toHaveBeenCalledWith(
279+
expect.objectContaining({
280+
name: 'prompts/get analyze-code',
281+
op: 'mcp.server',
282+
attributes: expect.objectContaining({
283+
// Required
284+
'mcp.method.name': 'prompts/get',
285+
// Conditionally Required (prompt operation)
286+
'mcp.prompt.name': 'analyze-code',
287+
'mcp.request.id': 'req-3',
288+
// Recommended
289+
'mcp.session.id': 'test-session-123',
290+
}),
291+
}),
292+
expect.any(Function)
293+
);
294+
});
295+
296+
it('should create spans with correct attributes for notifications (no request id)', () => {
297+
const jsonRpcNotification = {
298+
jsonrpc: '2.0',
299+
method: 'notifications/tools/list_changed',
300+
params: {}
301+
};
302+
303+
mockTransport.onmessage?.(jsonRpcNotification, {});
304+
305+
expect(tracingModule.startSpan).toHaveBeenCalledWith(
306+
expect.objectContaining({
307+
name: 'notifications/tools/list_changed',
308+
op: 'mcp.server',
309+
attributes: expect.objectContaining({
310+
// Required
311+
'mcp.method.name': 'notifications/tools/list_changed',
312+
// Recommended
313+
'mcp.session.id': 'test-session-123',
314+
// Notification-specific
315+
'mcp.notification.direction': 'client_to_server',
316+
// Sentry-specific
317+
'sentry.origin': 'auto.mcp.notification',
318+
}),
319+
}),
320+
expect.any(Function)
321+
);
322+
323+
// Should not include mcp.request.id for notifications
324+
const callArgs = vi.mocked(tracingModule.startSpan).mock.calls[0];
325+
expect(callArgs).toBeDefined();
326+
const attributes = callArgs?.[0]?.attributes;
327+
expect(attributes).not.toHaveProperty('mcp.request.id');
328+
});
329+
330+
it('should create spans for list operations without target in name', () => {
331+
const jsonRpcRequest = {
332+
jsonrpc: '2.0',
333+
method: 'tools/list',
334+
id: 'req-4',
335+
params: {}
336+
};
337+
338+
mockTransport.onmessage?.(jsonRpcRequest, {});
339+
340+
expect(tracingModule.startSpan).toHaveBeenCalledWith(
341+
expect.objectContaining({
342+
name: 'tools/list',
343+
op: 'mcp.server',
344+
attributes: expect.objectContaining({
345+
'mcp.method.name': 'tools/list',
346+
'mcp.request.id': 'req-4',
347+
'mcp.session.id': 'test-session-123',
348+
}),
349+
}),
350+
expect.any(Function)
351+
);
352+
});
353+
354+
355+
});
172356
});
173357

174358
// Test helpers

0 commit comments

Comments
 (0)