UNPKG

mcpcat

Version:

Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights

1,571 lines (1,550 loc) 93.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { publishCustomEvent: () => publishCustomEvent, track: () => track }); module.exports = __toCommonJS(index_exports); // src/modules/logging.ts var import_module = require("module"); var import_meta = {}; var fsModule = null; var logFilePath = null; var initAttempted = false; var useConsoleFallback = false; function tryInitSync() { if (initAttempted) return; initAttempted = true; try { const require2 = (0, import_module.createRequire)(import_meta.url); const fs = require2("fs"); const os = require2("os"); const path = require2("path"); const home = os.homedir?.(); if (home) { fsModule = fs; logFilePath = path.join(home, "mcpcat.log"); } else { useConsoleFallback = true; } } catch { useConsoleFallback = true; fsModule = null; logFilePath = null; } } function writeToLog(message) { tryInitSync(); const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const logEntry = `[${timestamp}] ${message}`; if (useConsoleFallback) { console.log(`[mcpcat] ${logEntry}`); return; } if (!logFilePath || !fsModule) { return; } try { if (!fsModule.existsSync(logFilePath)) { fsModule.writeFileSync(logFilePath, logEntry + "\n"); } else { fsModule.appendFileSync(logFilePath, logEntry + "\n"); } } catch { } } // src/modules/compatibility.ts function logCompatibilityWarning() { writeToLog( "MCPCat SDK Compatibility: This version only supports Model Context Protocol TypeScript SDK v1.11 and above. Please upgrade if using an older version." ); } function isHighLevelServer(server) { return server && typeof server === "object" && server.server && typeof server.server === "object"; } function isCompatibleServerType(server) { if (!server || typeof server !== "object") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server must be an object. Ensure you're using MCP SDK v1.11 or higher." ); } if (isHighLevelServer(server)) { if (!server._registeredTools || typeof server._registeredTools !== "object") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: High-level server must have _registeredTools object. This requires MCP SDK v1.11 or higher." ); } if (typeof server.tool !== "function") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: High-level server must have tool() method. This requires MCP SDK v1.11 or higher." ); } const targetServer = server.server; validateLowLevelServer(targetServer); return server; } else { validateLowLevelServer(server); return server; } } function validateLowLevelServer(server) { if (typeof server.setRequestHandler !== "function") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server must have a setRequestHandler method. This requires MCP SDK v1.11 or higher." ); } if (!server._requestHandlers || !(server._requestHandlers instanceof Map)) { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server._requestHandlers is not accessible. This requires MCP SDK v1.11 or higher." ); } if (typeof server._requestHandlers.get !== "function") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server._requestHandlers must be a Map with a get method. This requires MCP SDK v1.11 or higher." ); } if (typeof server.getClientVersion !== "function") { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server.getClientVersion must be a function. This requires MCP SDK v1.11 or higher." ); } if (!server._serverInfo || typeof server._serverInfo !== "object" || !server._serverInfo.name) { logCompatibilityWarning(); throw new Error( "MCPCat SDK compatibility error: Server._serverInfo is not accessible or missing name. This requires MCP SDK v1.11 or higher." ); } } function getMCPCompatibleErrorMessage(error) { if (error instanceof Error) { try { return JSON.stringify(error, Object.getOwnPropertyNames(error)); } catch { return "Unknown error"; } } else if (typeof error === "string") { return error; } else if (typeof error === "object" && error !== null) { return JSON.stringify(error); } return "Unknown error"; } // src/modules/tools.ts var import_types = require("@modelcontextprotocol/sdk/types.js"); // src/modules/internal.ts var import_mcpcat_api2 = require("mcpcat-api"); // src/modules/eventQueue.ts var import_mcpcat_api = require("mcpcat-api"); // src/thirdparty/ksuid/index.js var import_node_crypto = require("crypto"); var import_node_util = require("util"); var import_node_util2 = require("util"); // src/thirdparty/ksuid/base-convert-int-array.js var maxLength = (array, from, to) => Math.ceil(array.length * Math.log2(from) / Math.log2(to)); function baseConvertIntArray(array, { from, to, fixedLength = null }) { const length = fixedLength === null ? maxLength(array, from, to) : fixedLength; const result = new Array(length); let offset = length; let input = array; while (input.length > 0) { if (offset === 0) { throw new RangeError( `Fixed length of ${fixedLength} is too small, expected at least ${maxLength(array, from, to)}` ); } const quotients = []; let remainder = 0; for (const digit of input) { const acc = digit + remainder * from; const q = Math.floor(acc / to); remainder = acc % to; if (quotients.length > 0 || q > 0) { quotients.push(q); } } result[--offset] = remainder; input = quotients; } if (fixedLength === null) { return offset > 0 ? result.slice(offset) : result; } while (offset > 0) { result[--offset] = 0; } return result; } var base_convert_int_array_default = baseConvertIntArray; // src/thirdparty/ksuid/base62.js var CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; function encode(buffer, fixedLength) { return base_convert_int_array_default(buffer, { from: 256, to: 62, fixedLength }).map((value) => CHARS[value]).join(""); } function decode(string, fixedLength) { const input = Array.from(string, (char) => { const charCode = char.charCodeAt(0); if (charCode < 58) return charCode - 48; if (charCode < 91) return charCode - 55; return charCode - 61; }); return Buffer.from( base_convert_int_array_default(input, { from: 62, to: 256, fixedLength }) ); } // src/thirdparty/ksuid/index.js var customInspectSymbol = import_node_util.inspect.custom; var asyncRandomBytes = (0, import_node_util2.promisify)(import_node_crypto.randomBytes); var EPOCH_IN_MS = 14e11; var MAX_TIME_IN_MS = 1e3 * (2 ** 32 - 1) + EPOCH_IN_MS; var TIMESTAMP_BYTE_LENGTH = 4; var PAYLOAD_BYTE_LENGTH = 16; var BYTE_LENGTH = TIMESTAMP_BYTE_LENGTH + PAYLOAD_BYTE_LENGTH; var STRING_ENCODED_LENGTH = 27; var TIME_IN_MS_ASSERTION = `Valid KSUID timestamps must be in milliseconds since ${(/* @__PURE__ */ new Date(0)).toISOString()}, no earlier than ${new Date(EPOCH_IN_MS).toISOString()} and no later than ${new Date(MAX_TIME_IN_MS).toISOString()} `.trim().replace(/(\n|\s)+/g, " ").replace(/\.000Z/g, "Z"); var VALID_ENCODING_ASSERTION = `Valid encoded KSUIDs are ${STRING_ENCODED_LENGTH} characters`; var VALID_BUFFER_ASSERTION = `Valid KSUID buffers are ${BYTE_LENGTH} bytes`; var VALID_PAYLOAD_ASSERTION = `Valid KSUID payloads are ${PAYLOAD_BYTE_LENGTH} bytes`; function fromParts(timeInMs, payload) { const timestamp = Math.floor((timeInMs - EPOCH_IN_MS) / 1e3); const timestampBuffer = Buffer.allocUnsafe(TIMESTAMP_BYTE_LENGTH); timestampBuffer.writeUInt32BE(timestamp, 0); return Buffer.concat([timestampBuffer, payload], BYTE_LENGTH); } var bufferLookup = /* @__PURE__ */ new WeakMap(); var KSUID = class _KSUID { constructor(buffer) { if (!_KSUID.isValid(buffer)) { throw new TypeError(VALID_BUFFER_ASSERTION); } bufferLookup.set(this, buffer); Object.defineProperty(this, "buffer", { enumerable: true, get() { return Buffer.from(buffer); } }); } get raw() { return Buffer.from(bufferLookup.get(this).slice(0)); } get date() { return new Date(1e3 * this.timestamp + EPOCH_IN_MS); } get timestamp() { return bufferLookup.get(this).readUInt32BE(0); } get payload() { const payload = bufferLookup.get(this).slice(TIMESTAMP_BYTE_LENGTH, BYTE_LENGTH); return Buffer.from(payload); } get string() { const encoded = encode( bufferLookup.get(this), STRING_ENCODED_LENGTH ); return encoded.padStart(STRING_ENCODED_LENGTH, "0"); } compare(other) { if (!bufferLookup.has(other)) { return 0; } return bufferLookup.get(this).compare(bufferLookup.get(other), 0, BYTE_LENGTH); } equals(other) { return this === other || bufferLookup.has(other) && this.compare(other) === 0; } toString() { return `${this[Symbol.toStringTag]} { ${this.string} }`; } toJSON() { return this.string; } [customInspectSymbol]() { return this.toString(); } static async random(time = Date.now()) { const payload = await asyncRandomBytes(PAYLOAD_BYTE_LENGTH); return new _KSUID(fromParts(Number(time), payload)); } static randomSync(time = Date.now()) { const payload = (0, import_node_crypto.randomBytes)(PAYLOAD_BYTE_LENGTH); return new _KSUID(fromParts(Number(time), payload)); } static fromParts(timeInMs, payload) { if (!Number.isInteger(timeInMs) || timeInMs < EPOCH_IN_MS || timeInMs > MAX_TIME_IN_MS) { throw new TypeError(TIME_IN_MS_ASSERTION); } if (!Buffer.isBuffer(payload) || payload.byteLength !== PAYLOAD_BYTE_LENGTH) { throw new TypeError(VALID_PAYLOAD_ASSERTION); } return new _KSUID(fromParts(timeInMs, payload)); } static isValid(buffer) { return Buffer.isBuffer(buffer) && buffer.byteLength === BYTE_LENGTH; } static parse(string) { if (string.length !== STRING_ENCODED_LENGTH) { throw new TypeError(VALID_ENCODING_ASSERTION); } const decoded = decode(string, BYTE_LENGTH); if (decoded.byteLength === BYTE_LENGTH) { return new _KSUID(decoded); } const buffer = Buffer.allocUnsafe(BYTE_LENGTH); const padEnd = BYTE_LENGTH - decoded.byteLength; buffer.fill(0, 0, padEnd); decoded.copy(buffer, padEnd); return new _KSUID(buffer); } }; Object.defineProperty(KSUID.prototype, Symbol.toStringTag, { value: "KSUID" }); Object.defineProperty(KSUID, "MAX_STRING_ENCODED", { value: "aWgEPTl1tmebfsQzFP4bxwgy80V" }); Object.defineProperty(KSUID, "MIN_STRING_ENCODED", { value: "000000000000000000000000000" }); KSUID.withPrefix = function(prefix) { return { random: async (time = Date.now()) => { const ksuid = await KSUID.random(time); return `${prefix}_${ksuid.string}`; }, randomSync: (time = Date.now()) => { const ksuid = KSUID.randomSync(time); return `${prefix}_${ksuid.string}`; }, fromParts: (timeInMs, payload) => { const ksuid = KSUID.fromParts(timeInMs, payload); return `${prefix}_${ksuid.string}`; } }; }; var ksuid_default = KSUID; // package.json var package_default = { name: "mcpcat", version: "0.1.11", description: "Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights", type: "module", main: "dist/index.js", module: "dist/index.mjs", types: "dist/index.d.ts", exports: { ".": { types: "./dist/index.d.ts", import: "./dist/index.mjs", require: "./dist/index.cjs" } }, scripts: { build: "tsup", dev: "tsup --watch", test: "vitest", "test:compatibility": "vitest run src/tests/mcp-version-compatibility.test.ts", lint: "eslint src/", typecheck: "tsc --noEmit", prepare: "husky", prepublishOnly: "pnpm run build && pnpm run test && pnpm run lint && pnpm run typecheck" }, keywords: [ "ai", "authentication", "mcp", "observability", "ai-agents", "ai-platform", "ai-agent", "mcps", "aiagents", "ai-agent-tools", "mcp-servers", "mcp-server", "mcp-tools", "agent-runtime", "mcp-framework", "mcp-analytics" ], author: "MCPcat", license: "MIT", repository: { type: "git", url: "git+https://github.com/MCPCat/mcpcat-typescript-sdk.git" }, bugs: { url: "https://github.com/MCPCat/mcpcat-typescript-sdk/issues" }, homepage: "https://github.com/MCPCat/mcpcat-typescript-sdk#readme", packageManager: "pnpm@10.11.0", devDependencies: { "@changesets/cli": "^2.29.8", "@modelcontextprotocol/sdk": "~1.24.2", "@types/node": "^22.15.21", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", eslint: "^9.39.1", husky: "^9.1.7", "lint-staged": "^16.1.0", prettier: "^3.5.3", tsup: "^8.5.0", typescript: "^5.8.3", vitest: "^4.0.14", zod: "^3.25 || ^4.0" }, peerDependencies: { "@modelcontextprotocol/sdk": ">=1.11" }, dependencies: { "@opentelemetry/otlp-transformer": "^0.203.0", "mcpcat-api": "0.1.6" }, "lint-staged": { "*.{ts,js}": [ "eslint --fix", "prettier --write" ], "*.{json,md,yml,yaml}": [ "prettier --write" ] }, pnpm: { overrides: { "js-yaml": ">=4.1.1", tmp: ">=0.2.4", vite: ">=6.4.1", "body-parser": ">=2.2.1", "brace-expansion": "2.0.2" }, overridesComments: { "js-yaml": "Fixes GHSA-mh29-5h37-fv8m (prototype pollution in merge) - via @changesets/cli", tmp: "Fixes GHSA-52f5-9888-hmc6 (symlink attack) - via @changesets/cli", vite: "Fixes GHSA-93m4-6634-74q7 and other vite security issues - via vitest", "body-parser": "Fixes GHSA-wqch-xfxh-vrr4 (DoS via url encoding) - via @modelcontextprotocol/sdk", "brace-expansion": "Fixes GHSA-v6h2-p8h4-qcjw (ReDoS vulnerability) - via eslint" } } }; // src/modules/session.ts var import_crypto = require("crypto"); // src/modules/constants.ts var INACTIVITY_TIMEOUT_IN_MINUTES = 30; var DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): "Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization."`; var MCPCAT_CUSTOM_EVENT_TYPE = "mcpcat:custom"; // src/modules/session.ts function newSessionId() { return ksuid_default.withPrefix("ses").randomSync(); } function deriveSessionIdFromMCPSession(mcpSessionId, projectId) { const input = projectId ? `${mcpSessionId}:${projectId}` : mcpSessionId; const hash = (0, import_crypto.createHash)("sha256").update(input).digest(); const EPOCH_2024 = (/* @__PURE__ */ new Date("2024-01-01T00:00:00Z")).getTime(); const timestampOffset = hash.readUInt32BE(0) % (365 * 24 * 60 * 60 * 1e3); const timestamp = EPOCH_2024 + timestampOffset; const payload = hash.subarray(4, 20); return ksuid_default.withPrefix("ses").fromParts(timestamp, payload); } function getServerSessionId(server, extra) { const data = getServerTrackingData(server); if (!data) { throw new Error("Server tracking data not found"); } const mcpSessionId = extra?.sessionId; if (mcpSessionId) { data.sessionId = deriveSessionIdFromMCPSession( mcpSessionId, data.projectId || void 0 ); data.lastMcpSessionId = mcpSessionId; data.sessionSource = "mcp"; setServerTrackingData(server, data); setLastActivity(server); return data.sessionId; } if (data.sessionSource === "mcp" && data.lastMcpSessionId) { setLastActivity(server); return data.sessionId; } const now = Date.now(); const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1e3; if (now - data.lastActivity.getTime() > timeoutMs) { data.sessionId = newSessionId(); data.sessionSource = "mcpcat"; setServerTrackingData(server, data); } setLastActivity(server); return data.sessionId; } function setLastActivity(server) { const data = getServerTrackingData(server); if (!data) { throw new Error("Server tracking data not found"); } data.lastActivity = /* @__PURE__ */ new Date(); setServerTrackingData(server, data); } function getSessionInfo(server, data) { let clientInfo = { name: void 0, version: void 0 }; if (!data?.sessionInfo.clientName) { clientInfo = server.getClientVersion(); } const actorInfo = data?.identifiedSessions.get(data.sessionId); const sessionInfo = { ipAddress: void 0, // grab from django sdkLanguage: "TypeScript", // hardcoded for now mcpcatVersion: package_default.version, serverName: server._serverInfo?.name, serverVersion: server._serverInfo?.version, clientName: clientInfo?.name, clientVersion: clientInfo?.version, identifyActorGivenId: actorInfo?.userId, identifyActorName: actorInfo?.userName, identifyActorData: actorInfo?.userData || {} }; if (!data) { return sessionInfo; } data.sessionInfo = sessionInfo; setServerTrackingData(server, data); return data.sessionInfo; } // src/modules/redaction.ts var PROTECTED_FIELDS = /* @__PURE__ */ new Set([ "sessionId", "id", "projectId", "server", "identifyActorGivenId", "identifyActorName", "identifyData", "resourceName", "eventType", "actorId" ]); async function redactStringsInObject(obj, redactFn, path = "", isProtected = false) { if (obj === null || obj === void 0) { return obj; } if (typeof obj === "string") { if (isProtected) { return obj; } return await redactFn(obj); } if (Array.isArray(obj)) { return Promise.all( obj.map( (item, index) => redactStringsInObject(item, redactFn, `${path}[${index}]`, isProtected) ) ); } if (obj instanceof Date) { return obj; } if (typeof obj === "object") { const redactedObj = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === "function" || value === void 0) { continue; } const fieldPath = path ? `${path}.${key}` : key; const isFieldProtected = isProtected || path === "" && PROTECTED_FIELDS.has(key); redactedObj[key] = await redactStringsInObject( value, redactFn, fieldPath, isFieldProtected ); } return redactedObj; } return obj; } async function redactEvent(event, redactFn) { return redactStringsInObject(event, redactFn, "", false); } // src/modules/eventQueue.ts var EventQueue = class { constructor() { this.queue = []; this.processing = false; this.maxRetries = 3; this.maxQueueSize = 1e4; // Prevent unbounded growth this.concurrency = 5; // Max parallel requests this.activeRequests = 0; const config = new import_mcpcat_api.Configuration({ basePath: "https://api.mcpcat.io" }); this.apiClient = new import_mcpcat_api.EventsApi(config); } setTelemetryManager(telemetryManager) { this.telemetryManager = telemetryManager; } add(event) { if (this.queue.length >= this.maxQueueSize) { writeToLog("Event queue full, dropping oldest event"); this.queue.shift(); } this.queue.push(event); this.process(); } async process() { if (this.processing) return; this.processing = true; while (this.queue.length > 0 && this.activeRequests < this.concurrency) { const event = this.queue.shift(); if (event?.redactionFn) { try { const redactedEvent = await redactEvent(event, event.redactionFn); event.redactionFn = void 0; Object.assign(event, redactedEvent); } catch (error) { writeToLog(`Failed to redact event: ${error}`); continue; } } if (event) { event.id = event.id || await ksuid_default.withPrefix("evt").random(); this.activeRequests++; this.sendEvent(event).finally(() => { this.activeRequests--; this.process(); }); } } this.processing = false; } toPublishEventRequest(event) { return { // Core fields id: event.id, projectId: event.projectId, sessionId: event.sessionId, timestamp: event.timestamp, duration: event.duration, // Event data eventType: event.eventType, resourceName: event.resourceName, parameters: event.parameters, response: event.response, userIntent: event.userIntent, isError: event.isError, error: event.error, // Actor fields identifyActorGivenId: event.identifyActorGivenId, identifyActorName: event.identifyActorName, identifyData: event.identifyActorData, // Session info ipAddress: event.ipAddress, sdkLanguage: event.sdkLanguage, mcpcatVersion: event.mcpcatVersion, serverName: event.serverName, serverVersion: event.serverVersion, clientName: event.clientName, clientVersion: event.clientVersion, // Legacy fields actorId: event.actorId || event.identifyActorGivenId, eventId: event.eventId }; } async sendEvent(event, retries = 0) { if (this.telemetryManager) { this.telemetryManager.export(event).catch((error) => { writeToLog( `Telemetry export error: ${getMCPCompatibleErrorMessage(error)}` ); }); } if (event.projectId) { try { const publishRequest = this.toPublishEventRequest(event); await this.apiClient.publishEvent({ publishEventRequest: publishRequest }); writeToLog( `Successfully sent event ${event.id} | ${event.eventType} | ${event.projectId} | ${event.duration} ms | ${event.identifyActorGivenId || "anonymous"}` ); writeToLog(`Event details: ${JSON.stringify(event)}`); } catch (error) { writeToLog( `Failed to send event ${event.id}, retrying... [Error: ${getMCPCompatibleErrorMessage(error)}]` ); if (retries < this.maxRetries) { await this.delay(Math.pow(2, retries) * 1e3); return this.sendEvent(event, retries + 1); } throw error; } } } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // Get queue stats for monitoring getStats() { return { queueLength: this.queue.length, activeRequests: this.activeRequests, isProcessing: this.processing }; } // Graceful shutdown - wait for active requests async destroy() { this.add = () => { writeToLog("Queue is shutting down, event dropped"); }; const timeout = 5e3; const start = Date.now(); while ((this.queue.length > 0 || this.activeRequests > 0) && Date.now() - start < timeout) { await this.delay(100); } if (this.queue.length > 0) { writeToLog( `Shutting down with ${this.queue.length} events still in queue` ); } } }; var eventQueue = new EventQueue(); try { if (typeof process !== "undefined" && typeof process.once === "function") { process.once("SIGINT", () => eventQueue.destroy()); process.once("SIGTERM", () => eventQueue.destroy()); process.once("beforeExit", () => eventQueue.destroy()); } } catch { } function setTelemetryManager(telemetryManager) { eventQueue.setTelemetryManager(telemetryManager); } function publishEvent(server, eventInput) { const data = getServerTrackingData(server); if (!data) { writeToLog( "Warning: Server tracking data not found. Event will not be published." ); return; } const sessionInfo = getSessionInfo(server, data); const duration = eventInput.duration || (eventInput.timestamp ? (/* @__PURE__ */ new Date()).getTime() - eventInput.timestamp.getTime() : void 0); const fullEvent = { // Core fields (id will be generated later in the queue) id: eventInput.id || "", sessionId: eventInput.sessionId || data.sessionId, projectId: data.projectId, // Event metadata eventType: eventInput.eventType || "", timestamp: eventInput.timestamp || /* @__PURE__ */ new Date(), duration, // Session context from sessionInfo ipAddress: sessionInfo.ipAddress, sdkLanguage: sessionInfo.sdkLanguage, mcpcatVersion: sessionInfo.mcpcatVersion, serverName: sessionInfo.serverName, serverVersion: sessionInfo.serverVersion, clientName: sessionInfo.clientName, clientVersion: sessionInfo.clientVersion, // Actor information from sessionInfo identifyActorGivenId: sessionInfo.identifyActorGivenId, identifyActorName: sessionInfo.identifyActorName, identifyActorData: sessionInfo.identifyActorData, // Event-specific data from input resourceName: eventInput.resourceName, parameters: eventInput.parameters, response: eventInput.response, userIntent: eventInput.userIntent, isError: eventInput.isError, error: eventInput.error, // Preserve redaction function redactionFn: eventInput.redactionFn }; eventQueue.add(fullEvent); } // src/modules/exceptions.ts var import_module2 = require("module"); var import_meta2 = {}; var fsModule2 = null; var fsInitAttempted = false; function getFsSync() { if (!fsInitAttempted) { fsInitAttempted = true; try { const require2 = (0, import_module2.createRequire)(import_meta2.url); fsModule2 = require2("fs"); } catch { fsModule2 = null; } } return fsModule2; } var MAX_EXCEPTION_CHAIN_DEPTH = 10; var MAX_STACK_FRAMES = 50; function captureException(error, contextStack) { if (isCallToolResult(error)) { return captureCallToolResultError(error, contextStack); } if (!(error instanceof Error)) { return { message: stringifyNonError(error), type: void 0, platform: "javascript" }; } const errorData = { message: error.message || "", type: error.name || error.constructor?.name || void 0, platform: "javascript" }; if (error.stack) { errorData.stack = error.stack; errorData.frames = parseV8StackTrace(error.stack); } const chainedErrors = unwrapErrorCauses(error); if (chainedErrors.length > 0) { errorData.chained_errors = chainedErrors; } return errorData; } function parseV8StackTrace(stackTrace) { const frames = []; const lines = stackTrace.split("\n"); for (const line of lines) { if (!line.trim().startsWith("at ")) { continue; } const frame = parseV8StackFrame(line.trim()); if (frame) { addContextToFrame(frame); frames.push(frame); } if (frames.length >= MAX_STACK_FRAMES) { break; } } return frames; } function addContextToFrame(frame) { if (!frame.in_app || !frame.abs_path || !frame.lineno) { return frame; } const fs = getFsSync(); if (!fs) { return frame; } try { const source = fs.readFileSync(frame.abs_path, "utf8"); const lines = source.split("\n"); const lineIndex = frame.lineno - 1; if (lineIndex >= 0 && lineIndex < lines.length) { frame.context_line = lines[lineIndex]; } } catch { } return frame; } function parseLocation(location) { if (location === "native") { return { filename: "native", abs_path: "native" }; } if (location === "unknown location") { return { filename: "<unknown>", abs_path: "<unknown>" }; } if (location.startsWith("eval at ")) { return parseEvalOrigin(location); } const match = location.match(/^(.+):(\d+):(\d+)$/); if (match) { const [, filename, lineStr, colStr] = match; return { filename: makeRelativePath(filename), abs_path: filename, lineno: parseInt(lineStr, 10), colno: parseInt(colStr, 10) }; } return null; } function parseEvalOrigin(evalLocation) { let evalChainPart = evalLocation; const commaIndex = findCommaAfterBalancedParens(evalLocation); if (commaIndex !== -1) { evalChainPart = evalLocation.substring(0, commaIndex); } const match = evalChainPart.match(/^eval at (.+?) \((.+)\)$/); if (!match) { return null; } const innerLocation = match[2]; if (innerLocation.startsWith("eval at ")) { return parseEvalOrigin(innerLocation); } const locationMatch = innerLocation.match(/^(.+):(\d+):(\d+)$/); if (locationMatch) { const [, filename, lineStr, colStr] = locationMatch; return { filename: makeRelativePath(filename), abs_path: filename, lineno: parseInt(lineStr, 10), colno: parseInt(colStr, 10) }; } return null; } function findCommaAfterBalancedParens(str) { let depth = 0; let foundOpenParen = false; for (let i = 0; i < str.length; i++) { if (str[i] === "(") { depth++; foundOpenParen = true; } else if (str[i] === ")") { depth--; if (depth === 0 && foundOpenParen) { for (let j = i + 1; j < str.length; j++) { if (str[j] === ",") { return j; } else if (str[j] !== " ") { return -1; } } return -1; } } } return -1; } function parseV8StackFrame(line) { const withoutAt = line.substring(3); const matchWithFunction = withoutAt.match(/^(.+?)\s+\((.+)\)$/); if (matchWithFunction) { const [, functionName, location] = matchWithFunction; const parsedLocation2 = parseLocation(location); if (parsedLocation2) { return { function: functionName.trim(), filename: parsedLocation2.filename, abs_path: parsedLocation2.abs_path, lineno: parsedLocation2.lineno, colno: parsedLocation2.colno, in_app: isInApp(parsedLocation2.abs_path) }; } } const parsedLocation = parseLocation(withoutAt); if (parsedLocation) { return { function: "<anonymous>", filename: parsedLocation.filename, abs_path: parsedLocation.abs_path, lineno: parsedLocation.lineno, colno: parsedLocation.colno, in_app: isInApp(parsedLocation.abs_path) }; } return { function: withoutAt, filename: "<unknown>", in_app: false }; } function isInApp(filename) { if (filename.includes("/node_modules/") || filename.includes("\\node_modules\\")) { return false; } if (filename.startsWith("node:")) { return false; } if (filename === "native" || filename === "<unknown>") { return false; } return true; } function normalizeUrl(filename) { if (filename.startsWith("file://")) { let result = filename.substring(7); if (!result.startsWith("/") && !result.match(/^[A-Za-z]:/)) { result = "/" + result; } return result; } return filename; } function normalizeNodeInternals(filename) { if (filename.startsWith("node:internal")) { return "node:internal"; } if (filename.startsWith("node:")) { const parts = filename.split("/"); return parts[0]; } return filename; } function stripSystemPrefixes(path) { path = path.replace(/^\/Users\/[^/]+\//, "~/"); path = path.replace(/^\/home\/[^/]+\//, "~/"); path = path.replace(/^[A-Za-z]:[\\\/]Users[\\\/][^\\\/]+[\\\/]/, "~/"); return path; } function normalizeNodeModules(path) { const unixIndex = path.lastIndexOf("/node_modules/"); const winIndex = path.lastIndexOf("\\node_modules\\"); if (unixIndex !== -1) { return path.substring(unixIndex + 1); } if (winIndex !== -1) { return path.substring(winIndex + 1).replace(/\\/g, "/"); } return path; } function stripDeploymentPaths(path) { const deploymentPrefixes = [ /^\/var\/www\/[^/]+\//, // Apache/nginx: /var/www/myapp/ /^\/var\/task\//, // AWS Lambda: /var/task/ /^\/usr\/src\/app\//, // Docker: /usr/src/app/ /^\/app\//, // Heroku, Docker, generic: /app/ /^\/opt\/[^/]+\//, // Optional software: /opt/myapp/ /^\/srv\/[^/]+\// // Service data: /srv/myapp/ ]; for (const prefix of deploymentPrefixes) { path = path.replace(prefix, ""); } return path; } function findProjectPath(path) { const primaryMarkers = ["/src/", "/lib/", "/dist/", "/build/"]; const secondaryMarkers = [ "/app/", "/components/", "/pages/", "/api/", "/utils/", "/services/", "/modules/" ]; for (const marker of primaryMarkers) { const index = path.lastIndexOf(marker); if (index !== -1) { return path.substring(index + 1); } } for (const marker of secondaryMarkers) { const index = path.lastIndexOf(marker); if (index !== -1) { return path.substring(index + 1); } } return path; } function makeRelativePath(filename) { let result = filename; result = normalizeUrl(result); if (!result.startsWith("/") && !result.match(/^[A-Za-z]:\\/)) { if (result.startsWith("node:")) { return normalizeNodeInternals(result); } return result; } result = result.replace(/\\/g, "/"); if (result.startsWith("node:")) { return normalizeNodeInternals(result); } if (result.includes("/node_modules/")) { return normalizeNodeModules(result); } result = stripSystemPrefixes(result); result = stripDeploymentPaths(result); let cwd = null; try { if (typeof process !== "undefined" && typeof process.cwd === "function") { cwd = process.cwd(); } } catch { } if (cwd && result.startsWith(cwd)) { result = result.substring(cwd.length + 1); } if (result.startsWith("/") || result.match(/^[A-Za-z]:[/]/)) { result = findProjectPath(result); } else if (result.startsWith("~")) { const withoutTilde = result.substring(2); const projectPath = findProjectPath("/" + withoutTilde); if (projectPath !== "/" + withoutTilde) { result = projectPath; } } if (result.startsWith("/")) { result = result.substring(1); } return result; } function unwrapErrorCauses(error) { const chainedErrors = []; const seenErrors = /* @__PURE__ */ new Set(); let currentError = error.cause; let depth = 0; while (currentError && depth < MAX_EXCEPTION_CHAIN_DEPTH) { if (!(currentError instanceof Error)) { chainedErrors.push({ message: stringifyNonError(currentError), type: void 0 }); break; } if (seenErrors.has(currentError)) { break; } seenErrors.add(currentError); const chainedErrorData = { message: currentError.message || "", type: currentError.name || currentError.constructor?.name || "Error" }; if (currentError.stack) { chainedErrorData.stack = currentError.stack; chainedErrorData.frames = parseV8StackTrace(currentError.stack); } chainedErrors.push(chainedErrorData); currentError = currentError.cause; depth++; } return chainedErrors; } function isCallToolResult(value) { return value !== null && typeof value === "object" && "isError" in value && "content" in value && Array.isArray(value.content); } function captureCallToolResultError(result, _contextStack) { const message = result.content?.filter((c) => c.type === "text").map((c) => c.text).join(" ").trim() || "Unknown error"; const errorData = { message, type: void 0, // Can't determine actual type from CallToolResult platform: "javascript" // No stack or frames - SDK stripped the original error information }; return errorData; } function stringifyNonError(value) { if (value === null) { return "null"; } if (value === void 0) { return "undefined"; } if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } try { return JSON.stringify(value); } catch { return String(value); } } // src/modules/internal.ts var IdentityCache = class { constructor(maxSize = 1e3) { this.cache = /* @__PURE__ */ new Map(); this.maxSize = maxSize; } get(sessionId) { const entry = this.cache.get(sessionId); if (entry) { entry.timestamp = Date.now(); this.cache.delete(sessionId); this.cache.set(sessionId, entry); return entry.identity; } return void 0; } set(sessionId, identity) { this.cache.delete(sessionId); if (this.cache.size >= this.maxSize) { const oldestKey = this.cache.keys().next().value; if (oldestKey !== void 0) { this.cache.delete(oldestKey); } } this.cache.set(sessionId, { identity, timestamp: Date.now() }); } has(sessionId) { return this.cache.has(sessionId); } size() { return this.cache.size; } }; var _globalIdentityCache = new IdentityCache(1e3); var _serverTracking = /* @__PURE__ */ new WeakMap(); function getServerTrackingData(server) { return _serverTracking.get(server); } function setServerTrackingData(server, data) { _serverTracking.set(server, data); } function areIdentitiesEqual(a, b) { if (a.userId !== b.userId) return false; if (a.userName !== b.userName) return false; const aData = a.userData || {}; const bData = b.userData || {}; const aKeys = Object.keys(aData); const bKeys = Object.keys(bData); if (aKeys.length !== bKeys.length) return false; for (const key of aKeys) { if (!(key in bData)) return false; if (JSON.stringify(aData[key]) !== JSON.stringify(bData[key])) return false; } return true; } function mergeIdentities(previous, next) { if (!previous) { return next; } return { userId: next.userId, userName: next.userName, userData: { ...previous.userData || {}, ...next.userData || {} } }; } async function handleIdentify(server, data, request, extra) { if (!data.options.identify) { return; } const sessionId = data.sessionId; let identifyEvent = { sessionId, resourceName: request.params?.name || "Unknown", eventType: import_mcpcat_api2.PublishEventRequestEventTypeEnum.mcpcatIdentify, parameters: { request, extra }, timestamp: /* @__PURE__ */ new Date(), redactionFn: data.options.redactSensitiveInformation }; try { const identityResult = await data.options.identify(request, extra); if (identityResult) { const currentSessionId = data.sessionId; const previousIdentity = _globalIdentityCache.get(currentSessionId); const mergedIdentity = mergeIdentities(previousIdentity, identityResult); const hasChanged = !previousIdentity || !areIdentitiesEqual(previousIdentity, mergedIdentity); _globalIdentityCache.set(currentSessionId, mergedIdentity); data.identifiedSessions.set(data.sessionId, mergedIdentity); if (hasChanged) { writeToLog( `Identified session ${currentSessionId} with identity: ${JSON.stringify(mergedIdentity)}` ); publishEvent(server, identifyEvent); } } else { writeToLog( `Warning: Supplied identify function returned null for session ${sessionId}` ); } } catch (error) { writeToLog( `Warning: Supplied identify function threw an error while identifying session ${sessionId} - ${error}` ); identifyEvent.duration = identifyEvent.timestamp && (/* @__PURE__ */ new Date()).getTime() - identifyEvent.timestamp.getTime() || void 0; identifyEvent.isError = true; identifyEvent.error = captureException(error); publishEvent(server, identifyEvent); } } // src/modules/context-parameters.ts function addContextParameterToTool(tool, customContextDescription) { const modifiedTool = { ...tool }; const toolName = tool.name || "unknown"; const schema = modifiedTool.inputSchema; if (schema?.properties?.context) { writeToLog( `WARN: Tool "${toolName}" already has 'context' parameter. Skipping context injection.` ); return modifiedTool; } if (schema?.oneOf || schema?.allOf || schema?.anyOf) { writeToLog( `WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping context injection.` ); return modifiedTool; } if (!modifiedTool.inputSchema) { modifiedTool.inputSchema = { type: "object", properties: {}, required: [] }; } const contextDescription = customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION; modifiedTool.inputSchema = JSON.parse( JSON.stringify(modifiedTool.inputSchema) ); if (!modifiedTool.inputSchema.properties) { modifiedTool.inputSchema.properties = {}; } if (modifiedTool.inputSchema.additionalProperties === false) { delete modifiedTool.inputSchema.additionalProperties; } modifiedTool.inputSchema.properties.context = { type: "string", description: contextDescription }; if (Array.isArray(modifiedTool.inputSchema.required)) { if (!modifiedTool.inputSchema.required.includes("context")) { modifiedTool.inputSchema.required.push("context"); } } else { modifiedTool.inputSchema.required = ["context"]; } return modifiedTool; } function addContextParameterToTools(tools, customContextDescription) { return tools.map((tool) => { if (tool.name === "get_more_tools") { return tool; } return addContextParameterToTool(tool, customContextDescription); }); } // src/modules/tools.ts var import_mcpcat_api3 = require("mcpcat-api"); var GET_MORE_TOOLS_NAME = "get_more_tools"; function getReportMissingToolDescriptor() { return { name: GET_MORE_TOOLS_NAME, description: "Check for additional tools whenever your task might benefit from specialized capabilities - even if existing tools could work as a fallback.", inputSchema: { type: "object", properties: { context: { type: "string", description: "A description of your goal and what kind of tool would help accomplish it." } }, required: ["context"] } }; } function handleReportMissing(args) { writeToLog(`Missing tool reported: ${JSON.stringify(args)}`); return { content: [ { type: "text", text: `Unfortunately, we have shown you the full tool list. We have noted your feedback and will work to improve the tool list in the future.` } ] }; } function setupMCPCatTools(server) { const handlers = server._requestHandlers; const originalListToolsHandler = handlers.get("tools/list"); const originalCallToolHandler = handlers.get("tools/call"); if (!originalListToolsHandler || !originalCallToolHandler) { writeToLog( "Warning: Original tool handlers not found. Your tools may not be setup before MCPCat .track()." ); return; } try { server.setRequestHandler(import_types.ListToolsRequestSchema, async (request, extra) => { let tools = []; const data = getServerTrackingData(server); let event = { sessionId: getServerSessionId(server, extra), parameters: { request, extra }, eventType: import_mcpcat_api3.PublishEventRequestEventTypeEnum.mcpToolsList, timestamp: /* @__PURE__ */ new Date(), redactionFn: data?.options.redactSensitiveInformation }; try { const originalResponse = await originalListToolsHandler( request, extra ); tools = originalResponse.tools || []; } catch (error) { writeToLog( `Warning: Original list tools handler failed, this suggests an error MCPCat did not cause - ${error}` ); event.error = { message: getMCPCompatibleErrorMessage(error) }; event.isError = true; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); throw error; } if (!data) { writeToLog( "Warning: MCPCat is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls." ); return { tools }; } if (tools.length === 0) { writeToLog( "Warning: No tools found in the original list. This is likely due to the tools not being registered before MCPCat.track()." ); event.error = { message: "No tools were sent to MCP client." }; event.isError = true; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); return { tools }; } if (data.options.enableToolCallContext) { tools = addContextParameterToTools( tools, data.options.customContextDescription ); } if (data.options.enableReportMissing) { const alreadyPresent = tools.some( (t) => t?.name === GET_MORE_TOOLS_NAME ); if (!alreadyPresent) { tools.push(getReportMissingToolDescriptor()); } } event.response = { tools }; event.isError = false; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); return { tools }; }); } catch (error) { writeToLog(`Warning: Failed to override list tools handler - ${error}`); } } // src/modules/tracing.ts var import_types2 = require("@modelcontextprotocol/sdk/types.js"); var import_mcpcat_api4 = require("mcpcat-api"); function isToolResultError(result) { return result && typeof result === "object" && result.isError === true; } var listToolsTracingSetup = /* @__PURE__ */ new WeakMap(); function setupListToolsTracing(highLevelServer) { const server = highLevelServer.server; if (!server._capabilities?.tools) { return; } if (listToolsTracingSetup.get(server)) { return; } const handlers = server._requestHandlers; const originalListToolsHandler = handlers.get("tools/list"); if (!originalListToolsHandler) { return; } try { server.setRequestHandler(import_types2.ListToolsRequestSchema, async (request, extra) => { let tools = []; const data = getServerTrackingData(server); let event = { sessionId: getServerSessionId(server, extra), parameters: { request, extra }, eventType: import_mcpcat_api4.PublishEventRequestEventTypeEnum.mcpToolsList, timestamp: /* @__PURE__ */ new Date(), redactionFn: data?.options.redactSensitiveInformation }; try { const originalResponse = await originalListToolsHandler( request, extra ); tools = originalResponse.tools || []; if (data?.options.enableToolCallContext) { tools = addContextParameterToTools( tools, data.options.customContextDescription ); } if (data?.options.enableReportMissing) { const alreadyPresent = tools.some( (t) => t?.name === GET_MORE_TOOLS_NAME ); if (!alreadyPresent) tools.push(getReportMissingToolDescriptor()); } } catch (error) { writeToLog( `Warning: Original list tools handler failed, this suggests an error MCPCat did not cause - ${error}` ); event.error = { message: getMCPCompatibleErrorMessage(error) }; event.isError = true; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); throw error; } if (!data) { writeToLog( "Warning: MCPCat is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls." ); return { tools }; } if (tools.length === 0) { writeToLog( "Warning: No tools found in the original list. This is likely due to the tools not being registered before MCPCat.track()." ); event.error = { message: "No tools were sent to MCP client." }; event.isError = true; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); return { tools }; } event.response = { tools }; event.isError = false; event.duration = event.timestamp && (/* @__PURE__ */ new Date()).getTime() - event.timestamp.getTime() || 0; publishEvent(server, event); return { tools }; }); listToolsTracingSetup.set(server, true); } catch (error) { writeToLog(`Warning: Failed to override list tools handler - ${error}`); } } function setupInitializeTracing(highLevelServer)