UNPKG

@sentry/core

Version:
268 lines (233 loc) 9.7 kB
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