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
JavaScript
"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)