mcpcat
Version:
Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights
1 lines • 133 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/modules/logging.ts","../src/modules/compatibility.ts","../src/modules/tools.ts","../src/modules/internal.ts","../src/modules/context-parameters.ts","../src/modules/eventQueue.ts","../src/thirdparty/ksuid/index.js","../src/thirdparty/ksuid/base-convert-int-array.js","../src/thirdparty/ksuid/base62.js","../package.json","../src/modules/constants.ts","../src/modules/session.ts","../src/modules/redaction.ts","../src/modules/tracing.ts","../src/modules/tracingV2.ts","../src/modules/exporters/trace-context.ts","../src/modules/exporters/otlp.ts","../src/modules/exporters/datadog.ts","../src/modules/exporters/sentry.ts","../src/modules/telemetry.ts"],"sourcesContent":["// Import our minimal interface from types\nimport {\n MCPCatOptions,\n MCPCatData,\n UserIdentity,\n MCPServerLike,\n HighLevelMCPServerLike,\n} from \"./types.js\";\n\n// Import from modules\nimport {\n isCompatibleServerType,\n isHighLevelServer,\n} from \"./modules/compatibility.js\";\nimport { writeToLog } from \"./modules/logging.js\";\nimport { setupMCPCatTools } from \"./modules/tools.js\";\nimport { setupToolCallTracing } from \"./modules/tracing.js\";\nimport { getSessionInfo, newSessionId } from \"./modules/session.js\";\nimport { setServerTrackingData } from \"./modules/internal.js\";\nimport { setupTracking } from \"./modules/tracingV2.js\";\nimport { TelemetryManager } from \"./modules/telemetry.js\";\nimport { setTelemetryManager } from \"./modules/eventQueue.js\";\n\n/**\n * Integrates MCPCat analytics into an MCP server to track tool usage patterns and user interactions.\n *\n * @param server - The MCP server instance to track. Must be a compatible MCP server implementation.\n * @param projectId - Your MCPCat project ID obtained from mcpcat.io when creating an account. Pass null for telemetry-only mode.\n * @param options - Optional configuration to customize tracking behavior.\n * @param options.enableReportMissing - Adds a \"get_more_tools\" tool that allows LLMs to automatically report missing functionality.\n * @param options.enableTracing - Enables tracking of tool calls and usage patterns.\n * @param options.enableToolCallContext - Injects a \"context\" parameter to existing tools to capture user intent.\n * @param options.identify - Async function to identify users and attach custom data to their sessions.\n * @param options.redactSensitiveInformation - Function to redact sensitive data before sending to MCPCat.\n * @param options.exporters - Configure telemetry exporters to send events to external systems. Available exporters:\n * - `otlp`: OpenTelemetry Protocol exporter (see {@link ../modules/exporters/otlp.OTLPExporter})\n * - `datadog`: Datadog APM exporter (see {@link ../modules/exporters/datadog.DatadogExporter})\n * - `sentry`: Sentry Monitoring exporter (see {@link ../modules/exporters/sentry.SentryExporter})\n *\n * @returns The tracked server instance.\n *\n * @remarks\n * Analytics data and debug information are logged to `~/mcpcat.log` since console logs interfere\n * with STDIO-based MCP servers.\n *\n * Do not call `track()` multiple times on the same server instance as this will cause unexpected behavior.\n *\n * @example\n * ```typescript\n * import * as mcpcat from \"mcpcat\";\n *\n * const mcpServer = new Server({ name: \"my-mcp-server\", version: \"1.0.0\" });\n *\n * // Track the server with MCPCat\n * mcpcat.track(mcpServer, \"proj_abc123xyz\");\n *\n * // Register your tools\n * mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({\n * tools: [{ name: \"my_tool\", description: \"Does something useful\" }]\n * }));\n * ```\n *\n * @example\n * ```typescript\n * // With user identification\n * mcpcat.track(mcpServer, \"proj_abc123xyz\", {\n * identify: async (request, extra) => {\n * const user = await getUserFromToken(request.params.arguments.token);\n * return {\n * userId: user.id,\n * userData: { plan: user.plan, company: user.company }\n * };\n * }\n * });\n * ```\n *\n * @example\n * ```typescript\n * // With sensitive data redaction\n * mcpcat.track(mcpServer, \"proj_abc123xyz\", {\n * redactSensitiveInformation: async (text) => {\n * return text.replace(/api_key_\\w+/g, \"[REDACTED]\");\n * }\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Telemetry-only mode (no MCPCat account required)\n * mcpcat.track(mcpServer, null, {\n * exporters: {\n * otlp: {\n * type: \"otlp\",\n * endpoint: \"http://localhost:4318/v1/traces\"\n * }\n * }\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Dual mode - send to both MCPCat and telemetry exporters\n * mcpcat.track(mcpServer, \"proj_abc123xyz\", {\n * exporters: {\n * datadog: {\n * type: \"datadog\",\n * apiKey: process.env.DD_API_KEY,\n * site: \"datadoghq.com\"\n * }\n * }\n * });\n * ```\n */\nfunction track(\n server: any,\n projectId: string | null,\n options: MCPCatOptions = {},\n): any {\n try {\n const validatedServer = isCompatibleServerType(server);\n\n // For high-level servers, we need to pass the underlying server to some functions\n const lowLevelServer = (\n isHighLevelServer(validatedServer)\n ? (validatedServer as any).server\n : validatedServer\n ) as MCPServerLike;\n\n // Initialize telemetry if exporters are configured\n if (options.exporters) {\n const telemetryManager = new TelemetryManager(options.exporters);\n setTelemetryManager(telemetryManager);\n writeToLog(\n `Initialized telemetry with ${Object.keys(options.exporters).length} exporters`,\n );\n }\n\n // If projectId is null and no exporters, warn the user\n if (!projectId && !options.exporters) {\n writeToLog(\n \"Warning: No projectId provided and no exporters configured. Events will not be sent anywhere.\",\n );\n }\n\n const sessionInfo = getSessionInfo(lowLevelServer, undefined);\n const mcpcatData: MCPCatData = {\n projectId: projectId || \"\", // Use empty string for null projectId\n sessionId: newSessionId(),\n lastActivity: new Date(),\n identifiedSessions: new Map<string, UserIdentity>(),\n sessionInfo,\n options: {\n enableReportMissing: options.enableReportMissing ?? true,\n enableTracing: options.enableTracing ?? true,\n enableToolCallContext: options.enableToolCallContext ?? true,\n identify: options.identify,\n redactSensitiveInformation: options.redactSensitiveInformation,\n },\n };\n\n setServerTrackingData(lowLevelServer, mcpcatData);\n if (isHighLevelServer(validatedServer)) {\n const highLevelServer = validatedServer as HighLevelMCPServerLike;\n setupTracking(highLevelServer);\n }\n\n if (mcpcatData.options.enableReportMissing) {\n try {\n setupMCPCatTools(lowLevelServer);\n } catch (error) {\n writeToLog(`Warning: Failed to setup report missing tool - ${error}`);\n }\n }\n\n if (mcpcatData.options.enableTracing) {\n try {\n // Pass the low-level server to the current tracing module\n setupToolCallTracing(lowLevelServer);\n } catch (error) {\n writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);\n }\n }\n\n return validatedServer;\n } catch (error) {\n writeToLog(`Warning: Failed to track server - ${error}`);\n return server;\n }\n}\n\nexport type {\n MCPCatOptions,\n UserIdentity,\n RedactFunction,\n ExporterConfig,\n Exporter,\n} from \"./types.js\";\n\nexport type IdentifyFunction = MCPCatOptions[\"identify\"];\n\nexport { track };\n","import { writeFileSync, appendFileSync, existsSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nconst LOG_FILE = join(homedir(), \"mcpcat.log\");\n\nexport function writeToLog(message: string): void {\n const timestamp = new Date().toISOString();\n const logEntry = `[${timestamp}] ${message}\\n`;\n\n try {\n if (!existsSync(LOG_FILE)) {\n writeFileSync(LOG_FILE, logEntry);\n } else {\n appendFileSync(LOG_FILE, logEntry);\n }\n } catch {\n // Silently fail to avoid breaking the server\n }\n}\n","import { HighLevelMCPServerLike, MCPServerLike } from \"../types.js\";\nimport { writeToLog } from \"./logging.js\";\n\n// Function to log compatibility information\nexport function logCompatibilityWarning(): void {\n writeToLog(\n \"MCPCat SDK Compatibility: This version supports MCP SDK versions v1.0 - v1.12\",\n );\n}\n\n// Check if server has high-level structure (wrapper with .server property)\nexport function isHighLevelServer(server: any): boolean {\n return (\n server &&\n typeof server === \"object\" &&\n server.server &&\n typeof server.server === \"object\"\n );\n}\n\n// Check if server has low-level structure (no .server property)\nexport function isLowLevelServer(server: any): boolean {\n return server && typeof server === \"object\" && !server.server;\n}\n\n// Type guard function that validates server compatibility and returns typed server\nexport function isCompatibleServerType(\n server: any,\n): MCPServerLike | HighLevelMCPServerLike {\n if (!server || typeof server !== \"object\") {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server must be an object.\",\n );\n }\n\n if (isHighLevelServer(server)) {\n // Validate high-level server requirements\n if (\n !server._registeredTools ||\n typeof server._registeredTools !== \"object\"\n ) {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: High-level server must have _registeredTools object.\",\n );\n }\n if (typeof server.tool !== \"function\") {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: High-level server must have tool() method.\",\n );\n }\n\n // Validate the underlying low-level server\n const targetServer = server.server;\n validateLowLevelServer(targetServer);\n\n return server as HighLevelMCPServerLike;\n } else {\n // Direct low-level server validation\n validateLowLevelServer(server);\n return server as MCPServerLike;\n }\n}\n\n// Helper function to validate low-level server requirements\nfunction validateLowLevelServer(server: any): void {\n if (typeof server.setRequestHandler !== \"function\") {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server must have a setRequestHandler method.\",\n );\n }\n\n if (!server._requestHandlers || !(server._requestHandlers instanceof Map)) {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server._requestHandlers is not accessible.\",\n );\n }\n\n // Validate that _requestHandlers contains functions with compatible signatures\n if (typeof server._requestHandlers.get !== \"function\") {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server._requestHandlers must be a Map with a get method.\",\n );\n }\n\n if (typeof server.getClientVersion !== \"function\") {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server.getClientVersion must be a function.\",\n );\n }\n\n if (\n !server._serverInfo ||\n typeof server._serverInfo !== \"object\" ||\n !server._serverInfo.name\n ) {\n logCompatibilityWarning();\n throw new Error(\n \"MCPCat SDK compatibility error: Server._serverInfo is not accessible or missing name.\",\n );\n }\n}\n\nexport function getMCPCompatibleErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n try {\n return JSON.stringify(error, Object.getOwnPropertyNames(error));\n } catch {\n return \"Unknown error\";\n }\n } else if (typeof error === \"string\") {\n return error;\n } else if (typeof error === \"object\" && error !== null) {\n return JSON.stringify(error);\n }\n return \"Unknown error\";\n}\n","import {\n ListToolsRequestSchema,\n ListToolsResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { MCPServerLike, UnredactedEvent } from \"../types.js\";\nimport { writeToLog } from \"./logging.js\";\nimport { getServerTrackingData } from \"./internal.js\";\nimport { addContextParameterToTools } from \"./context-parameters.js\";\nimport { publishEvent } from \"./eventQueue.js\";\nimport { getServerSessionId } from \"./session.js\";\nimport { PublishEventRequestEventTypeEnum } from \"mcpcat-api\";\nimport { getMCPCompatibleErrorMessage } from \"./compatibility.js\";\n\nexport async function handleReportMissing(args: {\n description: string;\n context?: string;\n}) {\n writeToLog(`Missing tool reported: ${JSON.stringify(args)}`);\n\n return {\n content: [\n {\n type: \"text\" as const,\n 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.`,\n },\n ],\n };\n}\n\nexport function setupMCPCatTools(server: MCPServerLike): void {\n // Store reference to original handlers - need to use the method name, not the schema\n const handlers = server._requestHandlers;\n\n const originalListToolsHandler = handlers.get(\"tools/list\");\n const originalCallToolHandler = handlers.get(\"tools/call\");\n\n if (!originalListToolsHandler || !originalCallToolHandler) {\n writeToLog(\n \"Warning: Original tool handlers not found. Your tools may not be setup before MCPCat .track().\",\n );\n return;\n }\n\n // Override tools list to include get_more_tools and add context parameter\n try {\n server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {\n let tools: any[] = [];\n const data = getServerTrackingData(server);\n let event: UnredactedEvent = {\n sessionId: getServerSessionId(server),\n parameters: {\n request: request,\n extra: extra,\n },\n eventType: PublishEventRequestEventTypeEnum.mcpToolsList,\n timestamp: new Date(),\n redactionFn: data?.options.redactSensitiveInformation,\n };\n try {\n const originalResponse = (await originalListToolsHandler(\n request,\n extra,\n )) as ListToolsResult;\n tools = originalResponse.tools || [];\n } catch (error) {\n // If original handler fails, start with empty tools\n writeToLog(\n `Warning: Original list tools handler failed, this suggests an error MCPCat did not cause - ${error}`,\n );\n event.error = { message: getMCPCompatibleErrorMessage(error) };\n event.isError = true;\n event.duration =\n (event.timestamp &&\n new Date().getTime() - event.timestamp.getTime()) ||\n 0;\n publishEvent(server, event);\n throw error;\n }\n\n if (!data) {\n writeToLog(\n \"Warning: MCPCat is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.\",\n );\n return { tools };\n }\n\n if (tools.length === 0) {\n writeToLog(\n \"Warning: No tools found in the original list. This is likely due to the tools not being registered before MCPCat.track().\",\n );\n event.error = { message: \"No tools were sent to MCP client.\" };\n event.isError = true;\n event.duration =\n (event.timestamp &&\n new Date().getTime() - event.timestamp.getTime()) ||\n 0;\n publishEvent(server, event);\n return { tools };\n }\n\n // Add context parameter to all existing tools if enableToolCallContext is true\n if (data.options.enableToolCallContext) {\n tools = addContextParameterToTools(tools);\n }\n\n // Add report_missing tool\n tools.push({\n name: \"get_more_tools\",\n description:\n \"Check for additional tools whenever your task might benefit from specialized capabilities - even if existing tools could work as a fallback.\",\n inputSchema: {\n type: \"object\",\n properties: {\n context: {\n type: \"string\",\n description:\n \"A description of your goal and what kind of tool would help accomplish it.\",\n },\n },\n required: [\"context\"],\n },\n });\n\n event.response = { tools };\n event.isError = false;\n event.duration =\n (event.timestamp && new Date().getTime() - event.timestamp.getTime()) ||\n 0;\n publishEvent(server, event);\n return { tools };\n });\n } catch (error) {\n writeToLog(`Warning: Failed to override list tools handler - ${error}`);\n }\n}\n","import { MCPCatData, MCPServerLike } from \"../types.js\";\n\n// Internal tracking storage\nconst _serverTracking = new WeakMap<MCPServerLike, MCPCatData>();\n\nexport function getServerTrackingData(\n server: MCPServerLike,\n): MCPCatData | undefined {\n return _serverTracking.get(server);\n}\n\nexport function setServerTrackingData(\n server: MCPServerLike,\n data: MCPCatData,\n): void {\n _serverTracking.set(server, data);\n}\n","import { RegisteredTool } from \"../types\";\n\nexport function addContextParameterToTool(\n tool: RegisteredTool,\n): RegisteredTool {\n if (!tool.inputSchema) {\n tool.inputSchema = {\n type: \"object\",\n properties: {},\n required: [],\n };\n }\n\n // Add context property if it doesn't exist\n if (!tool.inputSchema.properties?.context) {\n tool.inputSchema.properties.context = {\n type: \"string\",\n description:\n \"Describe why you are calling this tool and how it fits into your overall task\",\n };\n\n // Add context to required array if it exists\n if (\n Array.isArray(tool.inputSchema.required) &&\n !tool.inputSchema.required.includes(\"context\")\n ) {\n tool.inputSchema.required.push(\"context\");\n } else if (!tool.inputSchema.required) {\n tool.inputSchema.required = [\"context\"];\n }\n }\n\n return tool;\n}\n\nexport function addContextParameterToTools(\n tools: RegisteredTool[],\n): RegisteredTool[] {\n return tools.map((tool) => addContextParameterToTool(tool));\n}\n","import {\n Configuration,\n EventsApi,\n PublishEventRequest,\n PublishEventRequestEventTypeEnum,\n} from \"mcpcat-api\";\nimport { Event, UnredactedEvent, MCPServerLike } from \"../types.js\";\nimport { writeToLog } from \"./logging.js\";\nimport { getServerTrackingData } from \"./internal.js\";\nimport { getSessionInfo } from \"./session.js\";\nimport { redactEvent } from \"./redaction.js\";\nimport KSUID from \"../thirdparty/ksuid/index.js\";\nimport { getMCPCompatibleErrorMessage } from \"./compatibility.js\";\nimport { TelemetryManager } from \"./telemetry.js\";\n\nclass EventQueue {\n private queue: UnredactedEvent[] = [];\n private processing = false;\n private maxRetries = 3;\n private maxQueueSize = 10000; // Prevent unbounded growth\n private concurrency = 5; // Max parallel requests\n private activeRequests = 0;\n private apiClient: EventsApi;\n private telemetryManager?: TelemetryManager;\n\n constructor() {\n const config = new Configuration({ basePath: \"https://api.mcpcat.io\" });\n this.apiClient = new EventsApi(config);\n }\n\n setTelemetryManager(telemetryManager: TelemetryManager): void {\n this.telemetryManager = telemetryManager;\n }\n\n add(event: UnredactedEvent): void {\n // Drop oldest events if queue is full (or implement your preferred strategy)\n if (this.queue.length >= this.maxQueueSize) {\n writeToLog(\"Event queue full, dropping oldest event\");\n this.queue.shift();\n }\n\n this.queue.push(event);\n this.process();\n }\n\n private async process(): Promise<void> {\n if (this.processing) return;\n\n this.processing = true;\n\n while (this.queue.length > 0 && this.activeRequests < this.concurrency) {\n const event = this.queue.shift();\n if (event?.redactionFn) {\n // Redact sensitive information if a redaction function is provided\n try {\n const redactedEvent = await redactEvent(event, event.redactionFn);\n event.redactionFn = undefined; // Clear the function to avoid reprocessing\n Object.assign(event, redactedEvent);\n } catch (error) {\n writeToLog(`Failed to redact event: ${error}`);\n continue; // Skip this event if redaction fails\n }\n }\n\n if (event) {\n event.id = event.id || (await KSUID.withPrefix(\"evt\").random());\n this.activeRequests++;\n this.sendEvent(event as Event).finally(() => {\n this.activeRequests--;\n // Try to process more events\n this.process();\n });\n }\n }\n\n this.processing = false;\n }\n\n private toPublishEventRequest(event: Event): PublishEventRequest {\n return {\n // Core fields\n id: event.id,\n projectId: event.projectId,\n sessionId: event.sessionId,\n timestamp: event.timestamp,\n duration: event.duration,\n\n // Event data\n eventType: event.eventType as PublishEventRequestEventTypeEnum,\n resourceName: event.resourceName,\n parameters: event.parameters,\n response: event.response,\n userIntent: event.userIntent,\n isError: event.isError,\n error: event.error,\n\n // Actor fields\n identifyActorGivenId: event.identifyActorGivenId,\n identifyActorName: event.identifyActorName,\n identifyData: event.identifyActorData,\n\n // Session info\n ipAddress: event.ipAddress,\n sdkLanguage: event.sdkLanguage,\n mcpcatVersion: event.mcpcatVersion,\n serverName: event.serverName,\n serverVersion: event.serverVersion,\n clientName: event.clientName,\n clientVersion: event.clientVersion,\n\n // Legacy fields\n actorId: event.actorId || event.identifyActorGivenId,\n eventId: event.eventId,\n };\n }\n\n private async sendEvent(event: Event, retries = 0): Promise<void> {\n // Export to telemetry if configured (fire-and-forget)\n if (this.telemetryManager) {\n this.telemetryManager.export(event).catch((error) => {\n writeToLog(\n `Telemetry export error: ${getMCPCompatibleErrorMessage(error)}`,\n );\n });\n }\n\n // Send to MCPCat API if projectId is provided\n if (event.projectId) {\n try {\n const publishRequest = this.toPublishEventRequest(event);\n await this.apiClient.publishEvent({\n publishEventRequest: publishRequest,\n });\n writeToLog(\n `Successfully sent event ${event.id} | ${event.eventType} | ${event.projectId} | ${event.duration} ms | ${event.identifyActorGivenId || \"anonymous\"}`,\n );\n writeToLog(`Event details: ${JSON.stringify(event)}`);\n } catch (error) {\n writeToLog(\n `Failed to send event ${event.id}, retrying... [Error: ${getMCPCompatibleErrorMessage(error)}]`,\n );\n if (retries < this.maxRetries) {\n // Exponential backoff: 1s, 2s, 4s\n await this.delay(Math.pow(2, retries) * 1000);\n return this.sendEvent(event, retries + 1);\n }\n throw error;\n }\n }\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n // Get queue stats for monitoring\n getStats() {\n return {\n queueLength: this.queue.length,\n activeRequests: this.activeRequests,\n isProcessing: this.processing,\n };\n }\n\n // Graceful shutdown - wait for active requests\n async destroy(): Promise<void> {\n // Stop accepting new events\n this.add = () => {\n writeToLog(\"Queue is shutting down, event dropped\");\n };\n\n // Wait for queue to drain (with timeout)\n const timeout = 5000; // 5 seconds\n const start = Date.now();\n\n while (\n (this.queue.length > 0 || this.activeRequests > 0) &&\n Date.now() - start < timeout\n ) {\n await this.delay(100);\n }\n\n if (this.queue.length > 0) {\n writeToLog(\n `Shutting down with ${this.queue.length} events still in queue`,\n );\n }\n }\n}\n\nexport const eventQueue = new EventQueue();\nprocess.once(\"SIGINT\", () => eventQueue.destroy());\nprocess.once(\"SIGTERM\", () => eventQueue.destroy());\nprocess.once(\"beforeExit\", () => eventQueue.destroy());\n\nexport function setTelemetryManager(telemetryManager: TelemetryManager): void {\n eventQueue.setTelemetryManager(telemetryManager);\n}\n\nexport function publishEvent(\n server: MCPServerLike,\n eventInput: UnredactedEvent,\n): void {\n const data = getServerTrackingData(server);\n if (!data) {\n writeToLog(\n \"Warning: Server tracking data not found. Event will not be published.\",\n );\n return;\n }\n\n const sessionInfo = getSessionInfo(server, data);\n\n // Calculate duration if not provided\n const duration =\n eventInput.duration ||\n (eventInput.timestamp\n ? new Date().getTime() - eventInput.timestamp.getTime()\n : undefined);\n\n // Build complete Event object with all fields explicit\n const fullEvent: UnredactedEvent = {\n // Core fields (id will be generated later in the queue)\n id: eventInput.id || \"\",\n sessionId: eventInput.sessionId || data.sessionId,\n projectId: data.projectId,\n\n // Event metadata\n eventType: eventInput.eventType || \"\",\n timestamp: eventInput.timestamp || new Date(),\n duration: duration,\n\n // Session context from sessionInfo\n ipAddress: sessionInfo.ipAddress,\n sdkLanguage: sessionInfo.sdkLanguage,\n mcpcatVersion: sessionInfo.mcpcatVersion,\n serverName: sessionInfo.serverName,\n serverVersion: sessionInfo.serverVersion,\n clientName: sessionInfo.clientName,\n clientVersion: sessionInfo.clientVersion,\n\n // Actor information from sessionInfo\n identifyActorGivenId: sessionInfo.identifyActorGivenId,\n identifyActorName: sessionInfo.identifyActorName,\n identifyActorData: sessionInfo.identifyActorData,\n\n // Event-specific data from input\n resourceName: eventInput.resourceName,\n parameters: eventInput.parameters,\n response: eventInput.response,\n userIntent: eventInput.userIntent,\n isError: eventInput.isError,\n error: eventInput.error,\n\n // Preserve redaction function\n redactionFn: eventInput.redactionFn,\n };\n\n eventQueue.add(fullEvent);\n}\n","\"use strict\";\nimport { randomBytes } from \"node:crypto\";\nimport { inspect } from \"node:util\";\nimport { promisify } from \"node:util\";\nimport * as base62 from \"./base62.js\";\n\nconst customInspectSymbol = inspect.custom;\n\nconst asyncRandomBytes = promisify(randomBytes);\n\n// KSUID's epoch starts more recently so that the 32-bit number space gives a\n// significantly higher useful lifetime of around 136 years from March 2014.\n// This number (14e11) was picked to be easy to remember.\nconst EPOCH_IN_MS = 14e11;\n\nconst MAX_TIME_IN_MS = 1e3 * (2 ** 32 - 1) + EPOCH_IN_MS;\n\n// Timestamp is a uint32\nconst TIMESTAMP_BYTE_LENGTH = 4;\n\n// Payload is 16-bytes\nconst PAYLOAD_BYTE_LENGTH = 16;\n\n// KSUIDs are 20 bytes when binary encoded\nconst BYTE_LENGTH = TIMESTAMP_BYTE_LENGTH + PAYLOAD_BYTE_LENGTH;\n\n// The length of a KSUID when string (base62) encoded\nconst STRING_ENCODED_LENGTH = 27;\n\nconst TIME_IN_MS_ASSERTION =\n `Valid KSUID timestamps must be in milliseconds since ${new Date(0).toISOString()},\n no earlier than ${new Date(EPOCH_IN_MS).toISOString()} and no later than ${new Date(MAX_TIME_IN_MS).toISOString()}\n`\n .trim()\n .replace(/(\\n|\\s)+/g, \" \")\n .replace(/\\.000Z/g, \"Z\");\n\nconst VALID_ENCODING_ASSERTION = `Valid encoded KSUIDs are ${STRING_ENCODED_LENGTH} characters`;\n\nconst VALID_BUFFER_ASSERTION = `Valid KSUID buffers are ${BYTE_LENGTH} bytes`;\n\nconst VALID_PAYLOAD_ASSERTION = `Valid KSUID payloads are ${PAYLOAD_BYTE_LENGTH} bytes`;\n\nfunction fromParts(timeInMs, payload) {\n const timestamp = Math.floor((timeInMs - EPOCH_IN_MS) / 1e3);\n const timestampBuffer = Buffer.allocUnsafe(TIMESTAMP_BYTE_LENGTH);\n timestampBuffer.writeUInt32BE(timestamp, 0);\n\n return Buffer.concat([timestampBuffer, payload], BYTE_LENGTH);\n}\n\nconst bufferLookup = new WeakMap();\n\nclass KSUID {\n constructor(buffer) {\n if (!KSUID.isValid(buffer)) {\n throw new TypeError(VALID_BUFFER_ASSERTION);\n }\n\n bufferLookup.set(this, buffer);\n Object.defineProperty(this, \"buffer\", {\n enumerable: true,\n get() {\n return Buffer.from(buffer);\n },\n });\n }\n\n get raw() {\n return Buffer.from(bufferLookup.get(this).slice(0));\n }\n\n get date() {\n return new Date(1e3 * this.timestamp + EPOCH_IN_MS);\n }\n\n get timestamp() {\n return bufferLookup.get(this).readUInt32BE(0);\n }\n\n get payload() {\n const payload = bufferLookup\n .get(this)\n .slice(TIMESTAMP_BYTE_LENGTH, BYTE_LENGTH);\n return Buffer.from(payload);\n }\n\n get string() {\n const encoded = base62.encode(\n bufferLookup.get(this),\n STRING_ENCODED_LENGTH,\n );\n return encoded.padStart(STRING_ENCODED_LENGTH, \"0\");\n }\n\n compare(other) {\n if (!bufferLookup.has(other)) {\n return 0;\n }\n\n return bufferLookup\n .get(this)\n .compare(bufferLookup.get(other), 0, BYTE_LENGTH);\n }\n\n equals(other) {\n return (\n this === other || (bufferLookup.has(other) && this.compare(other) === 0)\n );\n }\n\n toString() {\n return `${this[Symbol.toStringTag]} { ${this.string} }`;\n }\n\n toJSON() {\n return this.string;\n }\n\n [customInspectSymbol]() {\n return this.toString();\n }\n\n static async random(time = Date.now()) {\n const payload = await asyncRandomBytes(PAYLOAD_BYTE_LENGTH);\n return new KSUID(fromParts(Number(time), payload));\n }\n\n static randomSync(time = Date.now()) {\n const payload = randomBytes(PAYLOAD_BYTE_LENGTH);\n return new KSUID(fromParts(Number(time), payload));\n }\n\n static fromParts(timeInMs, payload) {\n if (\n !Number.isInteger(timeInMs) ||\n timeInMs < EPOCH_IN_MS ||\n timeInMs > MAX_TIME_IN_MS\n ) {\n throw new TypeError(TIME_IN_MS_ASSERTION);\n }\n if (\n !Buffer.isBuffer(payload) ||\n payload.byteLength !== PAYLOAD_BYTE_LENGTH\n ) {\n throw new TypeError(VALID_PAYLOAD_ASSERTION);\n }\n\n return new KSUID(fromParts(timeInMs, payload));\n }\n\n static isValid(buffer) {\n return Buffer.isBuffer(buffer) && buffer.byteLength === BYTE_LENGTH;\n }\n\n static parse(string) {\n if (string.length !== STRING_ENCODED_LENGTH) {\n throw new TypeError(VALID_ENCODING_ASSERTION);\n }\n\n const decoded = base62.decode(string, BYTE_LENGTH);\n if (decoded.byteLength === BYTE_LENGTH) {\n return new KSUID(decoded);\n }\n\n const buffer = Buffer.allocUnsafe(BYTE_LENGTH);\n const padEnd = BYTE_LENGTH - decoded.byteLength;\n buffer.fill(0, 0, padEnd);\n decoded.copy(buffer, padEnd);\n return new KSUID(buffer);\n }\n}\nObject.defineProperty(KSUID.prototype, Symbol.toStringTag, { value: \"KSUID\" });\n// A string-encoded maximum value for a KSUID\nObject.defineProperty(KSUID, \"MAX_STRING_ENCODED\", {\n value: \"aWgEPTl1tmebfsQzFP4bxwgy80V\",\n});\n// A string-encoded minimum value for a KSUID\nObject.defineProperty(KSUID, \"MIN_STRING_ENCODED\", {\n value: \"000000000000000000000000000\",\n});\n\n// Add prefix functionality\nKSUID.withPrefix = function (prefix) {\n return {\n random: async (time = Date.now()) => {\n const ksuid = await KSUID.random(time);\n return `${prefix}_${ksuid.string}`;\n },\n randomSync: (time = Date.now()) => {\n const ksuid = KSUID.randomSync(time);\n return `${prefix}_${ksuid.string}`;\n },\n fromParts: (timeInMs, payload) => {\n const ksuid = KSUID.fromParts(timeInMs, payload);\n return `${prefix}_${ksuid.string}`;\n },\n };\n};\n\nexport default KSUID;\n","\"use strict\";\n\nconst maxLength = (array, from, to) =>\n Math.ceil((array.length * Math.log2(from)) / Math.log2(to));\n\nfunction baseConvertIntArray(array, { from, to, fixedLength = null }) {\n const length =\n fixedLength === null ? maxLength(array, from, to) : fixedLength;\n const result = new Array(length);\n\n // Each iteration prepends the resulting value, so start the offset at the end.\n let offset = length;\n let input = array;\n while (input.length > 0) {\n if (offset === 0) {\n throw new RangeError(\n `Fixed length of ${fixedLength} is too small, expected at least ${maxLength(array, from, to)}`,\n );\n }\n\n const quotients = [];\n let remainder = 0;\n\n for (const digit of input) {\n const acc = digit + remainder * from;\n const q = Math.floor(acc / to);\n remainder = acc % to;\n\n if (quotients.length > 0 || q > 0) {\n quotients.push(q);\n }\n }\n\n result[--offset] = remainder;\n input = quotients;\n }\n\n // Trim leading padding, unless length is fixed.\n if (fixedLength === null) {\n return offset > 0 ? result.slice(offset) : result;\n }\n\n // Fill in any holes in the result array.\n while (offset > 0) {\n result[--offset] = 0;\n }\n return result;\n}\nexport default baseConvertIntArray;\n","\"use strict\";\nimport baseConvertIntArray from \"./base-convert-int-array.js\";\n\nconst CHARS = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\nfunction encode(buffer, fixedLength) {\n return baseConvertIntArray(buffer, { from: 256, to: 62, fixedLength })\n .map((value) => CHARS[value])\n .join(\"\");\n}\n\nfunction decode(string, fixedLength) {\n // Optimization from https://github.com/andrew/base62.js/pull/31.\n const input = Array.from(string, (char) => {\n const charCode = char.charCodeAt(0);\n if (charCode < 58) return charCode - 48;\n if (charCode < 91) return charCode - 55;\n return charCode - 61;\n });\n return Buffer.from(\n baseConvertIntArray(input, { from: 62, to: 256, fixedLength }),\n );\n}\nexport { encode, decode };\n","{\n \"name\": \"mcpcat\",\n \"version\": \"0.1.3\",\n \"description\": \"Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights\",\n \"type\": \"module\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest\",\n \"test:compatibility\": \"vitest run src/tests/mcp-version-compatibility.test.ts\",\n \"lint\": \"eslint src/\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepare\": \"husky\",\n \"prepublishOnly\": \"pnpm run build && pnpm run test && pnpm run lint && pnpm run typecheck\"\n },\n \"keywords\": [\n \"ai\",\n \"authentication\",\n \"mcp\",\n \"observability\",\n \"ai-agents\",\n \"ai-platform\",\n \"ai-agent\",\n \"mcps\",\n \"aiagents\",\n \"ai-agent-tools\",\n \"mcp-servers\",\n \"mcp-server\",\n \"mcp-tools\",\n \"agent-runtime\",\n \"mcp-framework\",\n \"mcp-analytics\"\n ],\n \"author\": \"MCPcat\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/MCPCat/mcpcat-typescript-sdk.git\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/MCPCat/mcpcat-typescript-sdk/issues\"\n },\n \"homepage\": \"https://github.com/MCPCat/mcpcat-typescript-sdk#readme\",\n \"packageManager\": \"pnpm@10.11.0\",\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.29.4\",\n \"@modelcontextprotocol/sdk\": \"1.17.1\",\n \"@types/node\": \"^22.15.21\",\n \"@typescript-eslint/eslint-plugin\": \"^8.32.1\",\n \"@typescript-eslint/parser\": \"^8.32.1\",\n \"@vitest/coverage-v8\": \"^3.1.4\",\n \"@vitest/ui\": \"^3.1.4\",\n \"eslint\": \"^9.27.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.1.0\",\n \"prettier\": \"^3.5.3\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.8.3\",\n \"vitest\": \"^3.1.4\"\n },\n \"peerDependencies\": {\n \"@modelcontextprotocol/sdk\": \">=1.3.1\"\n },\n \"dependencies\": {\n \"@opentelemetry/otlp-transformer\": \"^0.203.0\",\n \"mcpcat-api\": \"0.1.3\",\n \"redact-pii\": \"3.4.0\",\n \"zod\": \"3.25.30\"\n },\n \"lint-staged\": {\n \"*.{ts,js}\": [\n \"eslint --fix\",\n \"prettier --write\"\n ],\n \"*.{json,md,yml,yaml}\": [\n \"prettier --write\"\n ]\n }\n}\n","// MCPCat Settings\nexport const INACTIVITY_TIMEOUT_IN_MINUTES = 30;\n","import {\n MCPCatData,\n MCPServerLike,\n ServerClientInfoLike,\n SessionInfo,\n} from \"../types.js\";\nimport { getServerTrackingData, setServerTrackingData } from \"./internal.js\";\nimport KSUID from \"../thirdparty/ksuid/index.js\";\nimport packageJson from \"../../package.json\" with { type: \"json\" };\n\nimport { INACTIVITY_TIMEOUT_IN_MINUTES } from \"./constants.js\";\n\nexport function newSessionId(): string {\n return KSUID.withPrefix(\"ses\").randomSync();\n}\n\nexport function getServerSessionId(server: MCPServerLike): string {\n const data = getServerTrackingData(server);\n\n if (!data) {\n throw new Error(\"Server tracking data not found\");\n }\n\n const now = Date.now();\n const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1000;\n // If last activity timed out\n if (now - data.lastActivity.getTime() > timeoutMs) {\n data.sessionId = newSessionId();\n setServerTrackingData(server, data);\n }\n setLastActivity(server);\n\n return data.sessionId;\n}\n\nexport function setLastActivity(server: MCPServerLike): void {\n const data = getServerTrackingData(server);\n\n if (!data) {\n throw new Error(\"Server tracking data not found\");\n }\n\n data.lastActivity = new Date();\n setServerTrackingData(server, data);\n}\n\nexport function getSessionInfo(\n server: MCPServerLike,\n data: MCPCatData | undefined,\n): SessionInfo {\n let clientInfo: ServerClientInfoLike | undefined = {\n name: undefined,\n version: undefined,\n };\n if (!data?.sessionInfo.clientName) {\n clientInfo = server.getClientVersion();\n }\n const actorInfo = data?.identifiedSessions.get(data.sessionId);\n\n const sessionInfo: SessionInfo = {\n ipAddress: undefined, // grab from django\n sdkLanguage: \"TypeScript\", // hardcoded for now\n mcpcatVersion: packageJson.version,\n serverName: server._serverInfo?.name,\n serverVersion: server._serverInfo?.version,\n clientName: clientInfo?.name,\n clientVersion: clientInfo?.version,\n identifyActorGivenId: actorInfo?.userId,\n identifyActorName: actorInfo?.userName,\n identifyActorData: actorInfo?.userData || {},\n };\n\n if (!data) {\n return sessionInfo;\n }\n\n data.sessionInfo = sessionInfo;\n setServerTrackingData(server, data);\n return data.sessionInfo;\n}\n","import { Event, RedactFunction, UnredactedEvent } from \"../types.js\";\n\n/**\n * Set of field names that should be protected from redaction.\n * These fields contain system-level identifiers and metadata that\n * need to be preserved for analytics tracking.\n */\nconst PROTECTED_FIELDS = new Set([\n \"sessionId\",\n \"id\",\n \"projectId\",\n \"server\",\n \"identifyActorGivenId\",\n \"identifyActorName\",\n \"identifyData\",\n \"resourceName\",\n \"eventType\",\n \"actorId\",\n]);\n\n/**\n * Recursively applies a redaction function to all string values in an object.\n * This ensures that sensitive information is removed from all string fields\n * before events are sent to the analytics service.\n *\n * @param obj - The object to redact strings from\n * @param redactFn - The redaction function to apply to each string\n * @param path - The current path in the object tree (used to check protected fields)\n * @param isProtected - Whether the current object/value is within a protected field\n * @returns A new object with all strings redacted\n */\nasync function redactStringsInObject(\n obj: any,\n redactFn: RedactFunction,\n path: string = \"\",\n isProtected: boolean = false,\n): Promise<any> {\n if (obj === null || obj === undefined) {\n return obj;\n }\n\n // Handle strings\n if (typeof obj === \"string\") {\n // Don't redact if this field or any parent field is protected\n if (isProtected) {\n return obj;\n }\n return await redactFn(obj);\n }\n\n // Handle arrays\n if (Array.isArray(obj)) {\n return Promise.all(\n obj.map((item, index) =>\n redactStringsInObject(item, redactFn, `${path}[${index}]`, isProtected),\n ),\n );\n }\n\n // Handle dates (don't redact)\n if (obj instanceof Date) {\n return obj;\n }\n\n // Handle objects\n if (typeof obj === \"object\") {\n const redactedObj: any = {};\n\n for (const [key, value] of Object.entries(obj)) {\n // Skip functions and undefined values\n if (typeof value === \"function\" || value === undefined) {\n continue;\n }\n\n // Build the path for nested fields\n const fieldPath = path ? `${path}.${key}` : key;\n // Check if this field is protected (only check at top level)\n const isFieldProtected =\n isProtected || (path === \"\" && PROTECTED_FIELDS.has(key));\n redactedObj[key] = await redactStringsInObject(\n value,\n redactFn,\n fieldPath,\n isFieldProtected,\n );\n }\n\n return redactedObj;\n }\n\n // For all other types (numbers, booleans, etc.), return as-is\n return obj;\n}\n\n/**\n * Applies the customer's redaction function to all string fields in an Event object.\n * This is the main entry point for redacting sensitive information from events\n * before they are sent to the analytics service.\n *\n * @param event - The event to redact\n * @param redactFn - The customer's redaction function\n * @returns A new event object with all strings redacted\n */\nexport async function redactEvent(\n event: UnredactedEvent,\n redactFn: RedactFunction,\n): Promise<Event> {\n return redactStringsInObject(event, redactFn, \"\", false) as Promise<Event>;\n}\n","import {\n CallToolRequestSchema,\n InitializeRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { MCPServerLike, UnredactedEvent } from \"../types.js\";\nimport { writeToLog } from \"./logging.js\";\nimport { handleReportMissing } from \"./tools.js\";\nimport { getServerTrackingData } from \"./internal.js\";\nimport { getServerSessionId } from \"./session.js\";\nimport { PublishEventRequestEventTypeEnum } from \"mcpcat-api\";\nimport { publishEvent } from \"./eventQueue.js\";\nimport { getMCPCompatibleErrorMessage } from \"./compatibility.js\";\n\nfunction isToolResultError(result: any): boolean {\n return result && typeof result === \"object\" && result.isError === true;\n}\n\nexport function setupToolCallTracing(server: MCPServerLike): void {\n try {\n const handlers = server._requestHandlers;\n\n const originalCallToolHandler = handlers.get(\"tools/call\");\n const originalInitializeHandler = handlers.get(\"initialize\");\n\n if (originalInitializeHandler) {\n server.setRequestHandler(\n InitializeRequestSchema,\n async (request, extra) => {\n const data = getServerTrackingData(server);\n if (!data) {\n writeToLog(\n \"Warning: MCPCat is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.\",\n );\n return await originalInitializeHandler(request, extra);\n }\n\n const sessionId = getServerSessionId(server);\n let event: UnredactedEvent = {\n sessionId: sessionId,\n resourceName: request.params?.name || \"Unknown Tool Name\",\n eventType: PublishEventRequestEventTypeEnum.mcpInitialize,\n parameters: {\n request: request,\n extra: extra,\n },\n timestamp: new Date(),\n redactionFn: data.options.redactSensitiveInformation,\n };\n const result = await originalInitializeHandler(request, extra);\n event.response = result;\n publishEvent(server, event);\n return result;\n },\n );\n }\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n const data = getServerTrackingData(server);\n if (!data) {\n writeToLog(\n \"Warning: MCPCat is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.\",\n );\n return await originalCallToolHandler?.(request, extra);\n }\n\n const sessionId = getServerSessionId(server);\n let event: UnredactedEvent = {\n sessionId: sessionId,\n resourceName: request.params?.name || \"Unknown Tool Name\",\n parameters: {\n request: request,\n extra: extra,\n },\n eventType: PublishEventRequestEventTypeEnum.mcpToolsCall,\n timestamp: new Date(),\n redactionFn: data.options.redactSensitiveInformation,\n };\n\n try {\n // Try to identify the session if we haven't already and identify function is provided\n if (\n data.options.identify &&\n data.identifiedSessions.get(sessionId) === undefined\n ) {\n let identifyEvent: UnredactedEvent = {\n ...event,\n eventType: PublishEventRequestEventTypeEnum.mcpcatIdentify,\n };\n try {\n const identityResult = await data.options.identify(request, extra);\n if (identityResult) {\n writeToLog(\n `Identified session ${sessionId} with identity: ${JSON.stringify(identityResult)}`,\n );\n data.identifiedSessions.set(sessionId, identityResult);\n publishEvent(server, identifyEvent);\n } else {\n writeToLog(\n `Warning: Supplied identify function returned null for session ${sessionId}`,\n );\n }\n } catch (error) {\n writeToLog(\n `Warning: Supplied identify function threw an error while identifying session ${sessionId} - ${error}`,\n );\n identifyEvent.duration =\n (identifyEvent.timestamp &&\n new Date().getTime() - identifyEvent.timestamp.getTime()) ||\n undefined;\n identifyEvent.isError = true;\n identifyEvent.error = {\n message: getMCPCompatibleErrorMessage(error),\n };\n publishEvent(server, identifyEvent);\n }\n }\n\n // Check for missing context if enableToolCallContext is true and it's not report_missing\n if (\n data.options.enableToolCallContext &&\n request.params?.name !== \"get_more_tools\"\n ) {\n const hasContext =\n request.params?.arguments &&\n typeof request.params.arguments === \"object\" &&\n \"context\" in request.params.arguments;\n if (hasContext) {\n event.userIntent = request.params.arguments.context;\n }\n }\n\n let result;\n if (request.params?.name === \"get_more_tools\") {\n result = await handleReportMissing(request.params.arguments as any);\n event.userIntent = request.params.arguments.context;\n } else if (originalCallToolHandler) {\n result = await originalCallToolHandler(request, extra);\n } else {\n event.isError = true;\n event.error = {\n message: `Tool call handler not found for ${request.params?.name || \"unknown\"}`,\n };\n event.duration =\n (event.timestamp &&\n new Date().getTime() - event.timestamp.getTime()) ||\n undefined;\n publishEvent(server, event);\n throw new Error(`Unknown tool: ${request.params?.name || \"unknown\"}`);\n }\n\n // Check if the result indicates an error\n if (isToolResultError(result)) {\n event.isError = true;\n event.error = {\n message: getMCPCompatibleErrorMessage(result),\n };\n }\n\n event.response = result;\n publishEvent(server, event);\n return result;\n } catch (error) {\n event.isError = true;\n event.error = {\n message: getMCPCompatibleErrorMessage(error),\n };\n publishEvent(server, event);\n throw error;\n }\n });\n } catch (error) {\n writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);\n throw error;\n }\n}\n","import { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n HighLevelMCPServerLike,\n MCPServerLike,\n UnredactedEvent,\n RegisteredTool,\n CompatibleRequestHandlerExtra,\n} from \"../types.js\";\nimport { writeToLog } from \"./logging.js\";\nimport { getServerTrackingData } from \"./internal.js\";\nimport { getServerSessionId } from \"./session.js\";\nimport { PublishEventRequestEventTypeE