Skip to content

Commit 001555d

Browse files
committed
feat(mcp-server): Add options to capture input and output arguments in spans
This introduces `recordInputs` and `recordOutputs` options to the MCP server wrapper and related functions, allowing for more granular control over the data captured in spans.
1 parent d1dd308 commit 001555d

7 files changed

Lines changed: 70 additions & 77 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@ export function getNotificationAttributes(
9595
* @param type - Span type (request or notification)
9696
* @param message - JSON-RPC message
9797
* @param params - Optional parameters for attribute extraction
98+
* @param recordInputs - Whether to capture input arguments in spans
9899
* @returns Type-specific attributes for span instrumentation
99100
*/
100101
export function buildTypeSpecificAttributes(
101102
type: McpSpanType,
102103
message: JsonRpcRequest | JsonRpcNotification,
103104
params?: Record<string, unknown>,
105+
recordInputs?: boolean,
104106
): Record<string, string | number> {
105107
if (type === 'request') {
106108
const request = message as JsonRpcRequest;
@@ -109,7 +111,7 @@ export function buildTypeSpecificAttributes(
109111
return {
110112
...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }),
111113
...targetInfo.attributes,
112-
...getRequestArguments(request.method, params || {}),
114+
...(recordInputs ? getRequestArguments(request.method, params || {}) : {}),
113115
};
114116
}
115117

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

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
* request ID collisions between different MCP sessions.
77
*/
88

9-
import { getClient } from '../../currentScopes';
109
import { SPAN_STATUS_ERROR } from '../../tracing';
1110
import type { Span } from '../../types-hoist/span';
1211
import { MCP_PROTOCOL_VERSION_ATTRIBUTE } from './attributes';
13-
import { filterMcpPiiFromSpanData } from './piiFiltering';
1412
import { extractPromptResultAttributes, extractToolResultAttributes } from './resultExtraction';
1513
import { buildServerAttributesFromInfo, extractSessionDataFromInitializeResponse } from './sessionExtraction';
1614
import type { MCPTransport, RequestId, RequestSpanMapValue } from './types';
@@ -57,8 +55,14 @@ export function storeSpanForRequest(transport: MCPTransport, requestId: RequestI
5755
* @param transport - MCP transport instance
5856
* @param requestId - Request identifier
5957
* @param result - Execution result for attribute extraction
58+
* @param recordOutputs - Whether to capture output results in spans
6059
*/
61-
export function completeSpanWithResults(transport: MCPTransport, requestId: RequestId, result: unknown): void {
60+
export function completeSpanWithResults(
61+
transport: MCPTransport,
62+
requestId: RequestId,
63+
result: unknown,
64+
recordOutputs: boolean,
65+
): void {
6266
const spanMap = getOrCreateSpanMap(transport);
6367
const spanData = spanMap.get(requestId);
6468
if (spanData) {
@@ -76,19 +80,11 @@ export function completeSpanWithResults(transport: MCPTransport, requestId: Requ
7680
}
7781

7882
span.setAttributes(initAttributes);
79-
} else if (method === 'tools/call') {
80-
const rawToolAttributes = extractToolResultAttributes(result);
81-
const client = getClient();
82-
const sendDefaultPii = Boolean(client?.getOptions().sendDefaultPii);
83-
const toolAttributes = filterMcpPiiFromSpanData(rawToolAttributes, sendDefaultPii);
84-
83+
} else if (method === 'tools/call' && recordOutputs) {
84+
const toolAttributes = extractToolResultAttributes(result);
8585
span.setAttributes(toolAttributes);
86-
} else if (method === 'prompts/get') {
87-
const rawPromptAttributes = extractPromptResultAttributes(result);
88-
const client = getClient();
89-
const sendDefaultPii = Boolean(client?.getOptions().sendDefaultPii);
90-
const promptAttributes = filterMcpPiiFromSpanData(rawPromptAttributes, sendDefaultPii);
91-
86+
} else if (method === 'prompts/get' && recordOutputs) {
87+
const promptAttributes = extractPromptResultAttributes(result);
9288
span.setAttributes(promptAttributes);
9389
}
9490

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fill } from '../../utils/object';
22
import { wrapAllMCPHandlers } from './handlers';
33
import { wrapTransportError, wrapTransportOnClose, wrapTransportOnMessage, wrapTransportSend } from './transport';
4-
import type { MCPServerInstance, MCPTransport } from './types';
4+
import type { MCPServerInstance, McpServerWrapperOptions, MCPTransport } from './types';
55
import { validateMcpServerInstance } from './validation';
66

77
/**
@@ -22,18 +22,26 @@ const wrappedMcpServerInstances = new WeakSet();
2222
* import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2323
* import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2424
*
25+
* // Default: inputs and outputs are NOT captured
2526
* const server = Sentry.wrapMcpServerWithSentry(
2627
* new McpServer({ name: "my-server", version: "1.0.0" })
2728
* );
2829
*
30+
* // Capture both inputs and outputs
31+
* const server = Sentry.wrapMcpServerWithSentry(
32+
* new McpServer({ name: "my-server", version: "1.0.0" }),
33+
* { recordInputs: true, recordOutputs: true }
34+
* );
35+
*
2936
* const transport = new StreamableHTTPServerTransport();
3037
* await server.connect(transport);
3138
* ```
3239
*
3340
* @param mcpServerInstance - MCP server instance to instrument
41+
* @param options - Optional configuration for recording inputs and outputs
3442
* @returns Instrumented server instance (same reference)
3543
*/
36-
export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S): S {
44+
export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S, options?: McpServerWrapperOptions): S {
3745
if (wrappedMcpServerInstances.has(mcpServerInstance)) {
3846
return mcpServerInstance;
3947
}
@@ -43,6 +51,8 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
4351
}
4452

4553
const serverInstance = mcpServerInstance as MCPServerInstance;
54+
const recordInputs = options?.recordInputs ?? false;
55+
const recordOutputs = options?.recordOutputs ?? false;
4656

4757
fill(serverInstance, 'connect', originalConnect => {
4858
return async function (this: MCPServerInstance, transport: MCPTransport, ...restArgs: unknown[]) {
@@ -52,8 +62,8 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
5262
...restArgs,
5363
);
5464

55-
wrapTransportOnMessage(transport);
56-
wrapTransportSend(transport);
65+
wrapTransportOnMessage(transport, recordInputs);
66+
wrapTransportSend(transport, recordOutputs);
5767
wrapTransportOnClose(transport);
5868
wrapTransportError(transport);
5969

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

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,37 @@
11
/**
22
* PII filtering for MCP server spans
33
*
4-
* Removes sensitive data when sendDefaultPii is false.
5-
* Uses configurable attribute filtering to protect user privacy.
4+
* Removes network-level sensitive data when sendDefaultPii is false.
5+
* Input/output data (request arguments, tool/prompt results) is controlled
6+
* separately via recordInputs/recordOutputs options.
67
*/
78
import type { SpanAttributeValue } from '../../types-hoist/span';
8-
import {
9-
CLIENT_ADDRESS_ATTRIBUTE,
10-
CLIENT_PORT_ATTRIBUTE,
11-
MCP_LOGGING_MESSAGE_ATTRIBUTE,
12-
MCP_PROMPT_RESULT_DESCRIPTION_ATTRIBUTE,
13-
MCP_PROMPT_RESULT_MESSAGE_CONTENT_ATTRIBUTE,
14-
MCP_PROMPT_RESULT_PREFIX,
15-
MCP_REQUEST_ARGUMENT,
16-
MCP_RESOURCE_URI_ATTRIBUTE,
17-
MCP_TOOL_RESULT_CONTENT_ATTRIBUTE,
18-
MCP_TOOL_RESULT_PREFIX,
19-
} from './attributes';
9+
import { CLIENT_ADDRESS_ATTRIBUTE, CLIENT_PORT_ATTRIBUTE, MCP_RESOURCE_URI_ATTRIBUTE } from './attributes';
2010

2111
/**
22-
* PII attributes that should be removed when sendDefaultPii is false
12+
* Network PII attributes that should be removed when sendDefaultPii is false
2313
* @internal
2414
*/
25-
const PII_ATTRIBUTES = new Set([
26-
CLIENT_ADDRESS_ATTRIBUTE,
27-
CLIENT_PORT_ATTRIBUTE,
28-
MCP_LOGGING_MESSAGE_ATTRIBUTE,
29-
MCP_PROMPT_RESULT_DESCRIPTION_ATTRIBUTE,
30-
MCP_PROMPT_RESULT_MESSAGE_CONTENT_ATTRIBUTE,
31-
MCP_RESOURCE_URI_ATTRIBUTE,
32-
MCP_TOOL_RESULT_CONTENT_ATTRIBUTE,
33-
]);
15+
const NETWORK_PII_ATTRIBUTES = new Set([CLIENT_ADDRESS_ATTRIBUTE, CLIENT_PORT_ATTRIBUTE, MCP_RESOURCE_URI_ATTRIBUTE]);
3416

3517
/**
36-
* Checks if an attribute key should be considered PII.
18+
* Checks if an attribute key should be considered network PII.
3719
*
3820
* Returns true for:
39-
* - Explicit PII attributes (client.address, client.port, mcp.logging.message, etc.)
40-
* - All request arguments (mcp.request.argument.*)
41-
* - Tool and prompt result content (mcp.tool.result.*, mcp.prompt.result.*) except metadata
42-
*
43-
* Preserves metadata attributes ending with _count, _error, or .is_error as they don't contain sensitive data.
21+
* - client.address (IP address)
22+
* - client.port (port number)
23+
* - mcp.resource.uri (potentially sensitive URIs)
4424
*
4525
* @param key - Attribute key to evaluate
46-
* @returns true if the attribute should be filtered out (is PII), false if it should be preserved
26+
* @returns true if the attribute should be filtered out (is network PII), false if it should be preserved
4727
* @internal
4828
*/
49-
function isPiiAttribute(key: string): boolean {
50-
if (PII_ATTRIBUTES.has(key)) {
51-
return true;
52-
}
53-
54-
if (key.startsWith(`${MCP_REQUEST_ARGUMENT}.`)) {
55-
return true;
56-
}
57-
58-
if (key.startsWith(`${MCP_TOOL_RESULT_PREFIX}.`) || key.startsWith(`${MCP_PROMPT_RESULT_PREFIX}.`)) {
59-
if (!key.endsWith('_count') && !key.endsWith('_error') && !key.endsWith('.is_error')) {
60-
return true;
61-
}
62-
}
63-
64-
return false;
29+
function isNetworkPiiAttribute(key: string): boolean {
30+
return NETWORK_PII_ATTRIBUTES.has(key);
6531
}
6632

6733
/**
68-
* Removes PII attributes from span data when sendDefaultPii is false
34+
* Removes network PII attributes from span data when sendDefaultPii is false
6935
* @param spanData - Raw span attributes
7036
* @param sendDefaultPii - Whether to include PII data
7137
* @returns Filtered span attributes
@@ -80,7 +46,7 @@ export function filterMcpPiiFromSpanData(
8046

8147
return Object.entries(spanData).reduce(
8248
(acc, [key, value]) => {
83-
if (!isPiiAttribute(key)) {
49+
if (!isNetworkPiiAttribute(key)) {
8450
acc[key] = value as SpanAttributeValue;
8551
}
8652
return acc;

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function buildSentryAttributes(type: McpSpanConfig['type']): Record<string, stri
7676
* @returns Created span
7777
*/
7878
function createMcpSpan(config: McpSpanConfig): unknown {
79-
const { type, message, transport, extra, callback } = config;
79+
const { type, message, transport, extra, callback, recordInputs } = config;
8080
const { method } = message;
8181
const params = message.params;
8282

@@ -93,7 +93,7 @@ function createMcpSpan(config: McpSpanConfig): unknown {
9393
const rawAttributes: Record<string, string | number> = {
9494
...buildTransportAttributes(transport, extra),
9595
[MCP_METHOD_NAME_ATTRIBUTE]: method,
96-
...buildTypeSpecificAttributes(type, message, params),
96+
...buildTypeSpecificAttributes(type, message, params, recordInputs),
9797
...buildSentryAttributes(type),
9898
};
9999

@@ -116,13 +116,15 @@ function createMcpSpan(config: McpSpanConfig): unknown {
116116
* @param jsonRpcMessage - Notification message
117117
* @param transport - MCP transport instance
118118
* @param extra - Extra handler data
119+
* @param recordInputs - Whether to capture input arguments in spans
119120
* @param callback - Span execution callback
120121
* @returns Span execution result
121122
*/
122123
export function createMcpNotificationSpan(
123124
jsonRpcMessage: JsonRpcNotification,
124125
transport: MCPTransport,
125126
extra: ExtraHandlerData,
127+
recordInputs: boolean,
126128
callback: () => unknown,
127129
): unknown {
128130
return createMcpSpan({
@@ -131,6 +133,7 @@ export function createMcpNotificationSpan(
131133
transport,
132134
extra,
133135
callback,
136+
recordInputs,
134137
});
135138
}
136139

@@ -159,12 +162,14 @@ export function createMcpOutgoingNotificationSpan(
159162
* @param jsonRpcMessage - Request message
160163
* @param transport - MCP transport instance
161164
* @param extra - Optional extra handler data
165+
* @param recordInputs - Whether to capture input arguments in spans
162166
* @returns Span configuration object
163167
*/
164168
export function buildMcpServerSpanConfig(
165169
jsonRpcMessage: JsonRpcRequest,
166170
transport: MCPTransport,
167171
extra?: ExtraHandlerData,
172+
recordInputs?: boolean,
168173
): {
169174
name: string;
170175
op: string;
@@ -180,7 +185,7 @@ export function buildMcpServerSpanConfig(
180185
const rawAttributes: Record<string, string | number> = {
181186
...buildTransportAttributes(transport, extra),
182187
[MCP_METHOD_NAME_ATTRIBUTE]: method,
183-
...buildTypeSpecificAttributes('request', jsonRpcMessage, params),
188+
...buildTypeSpecificAttributes('request', jsonRpcMessage, params, recordInputs),
184189
...buildSentryAttributes('request'),
185190
};
186191

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, isValidCont
3030
* For "initialize" requests, extracts and stores client info and protocol version
3131
* in the session data for the transport.
3232
* @param transport - MCP transport instance to wrap
33+
* @param recordInputs - Whether to capture input arguments in spans
3334
*/
34-
export function wrapTransportOnMessage(transport: MCPTransport): void {
35+
export function wrapTransportOnMessage(transport: MCPTransport, recordInputs: boolean): void {
3536
if (transport.onmessage) {
3637
fill(transport, 'onmessage', originalOnMessage => {
3738
return function (this: MCPTransport, message: unknown, extra?: unknown) {
@@ -51,7 +52,7 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
5152
const isolationScope = getIsolationScope().clone();
5253

5354
return withIsolationScope(isolationScope, () => {
54-
const spanConfig = buildMcpServerSpanConfig(message, this, extra as ExtraHandlerData);
55+
const spanConfig = buildMcpServerSpanConfig(message, this, extra as ExtraHandlerData, recordInputs);
5556
const span = startInactiveSpan(spanConfig);
5657

5758
// For initialize requests, add client info directly to span (works even for stateless transports)
@@ -73,7 +74,7 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
7374
}
7475

7576
if (isJsonRpcNotification(message)) {
76-
return createMcpNotificationSpan(message, this, extra as ExtraHandlerData, () => {
77+
return createMcpNotificationSpan(message, this, extra as ExtraHandlerData, recordInputs, () => {
7778
return (originalOnMessage as (...args: unknown[]) => unknown).call(this, message, extra);
7879
});
7980
}
@@ -89,8 +90,9 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
8990
* For "initialize" responses, extracts and stores protocol version and server info
9091
* in the session data for the transport.
9192
* @param transport - MCP transport instance to wrap
93+
* @param recordOutputs - Whether to capture output results in spans
9294
*/
93-
export function wrapTransportSend(transport: MCPTransport): void {
95+
export function wrapTransportSend(transport: MCPTransport, recordOutputs: boolean): void {
9496
if (transport.send) {
9597
fill(transport, 'send', originalSend => {
9698
return async function (this: MCPTransport, ...args: unknown[]) {
@@ -119,7 +121,7 @@ export function wrapTransportSend(transport: MCPTransport): void {
119121
}
120122
}
121123

122-
completeSpanWithResults(this, message.id, message.result);
124+
completeSpanWithResults(this, message.id, message.result, recordOutputs);
123125
}
124126
}
125127

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface McpSpanConfig {
127127
transport: MCPTransport;
128128
extra?: ExtraHandlerData;
129129
callback: () => unknown;
130+
recordInputs?: boolean;
130131
}
131132

132133
export type SessionId = string;
@@ -183,3 +184,14 @@ export type SessionData = {
183184
protocolVersion?: string;
184185
serverInfo?: PartyInfo;
185186
};
187+
188+
/**
189+
* Options for configuring the MCP server wrapper.
190+
* @internal
191+
*/
192+
export type McpServerWrapperOptions = {
193+
/** Whether to capture tool/prompt input arguments in spans. @default false */
194+
recordInputs?: boolean;
195+
/** Whether to capture tool/prompt output results in spans. @default false */
196+
recordOutputs?: boolean;
197+
};

0 commit comments

Comments
 (0)