Skip to content

feat(core): MCP server instrumentation without breaking Miniflare #16817

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 34 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dff2080
feat(mcp-server): Enhance transport handling and request instrumentation
betegon Jun 27, 2025
cb28ccc
test transport layer
betegon Jul 1, 2025
1480b78
refactor(mcp-server.test): Simplify test setup by using beforeEach fo…
betegon Jul 1, 2025
5a97d69
test(mcp-server): Add tests for span creation and semantic convention…
betegon Jul 1, 2025
03077f8
test(mcp-server): Update tests to control transport connection in ind…
betegon Jul 2, 2025
cac9bd0
test(mcp-server): Refine span attributes and transport details
betegon Jul 2, 2025
37ef9a9
test(mcp-server): Replace direct tracing module calls with spies for …
betegon Jul 2, 2025
ac015ce
feat(mcp-server): Add TypeScript type definitions for MCP server inst…
betegon Jul 2, 2025
c2f3e82
feat(mcp-server): Introduce MCP attributes and methods for enhanced t…
betegon Jul 2, 2025
094574f
test(mcp-server): Add tests for span creation with various notificati…
betegon Jul 2, 2025
9972b09
test(mcp-server): Update test to use spy for startSpan
betegon Jul 2, 2025
ef52da5
refactor(mcp-server): improve span handling and attribute extraction
betegon Jul 2, 2025
aee709b
simplify attributes
betegon Jul 2, 2025
edc4e3c
refactor(mcp-server): improve types
betegon Jul 2, 2025
62ca0f3
refactor(mcp-server): refactor span handling and utility functions fo…
betegon Jul 2, 2025
08c39f1
remove unused import and comment legacy support
betegon Jul 3, 2025
fe2c865
refactor(mcp-server): improve notification span handling and set attr…
betegon Jul 3, 2025
ec3cb6f
refactor(mcp-server): span and attribute creation
betegon Jul 3, 2025
e193118
refactor(mcp-server): method configuration and argument extraction fo…
betegon Jul 3, 2025
347422c
refactor(mcp-server): improve transport type handling and add tests f…
betegon Jul 3, 2025
02cb799
Merge branch 'develop' into bete/mcp-server-semantic-convention
betegon Jul 3, 2025
9776402
fix lint
betegon Jul 3, 2025
d4c74a9
refactor(mcp-server): use fill for method wrapping for transport hand…
betegon Jul 4, 2025
25297f6
Merge branch 'develop' into bete/mcp-server-semantic-convention-fill
betegon Jul 4, 2025
7b5fd86
move files to intregations directory
betegon Jul 8, 2025
97990c6
use parseStringToURLObject for url handling and update test
betegon Jul 8, 2025
1efda74
(draft) fix span duration
betegon Jul 8, 2025
aa0a9fd
Add tool call results and MCP spans duration to cover their children …
betegon Jul 9, 2025
4973a60
Implement MCP server attribute extraction and correlation system. New…
betegon Jul 10, 2025
de1b87f
Refactor MCP server integration. improve attribute extraction and spa…
betegon Jul 10, 2025
7624a3e
fix lint
betegon Jul 10, 2025
d94a4d0
prettier
betegon Jul 10, 2025
6c67f51
Implement PII filtering for MCP server spans. Introduce a new utility…
betegon Jul 10, 2025
b305410
Merge branch 'develop' into bete/mcp-server-semantic-convention-fill
betegon Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integra
export { profiler } from './profiling';
export { instrumentFetchRequest } from './fetch';
export { trpcMiddleware } from './trpc';
export { wrapMcpServerWithSentry } from './mcp-server';
export { wrapMcpServerWithSentry } from './integrations/mcp-server';
export { captureFeedback } from './feedback';
export type { ReportDialogOptions } from './report-dialog';
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports';
Expand Down
365 changes: 365 additions & 0 deletions packages/core/src/integrations/mcp-server/attributeExtraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
/**
* Attribute extraction and building functions for MCP server instrumentation
*/

import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url';
import {
CLIENT_ADDRESS_ATTRIBUTE,
CLIENT_PORT_ATTRIBUTE,
MCP_PROMPT_NAME_ATTRIBUTE,
MCP_REQUEST_ID_ATTRIBUTE,
MCP_RESOURCE_URI_ATTRIBUTE,
MCP_SESSION_ID_ATTRIBUTE,
MCP_TOOL_NAME_ATTRIBUTE,
MCP_TRANSPORT_ATTRIBUTE,
NETWORK_PROTOCOL_VERSION_ATTRIBUTE,
NETWORK_TRANSPORT_ATTRIBUTE,
} from './attributes';
import type {
ExtraHandlerData,
JsonRpcNotification,
JsonRpcRequest,
McpSpanType,
MCPTransport,
MethodConfig,
} from './types';

/** Configuration for MCP methods to extract targets and arguments */
const METHOD_CONFIGS: Record<string, MethodConfig> = {
'tools/call': {
targetField: 'name',
targetAttribute: MCP_TOOL_NAME_ATTRIBUTE,
captureArguments: true,
argumentsField: 'arguments',
},
'resources/read': {
targetField: 'uri',
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
captureUri: true,
},
'resources/subscribe': {
targetField: 'uri',
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
},
'resources/unsubscribe': {
targetField: 'uri',
targetAttribute: MCP_RESOURCE_URI_ATTRIBUTE,
},
'prompts/get': {
targetField: 'name',
targetAttribute: MCP_PROMPT_NAME_ATTRIBUTE,
captureName: true,
captureArguments: true,
argumentsField: 'arguments',
},
};

/** Extracts target info from method and params based on method type */
export function extractTargetInfo(
method: string,
params: Record<string, unknown>,
): {
target?: string;
attributes: Record<string, string>;
} {
const config = METHOD_CONFIGS[method as keyof typeof METHOD_CONFIGS];
if (!config) {
return { attributes: {} };
}

const target =
config.targetField && typeof params?.[config.targetField] === 'string'
? (params[config.targetField] as string)
: undefined;

return {
target,
attributes: target && config.targetAttribute ? { [config.targetAttribute]: target } : {},
};
}

/** Extracts request arguments based on method type */
export function getRequestArguments(method: string, params: Record<string, unknown>): Record<string, string> {
const args: Record<string, string> = {};
const config = METHOD_CONFIGS[method as keyof typeof METHOD_CONFIGS];

if (!config) {
return args;
}

// Capture arguments from the configured field
if (config.captureArguments && config.argumentsField && params?.[config.argumentsField]) {
const argumentsObj = params[config.argumentsField];
if (typeof argumentsObj === 'object' && argumentsObj !== null) {
for (const [key, value] of Object.entries(argumentsObj as Record<string, unknown>)) {
args[`mcp.request.argument.${key.toLowerCase()}`] = JSON.stringify(value);
}
}
}

// Capture specific fields as arguments
if (config.captureUri && params?.uri) {
args['mcp.request.argument.uri'] = JSON.stringify(params.uri);
}

if (config.captureName && params?.name) {
args['mcp.request.argument.name'] = JSON.stringify(params.name);
}

return args;
}

/** Extracts transport types based on transport constructor name */
export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } {
const transportName = transport.constructor?.name?.toLowerCase() || '';

// Standard MCP transports per specification
if (transportName.includes('stdio')) {
return { mcpTransport: 'stdio', networkTransport: 'pipe' };
}

// Streamable HTTP is the standard HTTP-based transport
if (transportName.includes('streamablehttp') || transportName.includes('streamable')) {
return { mcpTransport: 'http', networkTransport: 'tcp' };
}

// SSE is deprecated (backwards compatibility)
if (transportName.includes('sse')) {
return { mcpTransport: 'sse', networkTransport: 'tcp' };
}

// For custom transports, mark as unknown
return { mcpTransport: 'unknown', networkTransport: 'unknown' };
}

/** Extracts additional attributes for specific notification types */
export function getNotificationAttributes(
method: string,
params: Record<string, unknown>,
): Record<string, string | number> {
const attributes: Record<string, string | number> = {};

switch (method) {
case 'notifications/cancelled':
if (params?.requestId) {
attributes['mcp.cancelled.request_id'] = String(params.requestId);
}
if (params?.reason) {
attributes['mcp.cancelled.reason'] = String(params.reason);
}
break;

case 'notifications/message':
if (params?.level) {
attributes['mcp.logging.level'] = String(params.level);
}
if (params?.logger) {
attributes['mcp.logging.logger'] = String(params.logger);
}
if (params?.data !== undefined) {
attributes['mcp.logging.data_type'] = typeof params.data;
// Store the actual message content
if (typeof params.data === 'string') {
attributes['mcp.logging.message'] = params.data;
} else {
attributes['mcp.logging.message'] = JSON.stringify(params.data);
}
}
break;

case 'notifications/progress':
if (params?.progressToken) {
attributes['mcp.progress.token'] = String(params.progressToken);
}
if (typeof params?.progress === 'number') {
attributes['mcp.progress.current'] = params.progress;
}
if (typeof params?.total === 'number') {
attributes['mcp.progress.total'] = params.total;
if (typeof params?.progress === 'number') {
attributes['mcp.progress.percentage'] = (params.progress / params.total) * 100;
}
}
if (params?.message) {
attributes['mcp.progress.message'] = String(params.message);
}
break;

case 'notifications/resources/updated':
if (params?.uri) {
attributes['mcp.resource.uri'] = String(params.uri);
// Extract protocol from URI
const urlObject = parseStringToURLObject(String(params.uri));
if (urlObject && !isURLObjectRelative(urlObject)) {
attributes['mcp.resource.protocol'] = urlObject.protocol.replace(':', '');
}
}
break;

case 'notifications/initialized':
attributes['mcp.lifecycle.phase'] = 'initialization_complete';
attributes['mcp.protocol.ready'] = 1;
break;
}

return attributes;
}

/** Extracts client connection info from extra handler data */
export function extractClientInfo(extra: ExtraHandlerData): {
address?: string;
port?: number;
} {
return {
address:
extra?.requestInfo?.remoteAddress ||
extra?.clientAddress ||
extra?.request?.ip ||
extra?.request?.connection?.remoteAddress,
port: extra?.requestInfo?.remotePort || extra?.clientPort || extra?.request?.connection?.remotePort,
};
}

/** Build transport and network attributes */
export function buildTransportAttributes(
transport: MCPTransport,
extra?: ExtraHandlerData,
): Record<string, string | number> {
const sessionId = transport.sessionId;
const clientInfo = extra ? extractClientInfo(extra) : {};
const { mcpTransport, networkTransport } = getTransportTypes(transport);

return {
...(sessionId && { [MCP_SESSION_ID_ATTRIBUTE]: sessionId }),
...(clientInfo.address && { [CLIENT_ADDRESS_ATTRIBUTE]: clientInfo.address }),
...(clientInfo.port && { [CLIENT_PORT_ATTRIBUTE]: clientInfo.port }),
[MCP_TRANSPORT_ATTRIBUTE]: mcpTransport,
[NETWORK_TRANSPORT_ATTRIBUTE]: networkTransport,
[NETWORK_PROTOCOL_VERSION_ATTRIBUTE]: '2.0',
};
}

/** Build type-specific attributes based on message type */
export function buildTypeSpecificAttributes(
type: McpSpanType,
message: JsonRpcRequest | JsonRpcNotification,
params?: Record<string, unknown>,
): Record<string, string | number> {
if (type === 'request') {
const request = message as JsonRpcRequest;
const targetInfo = extractTargetInfo(request.method, params || {});

return {
...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }),
...targetInfo.attributes,
...getRequestArguments(request.method, params || {}),
};
}

// For notifications, only include notification-specific attributes
return getNotificationAttributes(message.method, params || {});
}

/** Get metadata about tool result content array */
function getContentMetadata(content: unknown[]): Record<string, string | number> {
return {
'mcp.tool.result.content_count': content.length,
};
}

/** Build attributes from a single content item */
function buildContentItemAttributes(
contentItem: Record<string, unknown>,
prefix: string,
): Record<string, string | number> {
const attributes: Record<string, string | number> = {};

if (typeof contentItem.type === 'string') {
attributes[`${prefix}.content_type`] = contentItem.type;
}

if (typeof contentItem.text === 'string') {
const text = contentItem.text;
attributes[`${prefix}.content`] = text.length > 500 ? `${text.substring(0, 497)}...` : text;
}

if (typeof contentItem.mimeType === 'string') {
attributes[`${prefix}.mime_type`] = contentItem.mimeType;
}

if (typeof contentItem.uri === 'string') {
attributes[`${prefix}.uri`] = contentItem.uri;
}

if (typeof contentItem.name === 'string') {
attributes[`${prefix}.name`] = contentItem.name;
}

if (typeof contentItem.data === 'string') {
attributes[`${prefix}.data_size`] = contentItem.data.length;
}

return attributes;
}

/** Build attributes from embedded resource object */
function buildEmbeddedResourceAttributes(resource: Record<string, unknown>, prefix: string): Record<string, string> {
const attributes: Record<string, string> = {};

if (typeof resource.uri === 'string') {
attributes[`${prefix}.resource_uri`] = resource.uri;
}

if (typeof resource.mimeType === 'string') {
attributes[`${prefix}.resource_mime_type`] = resource.mimeType;
}

return attributes;
}

/** Build attributes for all content items in the result */
function buildAllContentItemAttributes(content: unknown[]): Record<string, string | number> {
const attributes: Record<string, string | number> = {};

for (let i = 0; i < content.length; i++) {
const item = content[i];
if (item && typeof item === 'object' && item !== null) {
const contentItem = item as Record<string, unknown>;
const prefix = content.length === 1 ? 'mcp.tool.result' : `mcp.tool.result.${i}`;

Object.assign(attributes, buildContentItemAttributes(contentItem, prefix));

if (contentItem.resource && typeof contentItem.resource === 'object') {
const resourceAttrs = buildEmbeddedResourceAttributes(contentItem.resource as Record<string, unknown>, prefix);
Object.assign(attributes, resourceAttrs);
}
}
}

return attributes;
}

/** Extract tool result attributes for span instrumentation */
export function extractToolResultAttributes(result: unknown): Record<string, string | number | boolean> {
let attributes: Record<string, string | number | boolean> = {};

if (typeof result !== 'object' || result === null) {
return attributes;
}

const resultObj = result as Record<string, unknown>;

if (typeof resultObj.isError === 'boolean') {
attributes['mcp.tool.result.is_error'] = resultObj.isError;
}

if (Array.isArray(resultObj.content)) {
attributes = {
...attributes,
...getContentMetadata(resultObj.content),
...buildAllContentItemAttributes(resultObj.content),
};
}

return attributes;
}
Loading
Loading