UNPKG

@upstash/context7-mcp

Version:
629 lines (628 loc) • 23.5 kB
import { BAGGAGE_META_KEY, McpServer, PROTOCOL_VERSION_META_KEY, SUPPORTED_PROTOCOL_VERSIONS, TRACEPARENT_META_KEY, TRACESTATE_META_KEY, isCallToolResult, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse, } from "@modelcontextprotocol/server"; import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, isSpanContextValid, metrics, propagation, trace, } from "@opentelemetry/api"; import { MCP_TOOL_NAMES } from "./tool-names.js"; import { runInMcpOperationScope } from "./mcp-operation-scope.js"; import { mcpRouteFromUrl } from "./mcp-route.js"; import { StdioSubscriptionTelemetry, instrumentMcpHttpHandler as instrumentHttpSubscriptions, } from "./mcp-subscription-telemetry.js"; export { mcpRouteFromUrl } from "./mcp-route.js"; const INSTRUMENTATION_NAME = "io.github.upstash.context7.mcp"; const MCP_DURATION_BUCKETS_SECONDS = [ 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, ]; const KNOWN_MCP_METHODS = new Set([ "completion/complete", "elicitation/create", "initialize", "logging/setLevel", "notifications/cancelled", "notifications/elicitation/complete", "notifications/initialized", "notifications/message", "notifications/progress", "notifications/prompts/list_changed", "notifications/resources/list_changed", "notifications/resources/updated", "notifications/roots/list_changed", "notifications/subscriptions/acknowledged", "notifications/tasks/status", "notifications/tools/list_changed", "ping", "prompts/get", "prompts/list", "resources/list", "resources/read", "resources/subscribe", "resources/templates/list", "resources/unsubscribe", "roots/list", "sampling/createMessage", "server/discover", "subscriptions/listen", "tasks/cancel", "tasks/get", "tasks/list", "tasks/result", "tools/call", "tools/list", ]); const KNOWN_TOOLS = new Set(MCP_TOOL_NAMES); export const MODERN_MCP_PROTOCOL_VERSION = "2026-07-28"; const KNOWN_PROTOCOL_VERSIONS = new Set([ ...SUPPORTED_PROTOCOL_VERSIONS, MODERN_MCP_PROTOCOL_VERSION, ]); const EMPTY_TRACE_CARRIER = Object.freeze({}); const CALLER_FAULT_CODES = new Set([-32700, -32600, -32601, -32602, -32002]); function getInstruments() { const meter = metrics.getMeter(INSTRUMENTATION_NAME); return { operationDuration: meter.createHistogram("mcp.server.operation.duration", { description: "MCP request or notification duration from receipt until the result or acknowledgement is sent", unit: "s", advice: { explicitBucketBoundaries: MCP_DURATION_BUCKETS_SECONDS }, }), sessionDuration: meter.createHistogram("mcp.server.session.duration", { description: "Duration of a stateful MCP server session", unit: "s", advice: { explicitBucketBoundaries: MCP_DURATION_BUCKETS_SECONDS }, }), activeOperations: meter.createUpDownCounter("context7.mcp.operations.active", { description: "Number of MCP requests and notifications currently being handled", unit: "{operation}", }), }; } let instruments; function mcpInstruments() { instruments ??= getInstruments(); return instruments; } function asRecord(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : undefined; } export function normalizeMcpMethodName(method) { return typeof method === "string" && KNOWN_MCP_METHODS.has(method) ? method : "unknown"; } export function normalizeMcpToolName(tool) { return typeof tool === "string" && KNOWN_TOOLS.has(tool) ? tool : "unknown"; } function normalizedTool(message) { if (!isJSONRPCRequest(message) || message.method !== "tools/call") return undefined; const name = asRecord(message.params)?.name; return normalizeMcpToolName(name); } function normalizedProtocolVersion(value) { return typeof value === "string" && KNOWN_PROTOCOL_VERSIONS.has(value) ? value : undefined; } function messageProtocolVersion(message) { if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) return undefined; const params = asRecord(message.params); const metadata = asRecord(params?._meta); return (normalizedProtocolVersion(metadata?.[PROTOCOL_VERSION_META_KEY]) ?? normalizedProtocolVersion(params?.protocolVersion)); } export function mcpTraceCarrier(message) { if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) return EMPTY_TRACE_CARRIER; const metadata = asRecord(asRecord(message.params)?._meta); if (!metadata) return EMPTY_TRACE_CARRIER; const carrier = {}; for (const key of [TRACEPARENT_META_KEY, TRACESTATE_META_KEY, BAGGAGE_META_KEY]) { const value = metadata[key]; if (typeof value === "string") carrier[key] = value; } return carrier; } function ambientLink(parentContext) { const ambient = trace.getSpan(context.active())?.spanContext(); const parent = trace.getSpan(parentContext)?.spanContext(); if (!ambient || !isSpanContextValid(ambient)) return undefined; if (parent && ambient.traceId === parent.traceId && ambient.spanId === parent.spanId) { return undefined; } return [{ context: ambient }]; } function elapsedSeconds(startedAt) { return (performance.now() - startedAt) / 1_000; } function requestIdAttribute(requestId) { return requestId === null ? undefined : String(requestId); } function startOperation(message, config, transportProtocolVersion) { if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) return undefined; const method = normalizeMcpMethodName(message.method); const tool = normalizedTool(message); const protocolVersion = messageProtocolVersion(message) ?? transportProtocolVersion ?? config.protocolVersion; const activeAttributes = { "context7.mcp.route": config.route, "mcp.method.name": method, }; const attributes = { ...activeAttributes, "network.transport": config.networkTransport, }; if (config.networkProtocol) attributes["network.protocol.name"] = config.networkProtocol; if (protocolVersion) attributes["mcp.protocol.version"] = protocolVersion; if (tool) { activeAttributes["gen_ai.tool.name"] = tool; attributes["gen_ai.tool.name"] = tool; attributes["gen_ai.operation.name"] = "execute_tool"; } const requestId = isJSONRPCRequest(message) ? message.id : undefined; const parentContext = propagation.extract(ROOT_CONTEXT, mcpTraceCarrier(message)); const span = trace.getTracer(INSTRUMENTATION_NAME).startSpan(tool ? `${method} ${tool}` : method, { attributes, kind: SpanKind.SERVER, links: ambientLink(parentContext), }, parentContext); if (requestId !== undefined) { const requestIdValue = requestIdAttribute(requestId); if (requestIdValue) span.setAttribute("jsonrpc.request.id", requestIdValue); } const operation = { activeAttributes, attributes, context: ROOT_CONTEXT, requestId, span, state: "handling", startedAt: performance.now(), }; operation.context = trace.setSpan(parentContext, span); mcpInstruments().activeOperations.add(1, activeAttributes); return operation; } function finishOperation(operation) { if (operation.state === "finished") return; operation.state = "finished"; const attributes = operation.errorType || operation.toolOutcome ? { ...operation.attributes } : operation.attributes; if (operation.errorType) attributes["error.type"] = operation.errorType; if (operation.toolOutcome) { attributes["context7.mcp.tool.outcome"] = operation.toolOutcome; operation.span.setAttribute("context7.mcp.tool.outcome", operation.toolOutcome); } const { activeOperations, operationDuration } = mcpInstruments(); activeOperations.add(-1, operation.activeAttributes); operationDuration.record(elapsedSeconds(operation.startedAt), attributes); if (operation.errorType) { operation.span.setAttribute("error.type", operation.errorType); operation.span.setStatus({ code: SpanStatusCode.ERROR, message: operation.statusDescription, }); } operation.span.end(); } function operationIsFinished(operation) { return operation.state === "finished"; } function runOperation(operation, handler) { return runInMcpOperationScope(operation, () => context.with(operation.context, handler)); } export function classifyServerResponse(message, operationMethod) { if (isJSONRPCErrorResponse(message)) { return { errorType: CALLER_FAULT_CODES.has(message.error.code) ? undefined : String(message.error.code), rpcStatusCode: String(message.error.code), statusDescription: message.error.message, }; } if (isJSONRPCResultResponse(message) && operationMethod === "tools/call" && isCallToolResult(message.result) && message.result.isError) { return { errorType: "tool_error" }; } return {}; } function applyServerResponse(message, operation) { const classification = classifyServerResponse(message, operation.attributes["mcp.method.name"]); // A JSON-RPC error is the canonical final classification, including caller // faults that intentionally clear a provisional server error. Successful // envelopes retain an application-level tool_error captured by the tool // wrapper when the SDK normalizes the result before transport serialization. if (isJSONRPCErrorResponse(message) || classification.errorType) { operation.errorType = classification.errorType; } if (classification.errorType && operation.attributes["mcp.method.name"] === "tools/call") { operation.toolOutcome = "error"; } operation.statusDescription = classification.statusDescription; if (classification.rpcStatusCode) { operation.attributes["rpc.response.status_code"] = classification.rpcStatusCode; operation.span.setAttribute("rpc.response.status_code", classification.rpcStatusCode); } } function cancellationRequestId(message) { if (!isJSONRPCNotification(message) || message.method !== "notifications/cancelled") { return undefined; } const requestId = asRecord(message.params)?.requestId; return typeof requestId === "string" || typeof requestId === "number" ? requestId : undefined; } function configFromRequestContext(requestContext) { const request = requestContext.requestInfo; if (!request) { return { route: "stdio", networkTransport: "pipe", protocolVersion: requestContext.era === "modern" ? MODERN_MCP_PROTOCOL_VERSION : undefined, }; } return { abortSignal: request.signal, route: mcpRouteFromUrl(request.url), networkProtocol: "http", networkTransport: "tcp", protocolVersion: normalizedProtocolVersion(request.headers.get("mcp-protocol-version")) ?? (requestContext.era === "modern" ? MODERN_MCP_PROTOCOL_VERSION : undefined), }; } function startSubscriptionEntryOperation(message, observation) { const operation = startOperation(message, { ...observation, protocolVersion: normalizedProtocolVersion(observation.protocolVersion), }); if (!operation) throw new TypeError("Expected an MCP request or notification"); return { applyResponse(response) { if (!operationIsFinished(operation)) applyServerResponse(response, operation); }, fail(errorType, error) { if (operationIsFinished(operation)) return; operation.errorType = errorType; operation.statusDescription = error instanceof Error ? error.message : undefined; if (error instanceof Error) operation.span.recordException(error); finishOperation(operation); }, finish() { finishOperation(operation); }, run: (callback) => runOperation(operation, callback), }; } export function instrumentMcpHttpHandler(handler) { return instrumentHttpSubscriptions(handler, startSubscriptionEntryOperation); } class InstrumentedTransport { transport; config; abortSignal; inFlight = new Map(); messageHandler; closeHandler; errorHandler; protocolVersion; constructor(transport, config) { this.transport = transport; this.config = config; this.abortSignal = config.abortSignal; this.protocolVersion = config.protocolVersion; this.onclose = transport.onclose; this.onerror = transport.onerror; this.onmessage = transport.onmessage; this.abortSignal?.addEventListener("abort", this.handleAbort, { once: true }); } handleAbort = () => { this.finishAll("cancelled", true); }; get hasPerRequestStream() { return this.transport.hasPerRequestStream; } get sessionId() { return this.transport.sessionId; } set sessionId(value) { this.transport.sessionId = value; } get onclose() { return this.closeHandler; } set onclose(handler) { this.closeHandler = handler; this.transport.onclose = () => { this.finishAll("connection_closed"); this.detachAbortHandler(); handler?.(); }; } get onerror() { return this.errorHandler; } set onerror(handler) { this.errorHandler = handler; this.transport.onerror = handler; } get onmessage() { return this.messageHandler; } set onmessage(handler) { this.messageHandler = handler; this.transport.onmessage = handler ? (message, extra) => this.receive(message, extra, handler) : undefined; } setProtocolVersion = (version) => { this.protocolVersion = normalizedProtocolVersion(version); this.transport.setProtocolVersion?.(version); }; setSupportedProtocolVersions = (versions) => { this.transport.setSupportedProtocolVersions?.(versions); }; async start() { await this.transport.start(); } async close() { try { await this.transport.close(); } finally { this.finishAll("connection_closed"); this.detachAbortHandler(); } } async send(message, options) { const responseId = isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message) ? message.id : undefined; const operation = responseId !== undefined && responseId !== null ? this.inFlight.get(responseId) : undefined; try { if (!operation) { await this.transport.send(message, options); return; } applyServerResponse(message, operation); operation.state = "sending"; await this.transport.send(message, options); } catch (error) { if (operation && !operationIsFinished(operation)) { operation.errorType = "transport_error"; operation.statusDescription = error instanceof Error ? error.message : undefined; if (error instanceof Error) operation.span.recordException(error); } throw error; } finally { if (operation) { this.removeInFlight(operation); finishOperation(operation); } } } receive(message, extra, handler) { const operation = startOperation(message, this.config, this.protocolVersion); if (!operation) { handler(message, extra); return; } if (operation.requestId !== undefined && operation.requestId !== null) { const previous = this.inFlight.get(operation.requestId); if (previous) { previous.errorType = "duplicate_request_id"; finishOperation(previous); } this.inFlight.set(operation.requestId, operation); } try { runOperation(operation, () => handler(message, extra)); } catch (error) { operation.errorType = "handler_error"; if (error instanceof Error) operation.span.recordException(error); if (operation.requestId !== undefined && operation.requestId !== null) { this.inFlight.delete(operation.requestId); } finishOperation(operation); throw error; } if (isJSONRPCNotification(message)) { const cancelledRequestId = cancellationRequestId(message); if (cancelledRequestId !== undefined) this.cancelOperation(cancelledRequestId); finishOperation(operation); } } cancelOperation(requestId) { const operation = this.inFlight.get(requestId); if (!operation) return; operation.errorType = "cancelled"; this.inFlight.delete(requestId); finishOperation(operation); } removeInFlight(operation) { const requestId = operation.requestId; if (requestId !== undefined && requestId !== null && this.inFlight.get(requestId) === operation) { this.inFlight.delete(requestId); } } detachAbortHandler() { this.abortSignal?.removeEventListener("abort", this.handleAbort); } finishAll(errorType, includeSending = false) { for (const [requestId, operation] of this.inFlight) { // A normal per-request HTTP transport closes its stream from inside send(). // Let an active send settle so its success or failure remains authoritative. if (operation.state === "sending" && !includeSending) continue; operation.errorType ??= errorType; finishOperation(operation); this.inFlight.delete(requestId); } } } /** * Owns session telemetry at the one process-level stdio wire. The SDK may * create multiple products while probing protocol eras, but they all share * this transport, which also receives the actually negotiated version. */ class InstrumentedStdioTransport { transport; closeHandler; closePromise; closeRequested = false; errorType; errorHandler; finished = false; messageHandler; pendingTerminalError = false; protocolVersion; startedAt = performance.now(); subscriptions = new StdioSubscriptionTelemetry(startSubscriptionEntryOperation, MODERN_MCP_PROTOCOL_VERSION); constructor(transport) { this.transport = transport; this.onclose = transport.onclose; this.onerror = transport.onerror; this.onmessage = transport.onmessage; } get hasPerRequestStream() { return this.transport.hasPerRequestStream; } get sessionId() { return this.transport.sessionId; } set sessionId(value) { this.transport.sessionId = value; } get onclose() { return this.closeHandler; } set onclose(handler) { this.closeHandler = handler; this.transport.onclose = () => { this.subscriptions.close("connection_closed"); if (!this.closeRequested) { this.finish(this.pendingTerminalError ? "transport_error" : undefined); } handler?.(); }; } get onerror() { return this.errorHandler; } set onerror(handler) { this.errorHandler = handler; this.transport.onerror = (error) => { this.pendingTerminalError = true; queueMicrotask(() => { this.pendingTerminalError = false; }); handler?.(error); }; } get onmessage() { return this.messageHandler; } set onmessage(handler) { this.messageHandler = handler; this.transport.onmessage = handler ? (message, extra) => this.receive(message, extra, handler) : undefined; } setProtocolVersion = (version) => { this.protocolVersion = normalizedProtocolVersion(version); this.transport.setProtocolVersion?.(version); }; setSupportedProtocolVersions = (versions) => { this.transport.setSupportedProtocolVersions?.(versions); }; async start() { try { await this.transport.start(); } catch (error) { this.errorType = "transport_error"; this.finish("transport_error"); throw error; } } close() { if (!this.closePromise) { this.closeRequested = true; this.closePromise = this.closeTransport(); } return this.closePromise; } async send(message, options) { try { await this.subscriptions.send(message, options, (outbound, sendOptions) => this.transport.send(outbound, sendOptions), this.protocolVersion); } catch (error) { this.errorType ??= "transport_error"; throw error; } } receive(message, extra, handler) { this.protocolVersion = messageProtocolVersion(message) ?? this.protocolVersion; if (this.subscriptions.receive(message, extra, handler, this.protocolVersion)) return; handler(message, extra); } async closeTransport() { try { await this.transport.close(); this.subscriptions.close("connection_closed", true); this.finish(); } catch (error) { this.errorType = "transport_error"; this.subscriptions.close("transport_error", true); this.finish("transport_error"); throw error; } } finish(errorType = this.errorType) { if (this.finished) return; this.finished = true; const attributes = { "network.transport": "pipe" }; if (this.protocolVersion) attributes["mcp.protocol.version"] = this.protocolVersion; if (errorType) attributes["error.type"] = errorType; mcpInstruments().sessionDuration.record(elapsedSeconds(this.startedAt), attributes); } } export function instrumentStdioTransport(transport) { return new InstrumentedStdioTransport(transport); } /** * High-level MCP server with protocol-aware OpenTelemetry at the SDK transport * boundary. This observes individual JSON-RPC operations for HTTP and stdio, * including batched messages, instead of treating an HTTP envelope as one MCP * operation. */ export class InstrumentedMcpServer extends McpServer { requestContext; constructor(serverInfo, options, requestContext) { super(serverInfo, options); this.requestContext = requestContext; } connect(transport) { return super.connect(new InstrumentedTransport(transport, configFromRequestContext(this.requestContext))); } }