@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
268 lines (233 loc) • 9.7 kB
JavaScript
import { DEBUG_BUILD } from './debug-build.js';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP } from './semanticAttributes.js';
import './tracing/errors.js';
import './utils-hoist/debug-build.js';
import { logger } from './utils-hoist/logger.js';
import './utils-hoist/time.js';
import { getActiveSpan } from './utils/spanUtils.js';
import { withActiveSpan, startSpan } from './tracing/trace.js';
const wrappedMcpServerInstances = new WeakSet();
/**
* Wraps a MCP Server instance from the `@modelcontextprotocol/sdk` package with Sentry instrumentation.
*
* Compatible with versions `^1.9.0` of the `@modelcontextprotocol/sdk` package.
*/
// We are exposing this API for non-node runtimes that cannot rely on auto-instrumentation.
function wrapMcpServerWithSentry(mcpServerInstance) {
if (wrappedMcpServerInstances.has(mcpServerInstance)) {
return mcpServerInstance;
}
if (!isMcpServerInstance(mcpServerInstance)) {
DEBUG_BUILD && logger.warn('Did not patch MCP server. Interface is incompatible.');
return mcpServerInstance;
}
// eslint-disable-next-line @typescript-eslint/unbound-method
mcpServerInstance.connect = new Proxy(mcpServerInstance.connect, {
apply(target, thisArg, argArray) {
const [transport, ...restArgs] = argArray ;
if (!transport.onclose) {
transport.onclose = () => {
if (transport.sessionId) {
handleTransportOnClose(transport.sessionId);
}
};
}
if (!transport.onmessage) {
transport.onmessage = jsonRpcMessage => {
if (transport.sessionId && isJsonRPCMessageWithRequestId(jsonRpcMessage)) {
handleTransportOnMessage(transport.sessionId, jsonRpcMessage.id);
}
};
}
const patchedTransport = new Proxy(transport, {
set(target, key, value) {
if (key === 'onmessage') {
target[key] = new Proxy(value, {
apply(onMessageTarget, onMessageThisArg, onMessageArgArray) {
const [jsonRpcMessage] = onMessageArgArray;
if (transport.sessionId && isJsonRPCMessageWithRequestId(jsonRpcMessage)) {
handleTransportOnMessage(transport.sessionId, jsonRpcMessage.id);
}
return Reflect.apply(onMessageTarget, onMessageThisArg, onMessageArgArray);
},
});
} else if (key === 'onclose') {
target[key] = new Proxy(value, {
apply(onCloseTarget, onCloseThisArg, onCloseArgArray) {
if (transport.sessionId) {
handleTransportOnClose(transport.sessionId);
}
return Reflect.apply(onCloseTarget, onCloseThisArg, onCloseArgArray);
},
});
} else {
target[key ] = value;
}
return true;
},
});
return Reflect.apply(target, thisArg, [patchedTransport, ...restArgs]);
},
});
mcpServerInstance.resource = new Proxy(mcpServerInstance.resource, {
apply(target, thisArg, argArray) {
const resourceName = argArray[0];
const resourceHandler = argArray[argArray.length - 1];
if (typeof resourceName !== 'string' || typeof resourceHandler !== 'function') {
return target.apply(thisArg, argArray);
}
const wrappedResourceHandler = new Proxy(resourceHandler, {
apply(resourceHandlerTarget, resourceHandlerThisArg, resourceHandlerArgArray) {
const extraHandlerDataWithRequestId = resourceHandlerArgArray.find(isExtraHandlerDataWithRequestId);
return associateContextWithRequestSpan(extraHandlerDataWithRequestId, () => {
return startSpan(
{
name: `mcp-server/resource:${resourceName}`,
forceTransaction: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'mcp_server.resource': resourceName,
},
},
() => resourceHandlerTarget.apply(resourceHandlerThisArg, resourceHandlerArgArray),
);
});
},
});
return Reflect.apply(target, thisArg, [...argArray.slice(0, -1), wrappedResourceHandler]);
},
});
mcpServerInstance.tool = new Proxy(mcpServerInstance.tool, {
apply(target, thisArg, argArray) {
const toolName = argArray[0];
const toolHandler = argArray[argArray.length - 1];
if (typeof toolName !== 'string' || typeof toolHandler !== 'function') {
return target.apply(thisArg, argArray);
}
const wrappedToolHandler = new Proxy(toolHandler, {
apply(toolHandlerTarget, toolHandlerThisArg, toolHandlerArgArray) {
const extraHandlerDataWithRequestId = toolHandlerArgArray.find(isExtraHandlerDataWithRequestId);
return associateContextWithRequestSpan(extraHandlerDataWithRequestId, () => {
return startSpan(
{
name: `mcp-server/tool:${toolName}`,
forceTransaction: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'mcp_server.tool': toolName,
},
},
() => toolHandlerTarget.apply(toolHandlerThisArg, toolHandlerArgArray),
);
});
},
});
return Reflect.apply(target, thisArg, [...argArray.slice(0, -1), wrappedToolHandler]);
},
});
mcpServerInstance.prompt = new Proxy(mcpServerInstance.prompt, {
apply(target, thisArg, argArray) {
const promptName = argArray[0];
const promptHandler = argArray[argArray.length - 1];
if (typeof promptName !== 'string' || typeof promptHandler !== 'function') {
return target.apply(thisArg, argArray);
}
const wrappedPromptHandler = new Proxy(promptHandler, {
apply(promptHandlerTarget, promptHandlerThisArg, promptHandlerArgArray) {
const extraHandlerDataWithRequestId = promptHandlerArgArray.find(isExtraHandlerDataWithRequestId);
return associateContextWithRequestSpan(extraHandlerDataWithRequestId, () => {
return startSpan(
{
name: `mcp-server/prompt:${promptName}`,
forceTransaction: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.mcp-server',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'mcp_server.prompt': promptName,
},
},
() => promptHandlerTarget.apply(promptHandlerThisArg, promptHandlerArgArray),
);
});
},
});
return Reflect.apply(target, thisArg, [...argArray.slice(0, -1), wrappedPromptHandler]);
},
});
wrappedMcpServerInstances.add(mcpServerInstance);
return mcpServerInstance ;
}
function isMcpServerInstance(mcpServerInstance) {
return (
typeof mcpServerInstance === 'object' &&
mcpServerInstance !== null &&
'resource' in mcpServerInstance &&
typeof mcpServerInstance.resource === 'function' &&
'tool' in mcpServerInstance &&
typeof mcpServerInstance.tool === 'function' &&
'prompt' in mcpServerInstance &&
typeof mcpServerInstance.prompt === 'function' &&
'connect' in mcpServerInstance &&
typeof mcpServerInstance.connect === 'function'
);
}
function isJsonRPCMessageWithRequestId(target) {
return (
typeof target === 'object' &&
target !== null &&
'id' in target &&
(typeof target.id === 'number' || typeof target.id === 'string')
);
}
// Note that not all versions of the MCP library have `requestId` as a field on the extra data.
function isExtraHandlerDataWithRequestId(target) {
return (
typeof target === 'object' &&
target !== null &&
'sessionId' in target &&
typeof target.sessionId === 'string' &&
'requestId' in target &&
(typeof target.requestId === 'number' || typeof target.requestId === 'string')
);
}
const sessionAndRequestToRequestParentSpanMap = new Map();
function handleTransportOnClose(sessionId) {
sessionAndRequestToRequestParentSpanMap.delete(sessionId);
}
function handleTransportOnMessage(sessionId, requestId) {
const activeSpan = getActiveSpan();
if (activeSpan) {
const requestIdToSpanMap = sessionAndRequestToRequestParentSpanMap.get(sessionId) ?? new Map();
requestIdToSpanMap.set(requestId, activeSpan);
sessionAndRequestToRequestParentSpanMap.set(sessionId, requestIdToSpanMap);
}
}
function associateContextWithRequestSpan(
extraHandlerData,
cb,
) {
if (extraHandlerData) {
const { sessionId, requestId } = extraHandlerData;
const requestIdSpanMap = sessionAndRequestToRequestParentSpanMap.get(sessionId);
if (!requestIdSpanMap) {
return cb();
}
const span = requestIdSpanMap.get(requestId);
if (!span) {
return cb();
}
// remove the span from the map so it can be garbage collected
requestIdSpanMap.delete(requestId);
return withActiveSpan(span, () => {
return cb();
});
}
return cb();
}
export { wrapMcpServerWithSentry };
//# sourceMappingURL=mcp-server.js.map