Skip to content

Commit 497a0b5

Browse files
authored
feat(core): Add recordInputs/recordOutputs options to MCP server wrapper (#18600)
Adds granular control over capturing tool/prompt inputs and outputs in MCP server instrumentation. **NOT a breaking change:** both options default to `sendDefaultPii`, so existing behavior is preserved. ## Motivation Previously, capturing MCP tool/prompt inputs and outputs required enabling `sendDefaultPii: true`, which also enables capturing IP addresses, user data, and other sensitive information. Bbut MCP inputs/outputs are important and user will want to record them, so it shouldn't require exposing all PII. This change decouples input/output capture from the broader PII setting, giving users granular control: - Want inputs/outputs for debugging but not IP addresses? → `recordInputs: true` + `sendDefaultPii: false` - Want full PII including network info? → `sendDefaultPii: true` (same as before) ## New Options for `wrapMcpServerWithSentry` - `recordInputs`: Controls whether tool/prompt input arguments are captured in spans - `recordOutputs`: Controls whether tool/prompt output results are captured in spans ## Usage ```typescript const mcpServer = new McpServer({name: "my-server"}) // Default: inherits from sendDefaultPii const server = wrapMcpServerWithSentry(mcpServer); // Explicit control const server = wrapMcpServerWithSentry( mcpServer, { recordInputs: true, recordOutputs: false } ); ``` ### PII Simplification - `piiFiltering.ts` now only handles network PII (`client.address`, `client.port`, `mcp.resource.uri`) - Input/output capture is controlled at the source via `recordInputs`/`recordOutputs` rather than filtering after capture ## Files Changed - `types.ts` - Added `McpServerWrapperOptions` type - `index.ts` - Added options parameter with `sendDefaultPii` defaults - `attributeExtraction.ts` - Conditional input argument capture - `spans.ts` - Threading `recordInputs` through span creation - `transport.ts` - Passing options to transport wrappers - `correlation.ts` - Conditional output result capture - `piiFiltering.ts` - Simplified to network PII only - Tests updated accordingly Closes #18603 (added automatically)
1 parent 8d946bd commit 497a0b5

File tree

11 files changed

+386
-256
lines changed

11 files changed

+386
-256
lines changed

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,25 @@ import {
1414
import { extractTargetInfo, getRequestArguments } from './methodConfig';
1515
import type { JsonRpcNotification, JsonRpcRequest, McpSpanType } from './types';
1616

17+
/**
18+
* Formats logging data for span attributes
19+
* @internal
20+
*/
21+
function formatLoggingData(data: unknown): string {
22+
return typeof data === 'string' ? data : JSON.stringify(data);
23+
}
24+
1725
/**
1826
* Extracts additional attributes for specific notification types
1927
* @param method - Notification method name
2028
* @param params - Notification parameters
29+
* @param recordInputs - Whether to include actual content or just metadata
2130
* @returns Method-specific attributes for span instrumentation
2231
*/
2332
export function getNotificationAttributes(
2433
method: string,
2534
params: Record<string, unknown>,
35+
recordInputs?: boolean,
2636
): Record<string, string | number> {
2737
const attributes: Record<string, string | number> = {};
2838

@@ -45,10 +55,8 @@ export function getNotificationAttributes(
4555
}
4656
if (params?.data !== undefined) {
4757
attributes[MCP_LOGGING_DATA_TYPE_ATTRIBUTE] = typeof params.data;
48-
if (typeof params.data === 'string') {
49-
attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = params.data;
50-
} else {
51-
attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = JSON.stringify(params.data);
58+
if (recordInputs) {
59+
attributes[MCP_LOGGING_MESSAGE_ATTRIBUTE] = formatLoggingData(params.data);
5260
}
5361
}
5462
break;
@@ -95,12 +103,14 @@ export function getNotificationAttributes(
95103
* @param type - Span type (request or notification)
96104
* @param message - JSON-RPC message
97105
* @param params - Optional parameters for attribute extraction
106+
* @param recordInputs - Whether to capture input arguments in spans
98107
* @returns Type-specific attributes for span instrumentation
99108
*/
100109
export function buildTypeSpecificAttributes(
101110
type: McpSpanType,
102111
message: JsonRpcRequest | JsonRpcNotification,
103112
params?: Record<string, unknown>,
113+
recordInputs?: boolean,
104114
): Record<string, string | number> {
105115
if (type === 'request') {
106116
const request = message as JsonRpcRequest;
@@ -109,11 +119,11 @@ export function buildTypeSpecificAttributes(
109119
return {
110120
...(request.id !== undefined && { [MCP_REQUEST_ID_ATTRIBUTE]: String(request.id) }),
111121
...targetInfo.attributes,
112-
...getRequestArguments(request.method, params || {}),
122+
...(recordInputs ? getRequestArguments(request.method, params || {}) : {}),
113123
};
114124
}
115125

116-
return getNotificationAttributes(message.method, params || {});
126+
return getNotificationAttributes(message.method, params || {}, recordInputs);
117127
}
118128

119129
// Re-export buildTransportAttributes for spans.ts

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
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';
16-
import type { MCPTransport, RequestId, RequestSpanMapValue } from './types';
14+
import type { MCPTransport, RequestId, RequestSpanMapValue, ResolvedMcpOptions } from './types';
1715

1816
/**
1917
* Transport-scoped correlation system that prevents collisions between different MCP sessions
@@ -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 options - Resolved MCP options
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+
options: ResolvedMcpOptions,
65+
): void {
6266
const spanMap = getOrCreateSpanMap(transport);
6367
const spanData = spanMap.get(requestId);
6468
if (spanData) {
@@ -77,18 +81,10 @@ export function completeSpanWithResults(transport: MCPTransport, requestId: Requ
7781

7882
span.setAttributes(initAttributes);
7983
} 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-
84+
const toolAttributes = extractToolResultAttributes(result, options.recordOutputs);
8585
span.setAttributes(toolAttributes);
8686
} 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-
87+
const promptAttributes = extractPromptResultAttributes(result, options.recordOutputs);
9288
span.setAttributes(promptAttributes);
9389
}
9490

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { getClient } from '../../currentScopes';
12
import { fill } from '../../utils/object';
23
import { wrapAllMCPHandlers } from './handlers';
34
import { wrapTransportError, wrapTransportOnClose, wrapTransportOnMessage, wrapTransportSend } from './transport';
4-
import type { MCPServerInstance, MCPTransport } from './types';
5+
import type { MCPServerInstance, McpServerWrapperOptions, MCPTransport, ResolvedMcpOptions } from './types';
56
import { validateMcpServerInstance } from './validation';
67

78
/**
@@ -22,18 +23,26 @@ const wrappedMcpServerInstances = new WeakSet();
2223
* import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2324
* import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2425
*
26+
* // Default: inputs/outputs captured based on sendDefaultPii option
2527
* const server = Sentry.wrapMcpServerWithSentry(
2628
* new McpServer({ name: "my-server", version: "1.0.0" })
2729
* );
2830
*
31+
* // Explicitly control input/output capture
32+
* const server = Sentry.wrapMcpServerWithSentry(
33+
* new McpServer({ name: "my-server", version: "1.0.0" }),
34+
* { recordInputs: true, recordOutputs: false }
35+
* );
36+
*
2937
* const transport = new StreamableHTTPServerTransport();
3038
* await server.connect(transport);
3139
* ```
3240
*
3341
* @param mcpServerInstance - MCP server instance to instrument
42+
* @param options - Optional configuration for recording inputs and outputs
3443
* @returns Instrumented server instance (same reference)
3544
*/
36-
export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S): S {
45+
export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S, options?: McpServerWrapperOptions): S {
3746
if (wrappedMcpServerInstances.has(mcpServerInstance)) {
3847
return mcpServerInstance;
3948
}
@@ -43,6 +52,13 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
4352
}
4453

4554
const serverInstance = mcpServerInstance as MCPServerInstance;
55+
const client = getClient();
56+
const sendDefaultPii = Boolean(client?.getOptions().sendDefaultPii);
57+
58+
const resolvedOptions: ResolvedMcpOptions = {
59+
recordInputs: options?.recordInputs ?? sendDefaultPii,
60+
recordOutputs: options?.recordOutputs ?? sendDefaultPii,
61+
};
4662

4763
fill(serverInstance, 'connect', originalConnect => {
4864
return async function (this: MCPServerInstance, transport: MCPTransport, ...restArgs: unknown[]) {
@@ -52,8 +68,8 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
5268
...restArgs,
5369
);
5470

55-
wrapTransportOnMessage(transport);
56-
wrapTransportSend(transport);
71+
wrapTransportOnMessage(transport, resolvedOptions);
72+
wrapTransportSend(transport, resolvedOptions);
5773
wrapTransportOnClose(transport);
5874
wrapTransportError(transport);
5975

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;

0 commit comments

Comments
 (0)