From 7f68a56094ac0104100036260acd9efb3608de42 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Sat, 2 Aug 2025 01:37:12 -0700 Subject: [PATCH 01/16] fix(mcp-server): Add defensive patches for StreamableHTTPServerTransport edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds defensive handling for edge cases in MCP server instrumentation when working with StreamableHTTPServerTransport and similar transport implementations. Changes: - Add transport constructor null checks in getTransportTypes() - Graceful sessionId undefined handling in buildTransportAttributes() - WeakMap correlation fallback system for invalid transport objects - Type validation for WeakMap operations in correlation.ts The patches prevent runtime errors during MCP initialization and session establishment while maintaining backward compatibility and proper instrumentation functionality. Includes comprehensive test coverage for all edge cases and transport scenarios. Fixes edge cases where: - Transport objects have undefined/null constructors - sessionId is undefined during initialization phases - Transport objects cannot be used as WeakMap keys - Invalid transport objects are passed to correlation functions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../mcp-server/attributeExtraction.ts | 10 +- .../integrations/mcp-server/correlation.ts | 12 + .../mcp-server/attributeExtraction.test.ts | 181 +++++++++++++++ .../mcp-server/correlation.test.ts | 206 ++++++++++++++++++ 4 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts create mode 100644 packages/core/test/lib/integrations/mcp-server/correlation.test.ts diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 68eade987a08..4a94d1818778 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -45,7 +45,11 @@ import type { * @returns Transport type mapping for span attributes */ export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } { - const transportName = transport.constructor?.name?.toLowerCase() || ''; + // Handle undefined transport gracefully while preserving type detection + if (!transport || !transport.constructor) { + return { mcpTransport: 'unknown', networkTransport: 'unknown' }; + } + const transportName = transport.constructor.name?.toLowerCase() || ''; if (transportName.includes('stdio')) { return { mcpTransport: 'stdio', networkTransport: 'pipe' }; @@ -265,7 +269,9 @@ export function buildTransportAttributes( transport: MCPTransport, extra?: ExtraHandlerData, ): Record { - const sessionId = transport.sessionId; + // Gracefully handle undefined sessionId during MCP initialization + // Respects client-provided sessions and waits for proper session establishment + const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined; const clientInfo = extra ? extractClientInfo(extra) : {}; const { mcpTransport, networkTransport } = getTransportTypes(transport); const clientAttributes = getClientAttributes(transport); diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts index 7f00341bdd5a..889a34477a13 100644 --- a/packages/core/src/integrations/mcp-server/correlation.ts +++ b/packages/core/src/integrations/mcp-server/correlation.ts @@ -19,6 +19,12 @@ import type { MCPTransport, RequestId, RequestSpanMapValue } from './types'; */ const transportToSpanMap = new WeakMap>(); +/** + * Fallback span map for invalid transport objects + * @internal Used when transport objects cannot be used as WeakMap keys + */ +const fallbackSpanMap = new Map(); + /** * Gets or creates the span map for a specific transport instance * @internal @@ -26,6 +32,12 @@ const transportToSpanMap = new WeakMap { + // Handle invalid transport values for WeakMap while preserving correlation + if (!transport || typeof transport !== 'object') { + // Return persistent fallback Map to maintain correlation across calls + return fallbackSpanMap; + } + let spanMap = transportToSpanMap.get(transport); if (!spanMap) { spanMap = new Map(); diff --git a/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts b/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts new file mode 100644 index 000000000000..3d112ff7e519 --- /dev/null +++ b/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from 'vitest'; +import { + buildTransportAttributes, + getTransportTypes, +} from '../../../../src/integrations/mcp-server/attributeExtraction'; +import type { MCPTransport } from '../../../../src/integrations/mcp-server/types'; + +describe('attributeExtraction edge cases', () => { + describe('getTransportTypes', () => { + it('handles undefined transport gracefully', () => { + const result = getTransportTypes(undefined as any); + expect(result).toEqual({ + mcpTransport: 'unknown', + networkTransport: 'unknown', + }); + }); + + it('handles null transport gracefully', () => { + const result = getTransportTypes(null as any); + expect(result).toEqual({ + mcpTransport: 'unknown', + networkTransport: 'unknown', + }); + }); + + it('handles transport with null constructor', () => { + const transport = { + constructor: null, + } as any; + + const result = getTransportTypes(transport); + expect(result).toEqual({ + mcpTransport: 'unknown', + networkTransport: 'unknown', + }); + }); + + it('handles transport with undefined constructor', () => { + const transport = { + constructor: undefined, + } as any; + + const result = getTransportTypes(transport); + expect(result).toEqual({ + mcpTransport: 'unknown', + networkTransport: 'unknown', + }); + }); + + it('correctly identifies StreamableHTTPServerTransport', () => { + class StreamableHTTPServerTransport { + sessionId = 'test-session'; + } + + const transport = new StreamableHTTPServerTransport() as MCPTransport; + const result = getTransportTypes(transport); + + expect(result).toEqual({ + mcpTransport: 'http', + networkTransport: 'tcp', + }); + }); + + it('correctly identifies SSE transport', () => { + class SSEServerTransport { + sessionId = 'sse-session'; + } + + const transport = new SSEServerTransport() as MCPTransport; + const result = getTransportTypes(transport); + + expect(result).toEqual({ + mcpTransport: 'sse', + networkTransport: 'tcp', + }); + }); + + it('correctly identifies stdio transport', () => { + class StdioServerTransport { + sessionId = 'stdio-session'; + } + + const transport = new StdioServerTransport() as MCPTransport; + const result = getTransportTypes(transport); + + expect(result).toEqual({ + mcpTransport: 'stdio', + networkTransport: 'pipe', + }); + }); + }); + + describe('buildTransportAttributes', () => { + it('handles undefined sessionId gracefully', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + // No sessionId property + } as MCPTransport; + + const attributes = buildTransportAttributes(transport); + + // Should not include sessionId in attributes when undefined + expect(attributes['mcp.session.id']).toBeUndefined(); + expect(attributes['mcp.transport']).toBe('http'); + }); + + it('handles transport without sessionId property', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + // sessionId property doesn't exist at all + } as any; + + const attributes = buildTransportAttributes(transport); + + // Should not include sessionId in attributes + expect(attributes['mcp.session.id']).toBeUndefined(); + expect(attributes['mcp.transport']).toBe('http'); + }); + + it('includes sessionId when properly set', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + sessionId: 'test-session-123', + } as MCPTransport; + + const attributes = buildTransportAttributes(transport); + + expect(attributes['mcp.session.id']).toBe('test-session-123'); + expect(attributes['mcp.transport']).toBe('http'); + }); + + it('handles null sessionId gracefully', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + sessionId: null, + } as any; + + const attributes = buildTransportAttributes(transport); + + // Should not include null sessionId in attributes + expect(attributes['mcp.session.id']).toBeUndefined(); + expect(attributes['mcp.transport']).toBe('http'); + }); + + it('handles empty string sessionId', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + sessionId: '', + } as MCPTransport; + + const attributes = buildTransportAttributes(transport); + + // Empty string is falsy, so should not be included + expect(attributes['mcp.session.id']).toBeUndefined(); + expect(attributes['mcp.transport']).toBe('http'); + }); + + it('preserves all other attributes when sessionId is undefined', () => { + const transport = { + constructor: { name: 'StreamableHTTPServerTransport' }, + // No sessionId + } as MCPTransport; + + const attributes = buildTransportAttributes(transport, { + clientAddress: '127.0.0.1', + clientPort: 8080, + }); + + expect(attributes).toMatchObject({ + 'mcp.transport': 'http', + 'network.transport': 'tcp', + 'network.protocol.version': '2.0', + 'client.address': '127.0.0.1', + 'client.port': 8080, + }); + + // sessionId should not be present + expect(attributes['mcp.session.id']).toBeUndefined(); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/test/lib/integrations/mcp-server/correlation.test.ts b/packages/core/test/lib/integrations/mcp-server/correlation.test.ts new file mode 100644 index 000000000000..d88c38f1f5ab --- /dev/null +++ b/packages/core/test/lib/integrations/mcp-server/correlation.test.ts @@ -0,0 +1,206 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getCurrentScope } from '../../../../src/currentScopes'; +import { + completeSpanFromToolResult, + storeSpanForRequest, +} from '../../../../src/integrations/mcp-server/correlation'; +import type { MCPTransport } from '../../../../src/integrations/mcp-server/types'; +import { createMockTransport } from './testUtils'; + +// Mock getCurrentScope +vi.mock('../../../../src/currentScopes', () => ({ + getCurrentScope: vi.fn(), +})); + +describe('correlation edge cases', () => { + let mockSpan: any; + + beforeEach(() => { + mockSpan = { + setStatus: vi.fn(), + setData: vi.fn(), + end: vi.fn(), + isRecording: vi.fn().mockReturnValue(true), + }; + + (getCurrentScope as any).mockReturnValue({ + getSpan: vi.fn().mockReturnValue(mockSpan), + }); + }); + + describe('WeakMap correlation fallback', () => { + it('handles null transport gracefully', () => { + const transport = null as any; + const requestId = 'test-request-1'; + + // Should not throw when storing span for null transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('handles undefined transport gracefully', () => { + const transport = undefined as any; + const requestId = 'test-request-2'; + + // Should not throw when storing span for undefined transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('handles non-object transport gracefully', () => { + const transport = 'not-an-object' as any; + const requestId = 'test-request-3'; + + // Should not throw when storing span for string transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('handles number transport gracefully', () => { + const transport = 123 as any; + const requestId = 'test-request-4'; + + // Should not throw when storing span for number transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('handles boolean transport gracefully', () => { + const transport = true as any; + const requestId = 'test-request-5'; + + // Should not throw when storing span for boolean transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('uses fallback map for invalid transports but maintains correlation', () => { + const invalidTransport1 = null as any; + const invalidTransport2 = 'string-transport' as any; + const requestId = 'test-request-6'; + + // Store span for first invalid transport + storeSpanForRequest(invalidTransport1, requestId, mockSpan, 'tools/call'); + + // Complete span from different invalid transport with same request ID + // This should work because they both use the fallback map + expect(() => { + completeSpanFromToolResult(invalidTransport2, requestId, { + content: [{ type: 'text', text: 'result' }], + }); + }).not.toThrow(); + }); + + it('works normally with valid transport objects', () => { + const transport = createMockTransport(); + const requestId = 'test-request-7'; + + // Should work normally with valid transport + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + completeSpanFromToolResult(transport, requestId, { + content: [{ type: 'text', text: 'result' }], + }); + }).not.toThrow(); + + expect(mockSpan.end).toHaveBeenCalled(); + }); + + it('maintains separate correlation for valid transports', () => { + const transport1 = createMockTransport(); + const transport2 = createMockTransport(); + const requestId = 'test-request-8'; + + const mockSpan1 = { ...mockSpan, id: 'span1', end: vi.fn() }; + const mockSpan2 = { ...mockSpan, id: 'span2', end: vi.fn() }; + + // Store spans for different transports with same request ID + storeSpanForRequest(transport1, requestId, mockSpan1, 'tools/call'); + storeSpanForRequest(transport2, requestId, mockSpan2, 'tools/call'); + + // Complete span for transport1 should only affect span1 + completeSpanFromToolResult(transport1, requestId, { + content: [{ type: 'text', text: 'result1' }], + }); + + expect(mockSpan1.end).toHaveBeenCalled(); + expect(mockSpan2.end).not.toHaveBeenCalled(); + }); + + it('isolates fallback map from valid transport maps', () => { + const validTransport = createMockTransport(); + const invalidTransport = null as any; + const requestId = 'test-request-9'; + + const mockSpan1 = { ...mockSpan, id: 'valid-span', end: vi.fn() }; + const mockSpan2 = { ...mockSpan, id: 'fallback-span', end: vi.fn() }; + + // Store spans for both valid and invalid transports + storeSpanForRequest(validTransport, requestId, mockSpan1, 'tools/call'); + storeSpanForRequest(invalidTransport, requestId, mockSpan2, 'tools/call'); + + // Complete span for valid transport should only affect valid span + completeSpanFromToolResult(validTransport, requestId, { + content: [{ type: 'text', text: 'valid-result' }], + }); + + expect(mockSpan1.end).toHaveBeenCalled(); + expect(mockSpan2.end).not.toHaveBeenCalled(); + + // Complete span for invalid transport should only affect fallback span + completeSpanFromToolResult(invalidTransport, requestId, { + content: [{ type: 'text', text: 'fallback-result' }], + }); + + expect(mockSpan2.end).toHaveBeenCalled(); + }); + }); + + describe('edge case transport objects', () => { + it('handles transport with null prototype', () => { + const transport = Object.create(null) as MCPTransport; + const requestId = 'test-request-10'; + + // Should not throw with null prototype object + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + }).not.toThrow(); + }); + + it('handles frozen transport object', () => { + const transport = Object.freeze(createMockTransport()); + const requestId = 'test-request-11'; + + // Should work with frozen object + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + completeSpanFromToolResult(transport, requestId, { + content: [{ type: 'text', text: 'result' }], + }); + }).not.toThrow(); + + expect(mockSpan.end).toHaveBeenCalled(); + }); + + it('handles transport with circular references', () => { + const transport = createMockTransport() as any; + transport.self = transport; // Create circular reference + const requestId = 'test-request-12'; + + // Should work with circular references + expect(() => { + storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); + completeSpanFromToolResult(transport, requestId, { + content: [{ type: 'text', text: 'result' }], + }); + }).not.toThrow(); + + expect(mockSpan.end).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file From a8b39b7328a51b2e1c22b01ea0b31c0a27395db3 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Sat, 2 Aug 2025 09:39:25 -0700 Subject: [PATCH 02/16] refactor(mcp-server): Simplify defensive patches to match production patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Streamline sessionId handling with cleaner undefined checks - Use optional chaining for transport constructor validation - Maintain WeakMap fallback system for invalid transport objects - Align TypeScript patterns with working JavaScript build output These changes ensure compatibility with StreamableHTTPServerTransport while following current Sentry v10.0.0 defensive programming patterns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../mcp-server/attributeExtraction.ts | 7 ++- .../integrations/mcp-server/correlation.ts | 2 +- .../mcp-server/attributeExtraction.test.ts | 44 +++++++------- .../mcp-server/correlation.test.ts | 57 +++++++++---------- 4 files changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 4a94d1818778..9f65b2241e31 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -46,7 +46,7 @@ import type { */ export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } { // Handle undefined transport gracefully while preserving type detection - if (!transport || !transport.constructor) { + if (!transport?.constructor) { return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } const transportName = transport.constructor.name?.toLowerCase() || ''; @@ -269,9 +269,12 @@ export function buildTransportAttributes( transport: MCPTransport, extra?: ExtraHandlerData, ): Record { - // Gracefully handle undefined sessionId during MCP initialization + // PATCHED: Gracefully handle undefined sessionId during MCP initialization // Respects client-provided sessions and waits for proper session establishment const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined; + + // Note: sessionId may be undefined during initial setup - this is expected behavior + // The actual session should be established by the client during the initialize flow const clientInfo = extra ? extractClientInfo(extra) : {}; const { mcpTransport, networkTransport } = getTransportTypes(transport); const clientAttributes = getClientAttributes(transport); diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts index 889a34477a13..bb857c363473 100644 --- a/packages/core/src/integrations/mcp-server/correlation.ts +++ b/packages/core/src/integrations/mcp-server/correlation.ts @@ -37,7 +37,7 @@ function getOrCreateSpanMap(transport: MCPTransport): Map { const transport = { constructor: null, } as any; - + const result = getTransportTypes(transport); expect(result).toEqual({ mcpTransport: 'unknown', @@ -39,7 +39,7 @@ describe('attributeExtraction edge cases', () => { const transport = { constructor: undefined, } as any; - + const result = getTransportTypes(transport); expect(result).toEqual({ mcpTransport: 'unknown', @@ -51,10 +51,10 @@ describe('attributeExtraction edge cases', () => { class StreamableHTTPServerTransport { sessionId = 'test-session'; } - + const transport = new StreamableHTTPServerTransport() as MCPTransport; const result = getTransportTypes(transport); - + expect(result).toEqual({ mcpTransport: 'http', networkTransport: 'tcp', @@ -65,10 +65,10 @@ describe('attributeExtraction edge cases', () => { class SSEServerTransport { sessionId = 'sse-session'; } - + const transport = new SSEServerTransport() as MCPTransport; const result = getTransportTypes(transport); - + expect(result).toEqual({ mcpTransport: 'sse', networkTransport: 'tcp', @@ -79,10 +79,10 @@ describe('attributeExtraction edge cases', () => { class StdioServerTransport { sessionId = 'stdio-session'; } - + const transport = new StdioServerTransport() as MCPTransport; const result = getTransportTypes(transport); - + expect(result).toEqual({ mcpTransport: 'stdio', networkTransport: 'pipe', @@ -96,9 +96,9 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, // No sessionId property } as MCPTransport; - + const attributes = buildTransportAttributes(transport); - + // Should not include sessionId in attributes when undefined expect(attributes['mcp.session.id']).toBeUndefined(); expect(attributes['mcp.transport']).toBe('http'); @@ -109,9 +109,9 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, // sessionId property doesn't exist at all } as any; - + const attributes = buildTransportAttributes(transport); - + // Should not include sessionId in attributes expect(attributes['mcp.session.id']).toBeUndefined(); expect(attributes['mcp.transport']).toBe('http'); @@ -122,9 +122,9 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, sessionId: 'test-session-123', } as MCPTransport; - + const attributes = buildTransportAttributes(transport); - + expect(attributes['mcp.session.id']).toBe('test-session-123'); expect(attributes['mcp.transport']).toBe('http'); }); @@ -134,9 +134,9 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, sessionId: null, } as any; - + const attributes = buildTransportAttributes(transport); - + // Should not include null sessionId in attributes expect(attributes['mcp.session.id']).toBeUndefined(); expect(attributes['mcp.transport']).toBe('http'); @@ -147,9 +147,9 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, sessionId: '', } as MCPTransport; - + const attributes = buildTransportAttributes(transport); - + // Empty string is falsy, so should not be included expect(attributes['mcp.session.id']).toBeUndefined(); expect(attributes['mcp.transport']).toBe('http'); @@ -160,12 +160,12 @@ describe('attributeExtraction edge cases', () => { constructor: { name: 'StreamableHTTPServerTransport' }, // No sessionId } as MCPTransport; - + const attributes = buildTransportAttributes(transport, { clientAddress: '127.0.0.1', clientPort: 8080, }); - + expect(attributes).toMatchObject({ 'mcp.transport': 'http', 'network.transport': 'tcp', @@ -173,9 +173,9 @@ describe('attributeExtraction edge cases', () => { 'client.address': '127.0.0.1', 'client.port': 8080, }); - + // sessionId should not be present expect(attributes['mcp.session.id']).toBeUndefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/core/test/lib/integrations/mcp-server/correlation.test.ts b/packages/core/test/lib/integrations/mcp-server/correlation.test.ts index d88c38f1f5ab..31114bfcfc51 100644 --- a/packages/core/test/lib/integrations/mcp-server/correlation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/correlation.test.ts @@ -1,9 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getCurrentScope } from '../../../../src/currentScopes'; -import { - completeSpanFromToolResult, - storeSpanForRequest, -} from '../../../../src/integrations/mcp-server/correlation'; +import { completeSpanFromToolResult, storeSpanForRequest } from '../../../../src/integrations/mcp-server/correlation'; import type { MCPTransport } from '../../../../src/integrations/mcp-server/types'; import { createMockTransport } from './testUtils'; @@ -22,7 +19,7 @@ describe('correlation edge cases', () => { end: vi.fn(), isRecording: vi.fn().mockReturnValue(true), }; - + (getCurrentScope as any).mockReturnValue({ getSpan: vi.fn().mockReturnValue(mockSpan), }); @@ -32,7 +29,7 @@ describe('correlation edge cases', () => { it('handles null transport gracefully', () => { const transport = null as any; const requestId = 'test-request-1'; - + // Should not throw when storing span for null transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -42,7 +39,7 @@ describe('correlation edge cases', () => { it('handles undefined transport gracefully', () => { const transport = undefined as any; const requestId = 'test-request-2'; - + // Should not throw when storing span for undefined transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -52,7 +49,7 @@ describe('correlation edge cases', () => { it('handles non-object transport gracefully', () => { const transport = 'not-an-object' as any; const requestId = 'test-request-3'; - + // Should not throw when storing span for string transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -62,7 +59,7 @@ describe('correlation edge cases', () => { it('handles number transport gracefully', () => { const transport = 123 as any; const requestId = 'test-request-4'; - + // Should not throw when storing span for number transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -72,7 +69,7 @@ describe('correlation edge cases', () => { it('handles boolean transport gracefully', () => { const transport = true as any; const requestId = 'test-request-5'; - + // Should not throw when storing span for boolean transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -83,10 +80,10 @@ describe('correlation edge cases', () => { const invalidTransport1 = null as any; const invalidTransport2 = 'string-transport' as any; const requestId = 'test-request-6'; - + // Store span for first invalid transport storeSpanForRequest(invalidTransport1, requestId, mockSpan, 'tools/call'); - + // Complete span from different invalid transport with same request ID // This should work because they both use the fallback map expect(() => { @@ -99,7 +96,7 @@ describe('correlation edge cases', () => { it('works normally with valid transport objects', () => { const transport = createMockTransport(); const requestId = 'test-request-7'; - + // Should work normally with valid transport expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -107,7 +104,7 @@ describe('correlation edge cases', () => { content: [{ type: 'text', text: 'result' }], }); }).not.toThrow(); - + expect(mockSpan.end).toHaveBeenCalled(); }); @@ -115,19 +112,19 @@ describe('correlation edge cases', () => { const transport1 = createMockTransport(); const transport2 = createMockTransport(); const requestId = 'test-request-8'; - + const mockSpan1 = { ...mockSpan, id: 'span1', end: vi.fn() }; const mockSpan2 = { ...mockSpan, id: 'span2', end: vi.fn() }; - + // Store spans for different transports with same request ID storeSpanForRequest(transport1, requestId, mockSpan1, 'tools/call'); storeSpanForRequest(transport2, requestId, mockSpan2, 'tools/call'); - + // Complete span for transport1 should only affect span1 completeSpanFromToolResult(transport1, requestId, { content: [{ type: 'text', text: 'result1' }], }); - + expect(mockSpan1.end).toHaveBeenCalled(); expect(mockSpan2.end).not.toHaveBeenCalled(); }); @@ -136,27 +133,27 @@ describe('correlation edge cases', () => { const validTransport = createMockTransport(); const invalidTransport = null as any; const requestId = 'test-request-9'; - + const mockSpan1 = { ...mockSpan, id: 'valid-span', end: vi.fn() }; const mockSpan2 = { ...mockSpan, id: 'fallback-span', end: vi.fn() }; - + // Store spans for both valid and invalid transports storeSpanForRequest(validTransport, requestId, mockSpan1, 'tools/call'); storeSpanForRequest(invalidTransport, requestId, mockSpan2, 'tools/call'); - + // Complete span for valid transport should only affect valid span completeSpanFromToolResult(validTransport, requestId, { content: [{ type: 'text', text: 'valid-result' }], }); - + expect(mockSpan1.end).toHaveBeenCalled(); expect(mockSpan2.end).not.toHaveBeenCalled(); - + // Complete span for invalid transport should only affect fallback span completeSpanFromToolResult(invalidTransport, requestId, { content: [{ type: 'text', text: 'fallback-result' }], }); - + expect(mockSpan2.end).toHaveBeenCalled(); }); }); @@ -165,7 +162,7 @@ describe('correlation edge cases', () => { it('handles transport with null prototype', () => { const transport = Object.create(null) as MCPTransport; const requestId = 'test-request-10'; - + // Should not throw with null prototype object expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -175,7 +172,7 @@ describe('correlation edge cases', () => { it('handles frozen transport object', () => { const transport = Object.freeze(createMockTransport()); const requestId = 'test-request-11'; - + // Should work with frozen object expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -183,7 +180,7 @@ describe('correlation edge cases', () => { content: [{ type: 'text', text: 'result' }], }); }).not.toThrow(); - + expect(mockSpan.end).toHaveBeenCalled(); }); @@ -191,7 +188,7 @@ describe('correlation edge cases', () => { const transport = createMockTransport() as any; transport.self = transport; // Create circular reference const requestId = 'test-request-12'; - + // Should work with circular references expect(() => { storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); @@ -199,8 +196,8 @@ describe('correlation edge cases', () => { content: [{ type: 'text', text: 'result' }], }); }).not.toThrow(); - + expect(mockSpan.end).toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); From a4f7b8ca6b4ff96f6896c0ae349fbc93b859dcda Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Sat, 2 Aug 2025 09:41:53 -0700 Subject: [PATCH 03/16] refactor(mcp-server): Remove patch references from baseline implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The defensive programming patterns are now the correct baseline implementation, not patches to be applied. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../core/src/integrations/mcp-server/attributeExtraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 9f65b2241e31..5d2da773c467 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -269,7 +269,7 @@ export function buildTransportAttributes( transport: MCPTransport, extra?: ExtraHandlerData, ): Record { - // PATCHED: Gracefully handle undefined sessionId during MCP initialization + // Gracefully handle undefined sessionId during MCP initialization // Respects client-provided sessions and waits for proper session establishment const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined; From ff7d1afe3c81d3e8b4156e7beadb2a15d00cd93d Mon Sep 17 00:00:00 2001 From: betegon Date: Sun, 3 Aug 2025 20:37:17 +0200 Subject: [PATCH 04/16] get custom transport names --- .../mcp-server/attributeExtraction.ts | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 5d2da773c467..a74c90d0a77b 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -45,25 +45,16 @@ import type { * @returns Transport type mapping for span attributes */ export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } { - // Handle undefined transport gracefully while preserving type detection if (!transport?.constructor) { return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } - const transportName = transport.constructor.name?.toLowerCase() || ''; + const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; + const networkTransport = transportName === 'stdio' ? 'pipe' : 'tcp'; - 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' }; + return { + mcpTransport: transportName, + networkTransport, + }; } /** @@ -264,17 +255,13 @@ export function extractClientInfo(extra: ExtraHandlerData): { * @param transport - MCP transport instance * @param extra - Optional extra handler data * @returns Transport attributes for span instrumentation + * @note sessionId may be undefined during initial setup - session should be established by client during initialize flow */ export function buildTransportAttributes( transport: MCPTransport, extra?: ExtraHandlerData, ): Record { - // Gracefully handle undefined sessionId during MCP initialization - // Respects client-provided sessions and waits for proper session establishment const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined; - - // Note: sessionId may be undefined during initial setup - this is expected behavior - // The actual session should be established by the client during the initialize flow const clientInfo = extra ? extractClientInfo(extra) : {}; const { mcpTransport, networkTransport } = getTransportTypes(transport); const clientAttributes = getClientAttributes(transport); From 00cd668892f59c5fc21bd65fb1756bc9c68bfa85 Mon Sep 17 00:00:00 2001 From: betegon Date: Sun, 3 Aug 2025 23:17:13 +0200 Subject: [PATCH 05/16] refactor(mcp-server): Remove fallback span map and simplify span handling --- .../integrations/mcp-server/correlation.ts | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts index bb857c363473..9bbb061e551a 100644 --- a/packages/core/src/integrations/mcp-server/correlation.ts +++ b/packages/core/src/integrations/mcp-server/correlation.ts @@ -19,12 +19,6 @@ import type { MCPTransport, RequestId, RequestSpanMapValue } from './types'; */ const transportToSpanMap = new WeakMap>(); -/** - * Fallback span map for invalid transport objects - * @internal Used when transport objects cannot be used as WeakMap keys - */ -const fallbackSpanMap = new Map(); - /** * Gets or creates the span map for a specific transport instance * @internal @@ -32,12 +26,6 @@ const fallbackSpanMap = new Map(); * @returns Span map for the transport */ function getOrCreateSpanMap(transport: MCPTransport): Map { - // Handle invalid transport values for WeakMap while preserving correlation - if (!transport || typeof transport !== 'object') { - // Return persistent fallback Map to maintain correlation across calls - return fallbackSpanMap; - } - let spanMap = transportToSpanMap.get(transport); if (!spanMap) { spanMap = new Map(); @@ -55,6 +43,7 @@ function getOrCreateSpanMap(transport: MCPTransport): Map Date: Sun, 3 Aug 2025 23:30:03 +0200 Subject: [PATCH 06/16] set network transport to pipe if MCP transport name includes "stdio" --- .../core/src/integrations/mcp-server/attributeExtraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index a74c90d0a77b..fcbecb46dd3c 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -49,7 +49,7 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; - const networkTransport = transportName === 'stdio' ? 'pipe' : 'tcp'; + const networkTransport = transportName.includes('stdio') ? 'pipe' : 'tcp'; return { mcpTransport: transportName, From fb40d64c0f3d986cb4e0b32ab425660d85aa5cd3 Mon Sep 17 00:00:00 2001 From: betegon Date: Sun, 3 Aug 2025 23:39:20 +0200 Subject: [PATCH 07/16] refactor tests to use complete transport name and check transport edge cases --- .../mcp-server/attributeExtraction.test.ts | 181 ---------------- .../mcp-server/correlation.test.ts | 203 ------------------ .../mcp-server/semanticConventions.test.ts | 12 +- .../transportInstrumentation.test.ts | 68 +++++- 4 files changed, 70 insertions(+), 394 deletions(-) delete mode 100644 packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts delete mode 100644 packages/core/test/lib/integrations/mcp-server/correlation.test.ts diff --git a/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts b/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts deleted file mode 100644 index 85d11a804a02..000000000000 --- a/packages/core/test/lib/integrations/mcp-server/attributeExtraction.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - buildTransportAttributes, - getTransportTypes, -} from '../../../../src/integrations/mcp-server/attributeExtraction'; -import type { MCPTransport } from '../../../../src/integrations/mcp-server/types'; - -describe('attributeExtraction edge cases', () => { - describe('getTransportTypes', () => { - it('handles undefined transport gracefully', () => { - const result = getTransportTypes(undefined as any); - expect(result).toEqual({ - mcpTransport: 'unknown', - networkTransport: 'unknown', - }); - }); - - it('handles null transport gracefully', () => { - const result = getTransportTypes(null as any); - expect(result).toEqual({ - mcpTransport: 'unknown', - networkTransport: 'unknown', - }); - }); - - it('handles transport with null constructor', () => { - const transport = { - constructor: null, - } as any; - - const result = getTransportTypes(transport); - expect(result).toEqual({ - mcpTransport: 'unknown', - networkTransport: 'unknown', - }); - }); - - it('handles transport with undefined constructor', () => { - const transport = { - constructor: undefined, - } as any; - - const result = getTransportTypes(transport); - expect(result).toEqual({ - mcpTransport: 'unknown', - networkTransport: 'unknown', - }); - }); - - it('correctly identifies StreamableHTTPServerTransport', () => { - class StreamableHTTPServerTransport { - sessionId = 'test-session'; - } - - const transport = new StreamableHTTPServerTransport() as MCPTransport; - const result = getTransportTypes(transport); - - expect(result).toEqual({ - mcpTransport: 'http', - networkTransport: 'tcp', - }); - }); - - it('correctly identifies SSE transport', () => { - class SSEServerTransport { - sessionId = 'sse-session'; - } - - const transport = new SSEServerTransport() as MCPTransport; - const result = getTransportTypes(transport); - - expect(result).toEqual({ - mcpTransport: 'sse', - networkTransport: 'tcp', - }); - }); - - it('correctly identifies stdio transport', () => { - class StdioServerTransport { - sessionId = 'stdio-session'; - } - - const transport = new StdioServerTransport() as MCPTransport; - const result = getTransportTypes(transport); - - expect(result).toEqual({ - mcpTransport: 'stdio', - networkTransport: 'pipe', - }); - }); - }); - - describe('buildTransportAttributes', () => { - it('handles undefined sessionId gracefully', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - // No sessionId property - } as MCPTransport; - - const attributes = buildTransportAttributes(transport); - - // Should not include sessionId in attributes when undefined - expect(attributes['mcp.session.id']).toBeUndefined(); - expect(attributes['mcp.transport']).toBe('http'); - }); - - it('handles transport without sessionId property', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - // sessionId property doesn't exist at all - } as any; - - const attributes = buildTransportAttributes(transport); - - // Should not include sessionId in attributes - expect(attributes['mcp.session.id']).toBeUndefined(); - expect(attributes['mcp.transport']).toBe('http'); - }); - - it('includes sessionId when properly set', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - sessionId: 'test-session-123', - } as MCPTransport; - - const attributes = buildTransportAttributes(transport); - - expect(attributes['mcp.session.id']).toBe('test-session-123'); - expect(attributes['mcp.transport']).toBe('http'); - }); - - it('handles null sessionId gracefully', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - sessionId: null, - } as any; - - const attributes = buildTransportAttributes(transport); - - // Should not include null sessionId in attributes - expect(attributes['mcp.session.id']).toBeUndefined(); - expect(attributes['mcp.transport']).toBe('http'); - }); - - it('handles empty string sessionId', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - sessionId: '', - } as MCPTransport; - - const attributes = buildTransportAttributes(transport); - - // Empty string is falsy, so should not be included - expect(attributes['mcp.session.id']).toBeUndefined(); - expect(attributes['mcp.transport']).toBe('http'); - }); - - it('preserves all other attributes when sessionId is undefined', () => { - const transport = { - constructor: { name: 'StreamableHTTPServerTransport' }, - // No sessionId - } as MCPTransport; - - const attributes = buildTransportAttributes(transport, { - clientAddress: '127.0.0.1', - clientPort: 8080, - }); - - expect(attributes).toMatchObject({ - 'mcp.transport': 'http', - 'network.transport': 'tcp', - 'network.protocol.version': '2.0', - 'client.address': '127.0.0.1', - 'client.port': 8080, - }); - - // sessionId should not be present - expect(attributes['mcp.session.id']).toBeUndefined(); - }); - }); -}); diff --git a/packages/core/test/lib/integrations/mcp-server/correlation.test.ts b/packages/core/test/lib/integrations/mcp-server/correlation.test.ts deleted file mode 100644 index 31114bfcfc51..000000000000 --- a/packages/core/test/lib/integrations/mcp-server/correlation.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getCurrentScope } from '../../../../src/currentScopes'; -import { completeSpanFromToolResult, storeSpanForRequest } from '../../../../src/integrations/mcp-server/correlation'; -import type { MCPTransport } from '../../../../src/integrations/mcp-server/types'; -import { createMockTransport } from './testUtils'; - -// Mock getCurrentScope -vi.mock('../../../../src/currentScopes', () => ({ - getCurrentScope: vi.fn(), -})); - -describe('correlation edge cases', () => { - let mockSpan: any; - - beforeEach(() => { - mockSpan = { - setStatus: vi.fn(), - setData: vi.fn(), - end: vi.fn(), - isRecording: vi.fn().mockReturnValue(true), - }; - - (getCurrentScope as any).mockReturnValue({ - getSpan: vi.fn().mockReturnValue(mockSpan), - }); - }); - - describe('WeakMap correlation fallback', () => { - it('handles null transport gracefully', () => { - const transport = null as any; - const requestId = 'test-request-1'; - - // Should not throw when storing span for null transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('handles undefined transport gracefully', () => { - const transport = undefined as any; - const requestId = 'test-request-2'; - - // Should not throw when storing span for undefined transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('handles non-object transport gracefully', () => { - const transport = 'not-an-object' as any; - const requestId = 'test-request-3'; - - // Should not throw when storing span for string transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('handles number transport gracefully', () => { - const transport = 123 as any; - const requestId = 'test-request-4'; - - // Should not throw when storing span for number transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('handles boolean transport gracefully', () => { - const transport = true as any; - const requestId = 'test-request-5'; - - // Should not throw when storing span for boolean transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('uses fallback map for invalid transports but maintains correlation', () => { - const invalidTransport1 = null as any; - const invalidTransport2 = 'string-transport' as any; - const requestId = 'test-request-6'; - - // Store span for first invalid transport - storeSpanForRequest(invalidTransport1, requestId, mockSpan, 'tools/call'); - - // Complete span from different invalid transport with same request ID - // This should work because they both use the fallback map - expect(() => { - completeSpanFromToolResult(invalidTransport2, requestId, { - content: [{ type: 'text', text: 'result' }], - }); - }).not.toThrow(); - }); - - it('works normally with valid transport objects', () => { - const transport = createMockTransport(); - const requestId = 'test-request-7'; - - // Should work normally with valid transport - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - completeSpanFromToolResult(transport, requestId, { - content: [{ type: 'text', text: 'result' }], - }); - }).not.toThrow(); - - expect(mockSpan.end).toHaveBeenCalled(); - }); - - it('maintains separate correlation for valid transports', () => { - const transport1 = createMockTransport(); - const transport2 = createMockTransport(); - const requestId = 'test-request-8'; - - const mockSpan1 = { ...mockSpan, id: 'span1', end: vi.fn() }; - const mockSpan2 = { ...mockSpan, id: 'span2', end: vi.fn() }; - - // Store spans for different transports with same request ID - storeSpanForRequest(transport1, requestId, mockSpan1, 'tools/call'); - storeSpanForRequest(transport2, requestId, mockSpan2, 'tools/call'); - - // Complete span for transport1 should only affect span1 - completeSpanFromToolResult(transport1, requestId, { - content: [{ type: 'text', text: 'result1' }], - }); - - expect(mockSpan1.end).toHaveBeenCalled(); - expect(mockSpan2.end).not.toHaveBeenCalled(); - }); - - it('isolates fallback map from valid transport maps', () => { - const validTransport = createMockTransport(); - const invalidTransport = null as any; - const requestId = 'test-request-9'; - - const mockSpan1 = { ...mockSpan, id: 'valid-span', end: vi.fn() }; - const mockSpan2 = { ...mockSpan, id: 'fallback-span', end: vi.fn() }; - - // Store spans for both valid and invalid transports - storeSpanForRequest(validTransport, requestId, mockSpan1, 'tools/call'); - storeSpanForRequest(invalidTransport, requestId, mockSpan2, 'tools/call'); - - // Complete span for valid transport should only affect valid span - completeSpanFromToolResult(validTransport, requestId, { - content: [{ type: 'text', text: 'valid-result' }], - }); - - expect(mockSpan1.end).toHaveBeenCalled(); - expect(mockSpan2.end).not.toHaveBeenCalled(); - - // Complete span for invalid transport should only affect fallback span - completeSpanFromToolResult(invalidTransport, requestId, { - content: [{ type: 'text', text: 'fallback-result' }], - }); - - expect(mockSpan2.end).toHaveBeenCalled(); - }); - }); - - describe('edge case transport objects', () => { - it('handles transport with null prototype', () => { - const transport = Object.create(null) as MCPTransport; - const requestId = 'test-request-10'; - - // Should not throw with null prototype object - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - }).not.toThrow(); - }); - - it('handles frozen transport object', () => { - const transport = Object.freeze(createMockTransport()); - const requestId = 'test-request-11'; - - // Should work with frozen object - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - completeSpanFromToolResult(transport, requestId, { - content: [{ type: 'text', text: 'result' }], - }); - }).not.toThrow(); - - expect(mockSpan.end).toHaveBeenCalled(); - }); - - it('handles transport with circular references', () => { - const transport = createMockTransport() as any; - transport.self = transport; // Create circular reference - const requestId = 'test-request-12'; - - // Should work with circular references - expect(() => { - storeSpanForRequest(transport, requestId, mockSpan, 'tools/call'); - completeSpanFromToolResult(transport, requestId, { - content: [{ type: 'text', text: 'result' }], - }); - }).not.toThrow(); - - expect(mockSpan.end).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts index 7b110a0b2756..46cc234f21f3 100644 --- a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts @@ -61,7 +61,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.session.id': 'test-session-123', 'client.address': '192.168.1.100', 'client.port': 54321, - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.location': '"Seattle, WA"', @@ -93,7 +93,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.resource.uri': 'file:///docs/api.md', 'mcp.request.id': 'req-2', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.uri': '"file:///docs/api.md"', @@ -125,7 +125,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.prompt.name': 'analyze-code', 'mcp.request.id': 'req-3', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.name': '"analyze-code"', @@ -154,7 +154,7 @@ describe('MCP Server Semantic Conventions', () => { attributes: { 'mcp.method.name': 'notifications/tools/list_changed', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'sentry.op': 'mcp.notification.client_to_server', @@ -193,7 +193,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.request.id': 'req-4', 'mcp.session.id': 'test-session-123', // Transport attributes - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', // Sentry-specific @@ -227,7 +227,7 @@ describe('MCP Server Semantic Conventions', () => { attributes: { 'mcp.method.name': 'notifications/message', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.logging.level': 'info', diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index 7f06eb886cdb..c1885bdbc8d9 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -2,8 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as currentScopes from '../../../../src/currentScopes'; import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server'; import { + buildTransportAttributes, extractSessionDataFromInitializeRequest, extractSessionDataFromInitializeResponse, + getTransportTypes, } from '../../../../src/integrations/mcp-server/attributeExtraction'; import { cleanupSessionDataForTransport, @@ -214,7 +216,7 @@ describe('MCP Server Transport Instrumentation', () => { 'mcp.tool.name': 'process-file', 'mcp.request.id': 'req-stdio-1', 'mcp.session.id': 'stdio-session-456', - 'mcp.transport': 'stdio', // Should be stdio, not http + 'mcp.transport': 'stdioservertransport', 'network.transport': 'pipe', // Should be pipe, not tcp 'network.protocol.version': '2.0', 'mcp.request.argument.path': '"/tmp/data.txt"', @@ -245,7 +247,7 @@ describe('MCP Server Transport Instrumentation', () => { attributes: expect.objectContaining({ 'mcp.method.name': 'notifications/message', 'mcp.session.id': 'stdio-session-456', - 'mcp.transport': 'stdio', + 'mcp.transport': 'stdioservertransport', 'network.transport': 'pipe', 'mcp.logging.level': 'debug', 'mcp.logging.message': 'Processing stdin input', @@ -286,7 +288,7 @@ describe('MCP Server Transport Instrumentation', () => { attributes: expect.objectContaining({ 'mcp.method.name': 'resources/read', 'mcp.resource.uri': 'https://api.example.com/data', - 'mcp.transport': 'sse', // Deprecated but supported + 'mcp.transport': 'sseservertransport', 'network.transport': 'tcp', 'mcp.session.id': 'sse-session-789', }), @@ -361,7 +363,7 @@ describe('MCP Server Transport Instrumentation', () => { 'mcp.session.id': 'test-session-direct', 'client.address': '127.0.0.1', 'client.port': 8080, - 'mcp.transport': 'http', + 'mcp.transport': 'streamablehttpservertransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.input': '"test"', @@ -500,4 +502,62 @@ describe('MCP Server Transport Instrumentation', () => { expect(getSessionDataForTransport(transportWithoutSession)).toBeUndefined(); }); }); + + describe('Transport Type Detection', () => { + it('extracts HTTP transport name correctly', () => { + const transport = createMockTransport(); + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('streamablehttpservertransport'); + expect(result.networkTransport).toBe('tcp'); + }); + + it('extracts stdio transport and maps to pipe network', () => { + const transport = createMockStdioTransport(); + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('stdioservertransport'); + expect(result.networkTransport).toBe('pipe'); + }); + + it('extracts SSE transport name', () => { + const transport = createMockSseTransport(); + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('sseservertransport'); + expect(result.networkTransport).toBe('tcp'); + }); + + it('handles transport without constructor', () => { + const transport = Object.create(null); + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('unknown'); + expect(result.networkTransport).toBe('unknown'); + }); + }); + + describe('buildTransportAttributes sessionId handling', () => { + it('includes sessionId when present', () => { + const transport = createMockTransport(); + const attributes = buildTransportAttributes(transport); + + expect(attributes['mcp.session.id']).toBe('test-session-123'); + }); + + it('excludes sessionId when undefined', () => { + const transport = createMockTransport(); + transport.sessionId = undefined; + const attributes = buildTransportAttributes(transport); + + expect(attributes['mcp.session.id']).toBeUndefined(); + }); + + it('excludes sessionId when not present in transport', () => { + const transport = { onmessage: () => {}, send: async () => {} }; + const attributes = buildTransportAttributes(transport); + + expect(attributes['mcp.session.id']).toBeUndefined(); + }); + }); }); From 9fcece14adb370982cfc76ce0e46277359e4b410 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 09:42:28 +0200 Subject: [PATCH 08/16] remove redundant optional chaining --- packages/core/src/integrations/mcp-server/correlation.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts index 9bbb061e551a..424edd766d64 100644 --- a/packages/core/src/integrations/mcp-server/correlation.ts +++ b/packages/core/src/integrations/mcp-server/correlation.ts @@ -43,7 +43,6 @@ function getOrCreateSpanMap(transport: MCPTransport): Map Date: Mon, 4 Aug 2025 09:55:06 +0200 Subject: [PATCH 09/16] set mcp transport and network transport to unknown if transport name is unknown --- .../mcp-server/attributeExtraction.ts | 5 ++++ .../transportInstrumentation.test.ts | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index fcbecb46dd3c..6dbb99137251 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -49,6 +49,11 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; + + if (transportName === 'unknown') { + return { mcpTransport: 'unknown', networkTransport: 'unknown' }; + } + const networkTransport = transportName.includes('stdio') ? 'pipe' : 'tcp'; return { diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index c1885bdbc8d9..d54cb9a05bc9 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -535,6 +535,30 @@ describe('MCP Server Transport Instrumentation', () => { expect(result.mcpTransport).toBe('unknown'); expect(result.networkTransport).toBe('unknown'); }); + + it('handles transport with null/undefined constructor name', () => { + const transport = { + constructor: { name: null }, + onmessage: () => {}, + send: async () => {}, + }; + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('unknown'); + expect(result.networkTransport).toBe('unknown'); + }); + + it('handles transport with empty constructor name', () => { + const transport = { + constructor: { name: '' }, + onmessage: () => {}, + send: async () => {}, + }; + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('unknown'); + expect(result.networkTransport).toBe('unknown'); + }); }); describe('buildTransportAttributes sessionId handling', () => { From b5903526c35cfc24a9cd44df03141974a033d572 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 10:06:50 +0200 Subject: [PATCH 10/16] set network transport to unknown in case it's not http / sse / stdio --- .../integrations/mcp-server/attributeExtraction.ts | 8 +++++++- .../mcp-server/transportInstrumentation.test.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index 6dbb99137251..b9ade58adeac 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -54,7 +54,13 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } - const networkTransport = transportName.includes('stdio') ? 'pipe' : 'tcp'; + let networkTransport = 'unknown'; + + if (transportName.includes('stdio')) { + networkTransport = 'pipe'; + } else if (transportName.includes('http') || transportName.includes('sse')) { + networkTransport = 'tcp'; + } return { mcpTransport: transportName, diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index d54cb9a05bc9..94bb9e61ae7a 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -559,6 +559,18 @@ describe('MCP Server Transport Instrumentation', () => { expect(result.mcpTransport).toBe('unknown'); expect(result.networkTransport).toBe('unknown'); }); + + it('returns unknown network transport for unrecognized transport types', () => { + const transport = { + constructor: { name: 'CustomTransport' }, + onmessage: () => {}, + send: async () => {}, + }; + const result = getTransportTypes(transport); + + expect(result.mcpTransport).toBe('customtransport'); + expect(result.networkTransport).toBe('unknown'); + }); }); describe('buildTransportAttributes sessionId handling', () => { From 46b69076178ed7eaa97c8e42e2a252b5ca467cb4 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 11:05:20 +0200 Subject: [PATCH 11/16] fix unknown transport --- .../core/src/integrations/mcp-server/attributeExtraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index b9ade58adeac..ce42eef02672 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -50,7 +50,7 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri } const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; - if (transportName === 'unknown') { + if (!transportName) { return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } From ba7435fdf5ab65f05acf17d1a57634a664048c34 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 11:15:36 +0200 Subject: [PATCH 12/16] remove dead code --- .../core/src/integrations/mcp-server/attributeExtraction.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index ce42eef02672..ac046d3cbe99 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -50,10 +50,6 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri } const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; - if (!transportName) { - return { mcpTransport: 'unknown', networkTransport: 'unknown' }; - } - let networkTransport = 'unknown'; if (transportName.includes('stdio')) { From ede7dafdba5953fa0be9e1194df5d2cd25a54fb2 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 11:32:07 +0200 Subject: [PATCH 13/16] guard from constructor name not being a string --- .../core/src/integrations/mcp-server/attributeExtraction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index ac046d3cbe99..f5e47e74ec64 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -48,8 +48,8 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri if (!transport?.constructor) { return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } - const transportName = transport.constructor.name?.toLowerCase() || 'unknown'; - + const transportName = + typeof transport.constructor?.name === 'string' ? transport.constructor.name.toLowerCase() : 'unknown'; let networkTransport = 'unknown'; if (transportName.includes('stdio')) { From b0bf0b98869ac0b8677cf0960dc08eb5f4234505 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 4 Aug 2025 16:28:54 +0200 Subject: [PATCH 14/16] remove legay test --- .../mcp-server/transportInstrumentation.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index 94bb9e61ae7a..9ecfdee4a0fb 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -548,18 +548,6 @@ describe('MCP Server Transport Instrumentation', () => { expect(result.networkTransport).toBe('unknown'); }); - it('handles transport with empty constructor name', () => { - const transport = { - constructor: { name: '' }, - onmessage: () => {}, - send: async () => {}, - }; - const result = getTransportTypes(transport); - - expect(result.mcpTransport).toBe('unknown'); - expect(result.networkTransport).toBe('unknown'); - }); - it('returns unknown network transport for unrecognized transport types', () => { const transport = { constructor: { name: 'CustomTransport' }, From bbfd2a47510f796a25574d85a35fad43e06e1275 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 6 Aug 2025 11:20:18 +0200 Subject: [PATCH 15/16] remove lowercasing for MCP transport name --- .../mcp-server/attributeExtraction.ts | 8 ++++---- .../mcp-server/semanticConventions.test.ts | 12 ++++++------ .../mcp-server/transportInstrumentation.test.ts | 16 ++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/attributeExtraction.ts b/packages/core/src/integrations/mcp-server/attributeExtraction.ts index f5e47e74ec64..adefbcab1de3 100644 --- a/packages/core/src/integrations/mcp-server/attributeExtraction.ts +++ b/packages/core/src/integrations/mcp-server/attributeExtraction.ts @@ -48,13 +48,13 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri if (!transport?.constructor) { return { mcpTransport: 'unknown', networkTransport: 'unknown' }; } - const transportName = - typeof transport.constructor?.name === 'string' ? transport.constructor.name.toLowerCase() : 'unknown'; + const transportName = typeof transport.constructor?.name === 'string' ? transport.constructor.name : 'unknown'; let networkTransport = 'unknown'; - if (transportName.includes('stdio')) { + const lowerTransportName = transportName.toLowerCase(); + if (lowerTransportName.includes('stdio')) { networkTransport = 'pipe'; - } else if (transportName.includes('http') || transportName.includes('sse')) { + } else if (lowerTransportName.includes('http') || lowerTransportName.includes('sse')) { networkTransport = 'tcp'; } diff --git a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts index 46cc234f21f3..6bdefdb7dbf4 100644 --- a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts @@ -61,7 +61,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.session.id': 'test-session-123', 'client.address': '192.168.1.100', 'client.port': 54321, - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.location': '"Seattle, WA"', @@ -93,7 +93,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.resource.uri': 'file:///docs/api.md', 'mcp.request.id': 'req-2', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.uri': '"file:///docs/api.md"', @@ -125,7 +125,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.prompt.name': 'analyze-code', 'mcp.request.id': 'req-3', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.name': '"analyze-code"', @@ -154,7 +154,7 @@ describe('MCP Server Semantic Conventions', () => { attributes: { 'mcp.method.name': 'notifications/tools/list_changed', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'sentry.op': 'mcp.notification.client_to_server', @@ -193,7 +193,7 @@ describe('MCP Server Semantic Conventions', () => { 'mcp.request.id': 'req-4', 'mcp.session.id': 'test-session-123', // Transport attributes - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', // Sentry-specific @@ -227,7 +227,7 @@ describe('MCP Server Semantic Conventions', () => { attributes: { 'mcp.method.name': 'notifications/message', 'mcp.session.id': 'test-session-123', - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.logging.level': 'info', diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts index 9ecfdee4a0fb..cbc41b069e8f 100644 --- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts @@ -216,7 +216,7 @@ describe('MCP Server Transport Instrumentation', () => { 'mcp.tool.name': 'process-file', 'mcp.request.id': 'req-stdio-1', 'mcp.session.id': 'stdio-session-456', - 'mcp.transport': 'stdioservertransport', + 'mcp.transport': 'StdioServerTransport', 'network.transport': 'pipe', // Should be pipe, not tcp 'network.protocol.version': '2.0', 'mcp.request.argument.path': '"/tmp/data.txt"', @@ -247,7 +247,7 @@ describe('MCP Server Transport Instrumentation', () => { attributes: expect.objectContaining({ 'mcp.method.name': 'notifications/message', 'mcp.session.id': 'stdio-session-456', - 'mcp.transport': 'stdioservertransport', + 'mcp.transport': 'StdioServerTransport', 'network.transport': 'pipe', 'mcp.logging.level': 'debug', 'mcp.logging.message': 'Processing stdin input', @@ -288,7 +288,7 @@ describe('MCP Server Transport Instrumentation', () => { attributes: expect.objectContaining({ 'mcp.method.name': 'resources/read', 'mcp.resource.uri': 'https://api.example.com/data', - 'mcp.transport': 'sseservertransport', + 'mcp.transport': 'SSEServerTransport', 'network.transport': 'tcp', 'mcp.session.id': 'sse-session-789', }), @@ -363,7 +363,7 @@ describe('MCP Server Transport Instrumentation', () => { 'mcp.session.id': 'test-session-direct', 'client.address': '127.0.0.1', 'client.port': 8080, - 'mcp.transport': 'streamablehttpservertransport', + 'mcp.transport': 'StreamableHTTPServerTransport', 'network.transport': 'tcp', 'network.protocol.version': '2.0', 'mcp.request.argument.input': '"test"', @@ -508,7 +508,7 @@ describe('MCP Server Transport Instrumentation', () => { const transport = createMockTransport(); const result = getTransportTypes(transport); - expect(result.mcpTransport).toBe('streamablehttpservertransport'); + expect(result.mcpTransport).toBe('StreamableHTTPServerTransport'); expect(result.networkTransport).toBe('tcp'); }); @@ -516,7 +516,7 @@ describe('MCP Server Transport Instrumentation', () => { const transport = createMockStdioTransport(); const result = getTransportTypes(transport); - expect(result.mcpTransport).toBe('stdioservertransport'); + expect(result.mcpTransport).toBe('StdioServerTransport'); expect(result.networkTransport).toBe('pipe'); }); @@ -524,7 +524,7 @@ describe('MCP Server Transport Instrumentation', () => { const transport = createMockSseTransport(); const result = getTransportTypes(transport); - expect(result.mcpTransport).toBe('sseservertransport'); + expect(result.mcpTransport).toBe('SSEServerTransport'); expect(result.networkTransport).toBe('tcp'); }); @@ -556,7 +556,7 @@ describe('MCP Server Transport Instrumentation', () => { }; const result = getTransportTypes(transport); - expect(result.mcpTransport).toBe('customtransport'); + expect(result.mcpTransport).toBe('CustomTransport'); expect(result.networkTransport).toBe('unknown'); }); }); From c121658a2a99e99da0dfc7787d16c7901804d70b Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 12 Aug 2025 13:54:17 +0200 Subject: [PATCH 16/16] session id guard --- packages/core/src/integrations/mcp-server/sessionExtraction.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/integrations/mcp-server/sessionExtraction.ts b/packages/core/src/integrations/mcp-server/sessionExtraction.ts index 4b1808ad2c73..62eaa94f9b71 100644 --- a/packages/core/src/integrations/mcp-server/sessionExtraction.ts +++ b/packages/core/src/integrations/mcp-server/sessionExtraction.ts @@ -176,12 +176,13 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri * @param transport - MCP transport instance * @param extra - Optional extra handler data * @returns Transport attributes for span instrumentation + * @note sessionId may be undefined during initial setup - session should be established by client during initialize flow */ export function buildTransportAttributes( transport: MCPTransport, extra?: ExtraHandlerData, ): Record { - const sessionId = transport.sessionId; + const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined; const clientInfo = extra ? extractClientInfo(extra) : {}; const { mcpTransport, networkTransport } = getTransportTypes(transport); const clientAttributes = getClientAttributes(transport);