From 9cf0447e5508ae93a3084d9a9ec6082086e29603 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 16 Apr 2025 16:43:05 +0100 Subject: [PATCH 01/46] wip --- package-lock.json | 7 ++ package.json | 1 + src/index.ts | 55 +++++++++- src/server.ts | 6 ++ src/types/@mongodb-js/get-os-info.d.ts | 8 ++ tracking-plan-mcp.md | 139 +++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 src/types/@mongodb-js/get-os-info.d.ts create mode 100644 tracking-plan-mcp.md diff --git a/package-lock.json b/package-lock.json index eb36763b..52fd24d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "@mongodb-js/devtools-connect": "^3.7.2", + "@mongodb-js/get-os-info": "^0.4.1", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", "mongodb": "^6.15.0", @@ -3114,6 +3115,12 @@ "system-ca": "^2.0.1" } }, + "node_modules/@mongodb-js/get-os-info": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/get-os-info/-/get-os-info-0.4.1.tgz", + "integrity": "sha512-K/rP3xjxfUOJl52EdrKFcYCVc8I/LH6bdFLorPIdXY/DByBSnbZgnwPy6mp5pHdkL3Bp01YCBcJeRLCn/4+frQ==", + "license": "Apache-2.0" + }, "node_modules/@mongodb-js/mongodb-downloader": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.9.tgz", diff --git a/package.json b/package.json index b2ca5adc..bc62b454 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "@mongodb-js/devtools-connect": "^3.7.2", + "@mongodb-js/get-os-info": "^0.4.1", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", "mongodb": "^6.15.0", diff --git a/src/index.ts b/src/index.ts index 944ee92a..4d3bd3cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,38 @@ import config from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; -try { +const POLL_INTERVAL_MS = 2000; // 2 seconds +const MAX_RETRIES = 15; // 30 seconds total +const CLIENT_VERSION_TIMEOUT = new Error('Timeout waiting for client version'); + +async function pollClientVersion(mcpServer: McpServer): Promise { + let attempts = 0; + + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + try { + const client = await mcpServer.server.getClientVersion(); + if (client?.name && client?.version) { + clearInterval(interval); + logger.info( + mongoLogId(1_000_003), + "server", + `Connected to client: ${client.name} v${client.version}` + ); + resolve(); + } else if (++attempts >= MAX_RETRIES) { + clearInterval(interval); + reject(CLIENT_VERSION_TIMEOUT); + } + } catch (error: unknown) { + clearInterval(interval); + reject(error); + } + }, POLL_INTERVAL_MS); + }); +} + +async function main() { const session = new Session(); const mcpServer = new McpServer({ name: "MongoDB Atlas", @@ -23,8 +54,26 @@ try { const transport = new StdioServerTransport(); await server.connect(transport); -} catch (error: unknown) { - logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`); + try { + await pollClientVersion(mcpServer); + } catch (error) { + logger.warning( + mongoLogId(1_000_006), + "server", + "Client version information unavailable after 30 seconds" + ); + + } +} +// Start the server +try { + await main(); +} catch (error: unknown) { + logger.emergency( + mongoLogId(1_000_004), + "server", + `Fatal error running server: ${error as string}` + ); process.exit(1); } diff --git a/src/server.ts b/src/server.ts index 72d9c4f9..284c942a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,6 +5,7 @@ import { AtlasTools } from "./tools/atlas/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; +import { getOsInfo } from '@mongodb-js/get-os-info'; export class Server { public readonly session: Session; @@ -18,6 +19,11 @@ export class Server { async connect(transport: Transport) { this.mcpServer.server.registerCapabilities({ logging: {} }); + + // log telemetry + const osInfo = await getOsInfo(); + logger.info(mongoLogId(1_000_005), "server", `Server started with osInfo ${JSON.stringify(osInfo)}`); + this.registerTools(); await initializeLogger(this.mcpServer); diff --git a/src/types/@mongodb-js/get-os-info.d.ts b/src/types/@mongodb-js/get-os-info.d.ts new file mode 100644 index 00000000..3229fa42 --- /dev/null +++ b/src/types/@mongodb-js/get-os-info.d.ts @@ -0,0 +1,8 @@ +declare module '@mongodb-js/get-os-info' { + export function getOsInfo(): Promise<{ + platform: string; + arch: string; + version: string; + release: string; + }>; +} diff --git a/tracking-plan-mcp.md b/tracking-plan-mcp.md new file mode 100644 index 00000000..435af873 --- /dev/null +++ b/tracking-plan-mcp.md @@ -0,0 +1,139 @@ +# Atlas MCP Server Tracking Plan + +Generated on April 16, 2025 + +## MDB MCP Event + +All events in the MCP server follow this base structure with common properties. Specific event types will have additional properties as detailed below. + +**Base Properties**: + +- **timestamp** (required): `string` + - ISO 8601 timestamp when the event occurred +- **machine_id** (required): `string` + - Unique anonymous identifier of the machine +- **mcp\_server\_version** (required): `string` + - The version of the MCP server. +- **mcp\_server\_name** (required): `string` + - The name of the MCP server. +- **mcp\_client\_version** (required): `string` + - The version of the MCP agent. +- **mcp\_client\_name** (required): `string` + - The name of the agent calling the MCP server. (e.g. claude) +- **platform** (required): `string` + - The platform on which the MCP server is running. +- **arch** (required): `string` + - The architecture of the system's processor. +- **os\_type** (optional): `string | undefined` + - The type of operating system. +- **os\_version** (optional): `string | undefined` + - Detailed kernel or system version information. +- **os\_linux\_dist** (optional): `string | undefined` + - The Linux distribution name, if applicable. +- **component** (required): `string` + - The component generating the event (e.g., "server", "tool", "atlas", "mongodb") +- **action** (required): `string` + - The specific action being performed + +## Events per component + +### Server Component + +#### Server Lifecycle +**component**: `"server"` +**action**: `"lifecycle"` + +**Additional Properties**: +- **state** (required): `"start" | "stop"` + - The lifecycle state change +- **startup_time_ms** (optional): `number` + - Time taken for the server to start. Present when state is "start". +- **connected_services** (optional): `string[] | undefined` + - List of services connected at launch. Present when state is "start". +- **runtime_duration_ms** (optional): `number` + - The total runtime duration. Present when state is "stop". +- **exit_code** (optional): `number` + - The exit code. Present when state is "stop". +- **reason** (optional): `string | undefined` + - The stop reason (e.g., "normal", "error", "timeout"). Present when state is "stop". + +#### Tool Registration +**component**: `"tool"` +**action**: `"register" | "deregister"` + +**Additional Properties**: +- **count** (required): `number` + - The number of tools registered +- **tool_list** (required): `string[]` + - List of tools registered + + +#### Tool Call +**component**: `"tool"` +**action**: `"call"` + +**Additional Properties**: +- **name** (required): `string` + - The name of the tool +- **target** (required): `"mongodb" | "atlas"` + - The service being targeted by the tool +- **operation** (required): `string` + - The type of operation being performed + - For MongoDB: `"query" | "aggregation" | "insert" | "update" | "delete" | "index" | "metadata" | "connect"` + - For Atlas: `"list_clusters" | "list_projects" | "create_cluster" | "manage_access_list" | "manage_database_user" | "connect"` +- **duration_ms** (required): `number` + - Execution time in milliseconds +- **success** (required): `boolean` + - Whether the call succeeded +- **error_code** (optional): `string | undefined` + - Error code if operation failed +- **error_type** (optional): `string | undefined` + - Type of error if operation failed +- **state** (optional): `"attempt" | "success" | "failure"` + - Connection state (when operation is "connect") +- **doc_count** (optional): `number | undefined` + - Number of affected documents (MongoDB only) +- **database** (optional): `string | undefined` + - Target database name (MongoDB only) +- **collection** (optional): `string | undefined` + - Target collection name (MongoDB only) +- **connection_id** (optional): `string | undefined` + - Connection identifier (required when operation is "connect") +- **project_id** (optional): `string | undefined` + - Atlas project ID (Atlas only) +- **org_id** (optional): `string | undefined` + - Atlas organization ID (Atlas only) +- **cluster_name** (optional): `string | undefined` + - Target cluster name (Atlas only) +- **is_atlas** (optional): `boolean | undefined` + - Whether using Atlas connection string (when operation is "connect") + +#### Error +**component**: `"error"` +**action**: `"occur"` + +**Additional Properties**: +- **code** (required): `number` + - The error code +- **context** (required): `string` + - Where the error occurred (e.g., "auth", "tool", "connection") +- **type** (required): `string` + - Error category or type + +### Authentication Component + +#### Authentication +**component**: `"auth"` +**action**: `"authenticate"` + +**Additional Properties**: +- **target** (required): `"mongodb" | "atlas"` + - The service being authenticated with +- **success** (required): `boolean` + - Whether authentication succeeded +- **method** (required): `string` + - Authentication method used +- **duration_ms** (required): `number` + - Time taken to authenticate +- **error_code** (optional): `string | undefined` + - Error code if failed \ No newline at end of file From 1b8ea02f949d00f83b4c2f1b42970b4ef890635a Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 17 Apr 2025 00:20:38 +0100 Subject: [PATCH 02/46] WIP --- package-lock.json | 7 -- package.json | 1 - src/common/atlas/apiClient.ts | 5 +- src/config.ts | 11 ++ src/index.ts | 40 ------- src/server.ts | 16 +-- src/session.ts | 13 +++ src/tools/atlas/atlasTool.ts | 1 + src/tools/tool.ts | 3 +- src/types/@mongodb-js/get-os-info.d.ts | 8 -- tracking-plan-mcp.md | 139 ------------------------- 11 files changed, 38 insertions(+), 206 deletions(-) delete mode 100644 src/types/@mongodb-js/get-os-info.d.ts delete mode 100644 tracking-plan-mcp.md diff --git a/package-lock.json b/package-lock.json index 52fd24d1..eb36763b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "@mongodb-js/devtools-connect": "^3.7.2", - "@mongodb-js/get-os-info": "^0.4.1", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", "mongodb": "^6.15.0", @@ -3115,12 +3114,6 @@ "system-ca": "^2.0.1" } }, - "node_modules/@mongodb-js/get-os-info": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/get-os-info/-/get-os-info-0.4.1.tgz", - "integrity": "sha512-K/rP3xjxfUOJl52EdrKFcYCVc8I/LH6bdFLorPIdXY/DByBSnbZgnwPy6mp5pHdkL3Bp01YCBcJeRLCn/4+frQ==", - "license": "Apache-2.0" - }, "node_modules/@mongodb-js/mongodb-downloader": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.9.tgz", diff --git a/package.json b/package.json index bc62b454..b2ca5adc 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "@mongodb-js/devtools-connect": "^3.7.2", - "@mongodb-js/get-os-info": "^0.4.1", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", "mongodb": "^6.15.0", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index b784e43e..9aab2cea 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,5 +1,6 @@ import config from "../../config.js"; -import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch"; +import createClient, { Client, Middleware } from "openapi-fetch"; +import type { FetchOptions } from "openapi-fetch"; import { AccessToken, ClientCredentials } from "simple-oauth2"; import { ApiClientError } from "./apiClientError.js"; import { paths, operations } from "./openapi.js"; @@ -65,7 +66,7 @@ export class ApiClient { baseUrl: options?.baseUrl || "https://cloud.mongodb.com/", userAgent: options?.userAgent || - `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, + `${config.mcp_server_name}/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }; this.client = createClient({ diff --git a/src/config.ts b/src/config.ts index f5f18ca5..350e0305 100644 --- a/src/config.ts +++ b/src/config.ts @@ -39,9 +39,20 @@ const mergedUserConfig = { ...getCliConfig(), }; + +const machineMetadata = { + device_id: "id", // TODO: use @mongodb-js/machine-id + platform: process.platform, + arch: process.arch, + os_type: process.platform, + os_version: process.version, +} + const config = { ...mergedUserConfig, + ...machineMetadata, version: packageJson.version, + mcp_server_name: "MdbMcpServer" }; export default config; diff --git a/src/index.ts b/src/index.ts index 4d3bd3cd..5a0c354e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,36 +8,6 @@ import config from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; -const POLL_INTERVAL_MS = 2000; // 2 seconds -const MAX_RETRIES = 15; // 30 seconds total -const CLIENT_VERSION_TIMEOUT = new Error('Timeout waiting for client version'); - -async function pollClientVersion(mcpServer: McpServer): Promise { - let attempts = 0; - - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - try { - const client = await mcpServer.server.getClientVersion(); - if (client?.name && client?.version) { - clearInterval(interval); - logger.info( - mongoLogId(1_000_003), - "server", - `Connected to client: ${client.name} v${client.version}` - ); - resolve(); - } else if (++attempts >= MAX_RETRIES) { - clearInterval(interval); - reject(CLIENT_VERSION_TIMEOUT); - } - } catch (error: unknown) { - clearInterval(interval); - reject(error); - } - }, POLL_INTERVAL_MS); - }); -} async function main() { const session = new Session(); @@ -54,16 +24,6 @@ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); - try { - await pollClientVersion(mcpServer); - } catch (error) { - logger.warning( - mongoLogId(1_000_006), - "server", - "Client version information unavailable after 30 seconds" - ); - - } } // Start the server diff --git a/src/server.ts b/src/server.ts index 284c942a..882fe653 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,7 +5,7 @@ import { AtlasTools } from "./tools/atlas/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; -import { getOsInfo } from '@mongodb-js/get-os-info'; + export class Server { public readonly session: Session; @@ -18,19 +18,19 @@ export class Server { async connect(transport: Transport) { this.mcpServer.server.registerCapabilities({ logging: {} }); - - - // log telemetry - const osInfo = await getOsInfo(); - logger.info(mongoLogId(1_000_005), "server", `Server started with osInfo ${JSON.stringify(osInfo)}`); - this.registerTools(); await initializeLogger(this.mcpServer); await this.mcpServer.connect(transport); - logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); + this.mcpServer.server.oninitialized = () => { + const client = this.mcpServer.server.getClientVersion(); + this.session.clientName = client?.name; + this.session.clientVersion = client?.version; + + logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); + }; } async close(): Promise { diff --git a/src/session.ts b/src/session.ts index 0d5ac951..450b9c41 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,10 +1,15 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { ApiClient } from "./common/atlas/apiClient.js"; import config from "./config.js"; +import logger from "./logger.js"; +import { mongoLogId } from "mongodb-log-writer"; export class Session { + sessionId?: string; serviceProvider?: NodeDriverServiceProvider; apiClient?: ApiClient; + clientName?: string; + clientVersion?: string; ensureAuthenticated(): asserts this is { apiClient: ApiClient } { if (!this.apiClient) { @@ -34,4 +39,12 @@ export class Session { this.serviceProvider = undefined; } } + + async emitTelemetry(todo: unknown): Promise { + logger.info( + mongoLogId(1_000_001), + "telemetry", + `Telemetry event: ${JSON.stringify(todo)}` + ); + } } diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 74683d75..ea6fdae9 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -2,6 +2,7 @@ import { ToolBase, ToolCategory } from "../tool.js"; import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { + protected category = "atlas"; constructor(protected readonly session: Session) { super(session); } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 0fe6e80f..da5cbb8c 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -22,6 +22,8 @@ export abstract class ToolBase { protected abstract argsShape: ZodRawShape; + protected abstract category: string; + protected abstract execute(...args: Parameters>): Promise; protected constructor(protected session: Session) {} @@ -33,7 +35,6 @@ export abstract class ToolBase { const callback: ToolCallback = async (...args) => { try { - // TODO: add telemetry here logger.debug( mongoLogId(1_000_006), "tool", diff --git a/src/types/@mongodb-js/get-os-info.d.ts b/src/types/@mongodb-js/get-os-info.d.ts deleted file mode 100644 index 3229fa42..00000000 --- a/src/types/@mongodb-js/get-os-info.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare module '@mongodb-js/get-os-info' { - export function getOsInfo(): Promise<{ - platform: string; - arch: string; - version: string; - release: string; - }>; -} diff --git a/tracking-plan-mcp.md b/tracking-plan-mcp.md deleted file mode 100644 index 435af873..00000000 --- a/tracking-plan-mcp.md +++ /dev/null @@ -1,139 +0,0 @@ -# Atlas MCP Server Tracking Plan - -Generated on April 16, 2025 - -## MDB MCP Event - -All events in the MCP server follow this base structure with common properties. Specific event types will have additional properties as detailed below. - -**Base Properties**: - -- **timestamp** (required): `string` - - ISO 8601 timestamp when the event occurred -- **machine_id** (required): `string` - - Unique anonymous identifier of the machine -- **mcp\_server\_version** (required): `string` - - The version of the MCP server. -- **mcp\_server\_name** (required): `string` - - The name of the MCP server. -- **mcp\_client\_version** (required): `string` - - The version of the MCP agent. -- **mcp\_client\_name** (required): `string` - - The name of the agent calling the MCP server. (e.g. claude) -- **platform** (required): `string` - - The platform on which the MCP server is running. -- **arch** (required): `string` - - The architecture of the system's processor. -- **os\_type** (optional): `string | undefined` - - The type of operating system. -- **os\_version** (optional): `string | undefined` - - Detailed kernel or system version information. -- **os\_linux\_dist** (optional): `string | undefined` - - The Linux distribution name, if applicable. -- **component** (required): `string` - - The component generating the event (e.g., "server", "tool", "atlas", "mongodb") -- **action** (required): `string` - - The specific action being performed - -## Events per component - -### Server Component - -#### Server Lifecycle -**component**: `"server"` -**action**: `"lifecycle"` - -**Additional Properties**: -- **state** (required): `"start" | "stop"` - - The lifecycle state change -- **startup_time_ms** (optional): `number` - - Time taken for the server to start. Present when state is "start". -- **connected_services** (optional): `string[] | undefined` - - List of services connected at launch. Present when state is "start". -- **runtime_duration_ms** (optional): `number` - - The total runtime duration. Present when state is "stop". -- **exit_code** (optional): `number` - - The exit code. Present when state is "stop". -- **reason** (optional): `string | undefined` - - The stop reason (e.g., "normal", "error", "timeout"). Present when state is "stop". - -#### Tool Registration -**component**: `"tool"` -**action**: `"register" | "deregister"` - -**Additional Properties**: -- **count** (required): `number` - - The number of tools registered -- **tool_list** (required): `string[]` - - List of tools registered - - -#### Tool Call -**component**: `"tool"` -**action**: `"call"` - -**Additional Properties**: -- **name** (required): `string` - - The name of the tool -- **target** (required): `"mongodb" | "atlas"` - - The service being targeted by the tool -- **operation** (required): `string` - - The type of operation being performed - - For MongoDB: `"query" | "aggregation" | "insert" | "update" | "delete" | "index" | "metadata" | "connect"` - - For Atlas: `"list_clusters" | "list_projects" | "create_cluster" | "manage_access_list" | "manage_database_user" | "connect"` -- **duration_ms** (required): `number` - - Execution time in milliseconds -- **success** (required): `boolean` - - Whether the call succeeded -- **error_code** (optional): `string | undefined` - - Error code if operation failed -- **error_type** (optional): `string | undefined` - - Type of error if operation failed -- **state** (optional): `"attempt" | "success" | "failure"` - - Connection state (when operation is "connect") -- **doc_count** (optional): `number | undefined` - - Number of affected documents (MongoDB only) -- **database** (optional): `string | undefined` - - Target database name (MongoDB only) -- **collection** (optional): `string | undefined` - - Target collection name (MongoDB only) -- **connection_id** (optional): `string | undefined` - - Connection identifier (required when operation is "connect") -- **project_id** (optional): `string | undefined` - - Atlas project ID (Atlas only) -- **org_id** (optional): `string | undefined` - - Atlas organization ID (Atlas only) -- **cluster_name** (optional): `string | undefined` - - Target cluster name (Atlas only) -- **is_atlas** (optional): `boolean | undefined` - - Whether using Atlas connection string (when operation is "connect") - -#### Error -**component**: `"error"` -**action**: `"occur"` - -**Additional Properties**: -- **code** (required): `number` - - The error code -- **context** (required): `string` - - Where the error occurred (e.g., "auth", "tool", "connection") -- **type** (required): `string` - - Error category or type - -### Authentication Component - -#### Authentication -**component**: `"auth"` -**action**: `"authenticate"` - -**Additional Properties**: -- **target** (required): `"mongodb" | "atlas"` - - The service being authenticated with -- **success** (required): `boolean` - - Whether authentication succeeded -- **method** (required): `string` - - Authentication method used -- **duration_ms** (required): `number` - - Time taken to authenticate -- **error_code** (optional): `string | undefined` - - Error code if failed \ No newline at end of file From af714b4ad866a15484990c16fd72c4cfb35d9134 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 17 Apr 2025 00:18:54 +0100 Subject: [PATCH 03/46] wip --- package-lock.json | 7 ++++ package.json | 1 + src/common/atlas/apiClient.ts | 31 ++++++++++++++- src/config.ts | 4 +- src/server.ts | 7 +--- src/session.ts | 31 ++++++++++----- src/telemetry/telemetry.ts | 75 +++++++++++++++++++++++++++++++++++ src/telemetry/types.ts | 41 +++++++++++++++++++ src/tools/tool.ts | 37 ++++++++++++++--- 9 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 src/telemetry/telemetry.ts create mode 100644 src/telemetry/types.ts diff --git a/package-lock.json b/package-lock.json index eb36763b..62dcbe43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "node-machine-id": "^1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", @@ -11467,6 +11468,12 @@ "dev": true, "license": "MIT" }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "license": "MIT" + }, "node_modules/node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", diff --git a/package.json b/package.json index b2ca5adc..1a3f7ca9 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "node-machine-id": "^1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 9aab2cea..81863514 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -4,6 +4,7 @@ import type { FetchOptions } from "openapi-fetch"; import { AccessToken, ClientCredentials } from "simple-oauth2"; import { ApiClientError } from "./apiClientError.js"; import { paths, operations } from "./openapi.js"; +import { BaseEvent } from "../../telemetry/types.js"; const ATLAS_API_VERSION = "2025-03-12"; @@ -63,10 +64,10 @@ export class ApiClient { constructor(options?: ApiClientOptions) { this.options = { ...options, - baseUrl: options?.baseUrl || "https://cloud.mongodb.com/", + baseUrl: options?.baseUrl || "https://cloud-dev.mongodb.com/", userAgent: options?.userAgent || - `${config.mcp_server_name}/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, + `${config.mcpServerName}/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }; this.client = createClient({ @@ -117,6 +118,32 @@ export class ApiClient { }>; } + async sendEvents(events: BaseEvent[]): Promise { + let endpoint = "api/private/unauth/telemetry/events"; + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + "User-Agent": this.options.userAgent, + }; + + const accessToken = await this.getAccessToken(); + if (accessToken) { + endpoint = "api/private/v1.0/telemetry/events"; + headers["Authorization"] = `Bearer ${accessToken}`; + } + + const url = new URL(endpoint, this.options.baseUrl); + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(events), + }); + + if (!response.ok) { + throw await ApiClientError.fromResponse(response); + } + } + // DO NOT EDIT. This is auto-generated code. async listClustersForAllProjects(options?: FetchOptions) { const { data } = await this.client.GET("/api/atlas/v2/clusters", options); diff --git a/src/config.ts b/src/config.ts index 350e0305..2d8bb740 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,6 +11,7 @@ interface UserConfig { apiBaseUrl?: string; apiClientId?: string; apiClientSecret?: string; + telemetry?: 'enabled' | 'disabled'; logPath: string; connectionString?: string; connectOptions: { @@ -52,7 +53,8 @@ const config = { ...mergedUserConfig, ...machineMetadata, version: packageJson.version, - mcp_server_name: "MdbMcpServer" + mcpServerName: "MdbMcpServer", + isTelemetryEnabled: true }; export default config; diff --git a/src/server.ts b/src/server.ts index 882fe653..4e51be9d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,11 +25,8 @@ export class Server { await this.mcpServer.connect(transport); this.mcpServer.server.oninitialized = () => { - const client = this.mcpServer.server.getClientVersion(); - this.session.clientName = client?.name; - this.session.clientVersion = client?.version; - - logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); + this.session.setAgentClientData(this.mcpServer.server.getClientVersion()); + logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); }; } diff --git a/src/session.ts b/src/session.ts index 450b9c41..5173433f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,13 +3,32 @@ import { ApiClient } from "./common/atlas/apiClient.js"; import config from "./config.js"; import logger from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; +import { Implementation } from "@modelcontextprotocol/sdk/types.js"; export class Session { sessionId?: string; serviceProvider?: NodeDriverServiceProvider; apiClient?: ApiClient; - clientName?: string; - clientVersion?: string; + agentClientName?: string; + agentClientVersion?: string; + + constructor() { + // configure api client if credentials are set + if (config.apiClientId && config.apiClientSecret) { + this.apiClient = new ApiClient({ + baseUrl: config.apiBaseUrl, + credentials: { + clientId: config.apiClientId, + clientSecret: config.apiClientSecret, + }, + }); + } + } + + setAgentClientData(agentClient: Implementation | undefined) { + this.agentClientName = agentClient?.name; + this.agentClientVersion = agentClient?.version; + } ensureAuthenticated(): asserts this is { apiClient: ApiClient } { if (!this.apiClient) { @@ -39,12 +58,4 @@ export class Session { this.serviceProvider = undefined; } } - - async emitTelemetry(todo: unknown): Promise { - logger.info( - mongoLogId(1_000_001), - "telemetry", - `Telemetry event: ${JSON.stringify(todo)}` - ); - } } diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts new file mode 100644 index 00000000..423d2a14 --- /dev/null +++ b/src/telemetry/telemetry.ts @@ -0,0 +1,75 @@ +import { Session } from '../session.js'; +import { BaseEvent, type ToolEvent } from './types.js'; +import pkg from '../../package.json' with { type: 'json' }; +import config from '../config.js'; +import logger from '../logger.js'; +import { mongoLogId } from 'mongodb-log-writer'; +import { ApiClient } from '../common/atlas/apiClient.js'; +import { ApiClientError } from '../common/atlas/apiClientError.js'; + +export class Telemetry { + constructor(private readonly session: Session) {} + + private readonly commonProperties = { + mcp_server_version: pkg.version, + mcp_server_name: config.mcpServerName, + mcp_client_version: this.session.agentClientVersion, + mcp_client_name: this.session.agentClientName, + session_id: this.session.sessionId, + device_id: config.device_id, + platform: config.platform, + arch: config.arch, + os_type: config.os_type, + os_version: config.os_version, + }; + + private readonly isTelemetryEnabled = config.telemetry === 'enabled'; + + async emitToolEvent(command: string, category: string, startTime: number, result: 'success' | 'failure', error?: Error): Promise { + if (!this.isTelemetryEnabled) { + logger.debug(mongoLogId(1_000_000), "telemetry", `Telemetry is disabled, skipping event.`); + return; + } + + const duration = Date.now() - startTime; + + const event: ToolEvent = { + timestamp: new Date().toISOString(), + source: 'mdbmcp', + properties: { + ...this.commonProperties, + command: command, + category: category, + duration_ms: duration, + result: result + } + }; + + if (result === 'failure') { + event.properties.error_type = error?.name; + event.properties.error_code = error?.message; + } + + await this.emit(event); + } + + private async emit(event: BaseEvent): Promise { + try { + if (this.session.apiClient) { + await this.session.apiClient.sendEvents([event]); + } + } catch (error) { + logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to authenticated client: ${error}`); + } + + // if it is unauthenticated, send to temp client + try { + const tempApiClient = new ApiClient({ + baseUrl: config.apiBaseUrl, + }); + await tempApiClient.sendEvents([event]); + } catch (error) { + logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to unauthenticated client: ${error}`); + } + } +} diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts new file mode 100644 index 00000000..b3ba9b9f --- /dev/null +++ b/src/telemetry/types.ts @@ -0,0 +1,41 @@ +/** + * Base interface for all events + */ +export interface Event { + timestamp: string; + source: 'mdbmcp'; + properties: Record; +} + +export interface BaseEvent extends Event { + properties: { + device_id: string; + mcp_server_version: string; + mcp_server_name: string; + mcp_client_version?: string; + mcp_client_name?: string; + platform: string; + arch: string; + os_type: string; + os_version?: string; + session_id?: string; + } & Event['properties']; +} + +/** + * Interface for tool events + */ +export interface ToolEvent extends BaseEvent { + properties: { + command: string; + category: string; + duration_ms: number; + result: 'success' | 'failure'; + error_code?: string; + error_type?: string; + project_id?: string; + org_id?: string; + cluster_name?: string; + is_atlas?: boolean; + } & BaseEvent['properties']; +} diff --git a/src/tools/tool.ts b/src/tools/tool.ts index da5cbb8c..18d69f73 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,10 +1,14 @@ -import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z, ZodNever, ZodRawShape } from "zod"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z, type ZodRawShape, type ZodNever } from "zod"; +import type { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Session } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; +<<<<<<< HEAD import config from "../config.js"; +======= +import { Telemetry } from "../telemetry/telemetry.js"; +>>>>>>> 61e295a (wip) export type ToolArgs = z.objectOutputType; @@ -12,6 +16,7 @@ export type OperationType = "metadata" | "read" | "create" | "delete" | "update" export type ToolCategory = "mongodb" | "atlas"; export abstract class ToolBase { +<<<<<<< HEAD protected abstract name: string; protected abstract category: ToolCategory; @@ -21,12 +26,20 @@ export abstract class ToolBase { protected abstract description: string; protected abstract argsShape: ZodRawShape; +======= + protected abstract readonly name: string; + protected abstract readonly description: string; + protected abstract readonly argsShape: ZodRawShape; +>>>>>>> 61e295a (wip) protected abstract category: string; + private readonly telemetry: Telemetry; protected abstract execute(...args: Parameters>): Promise; - protected constructor(protected session: Session) {} + protected constructor(protected session: Session) { + this.telemetry = new Telemetry(session); + } public register(server: McpServer): void { if (!this.verifyAllowed()) { @@ -34,6 +47,7 @@ export abstract class ToolBase { } const callback: ToolCallback = async (...args) => { + const startTime = Date.now(); try { logger.debug( mongoLogId(1_000_006), @@ -41,10 +55,20 @@ export abstract class ToolBase { `Executing ${this.name} with args: ${JSON.stringify(args)}` ); - return await this.execute(...args); + const result = await this.execute(...args); + await this.telemetry.emitToolEvent(this.name, this.category, startTime, "success"); + return result; } catch (error: unknown) { logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); + await this.telemetry.emitToolEvent( + this.name, + this.category, + startTime, + "failure", + error instanceof Error ? error : new Error(String(error)) + ); + return await this.handleError(error); } }; @@ -52,6 +76,7 @@ export abstract class ToolBase { server.tool(this.name, this.description, this.argsShape, callback); } +<<<<<<< HEAD // Checks if a tool is allowed to run based on the config private verifyAllowed(): boolean { let errorClarification: string | undefined; @@ -76,6 +101,8 @@ export abstract class ToolBase { return true; } +======= +>>>>>>> 61e295a (wip) // This method is intended to be overridden by subclasses to handle errors protected handleError(error: unknown): Promise | CallToolResult { return { From 40aaef09e667d99f28b1e499083dc608c933b7a5 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 17 Apr 2025 00:34:05 +0100 Subject: [PATCH 04/46] wip: simple approach --- src/telemetry/telemetry.ts | 65 +++++++++++++++++++++++++++++++----- src/tools/atlas/atlasTool.ts | 1 - src/tools/tool.ts | 13 -------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 423d2a14..2ab835db 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -6,6 +6,11 @@ import logger from '../logger.js'; import { mongoLogId } from 'mongodb-log-writer'; import { ApiClient } from '../common/atlas/apiClient.js'; import { ApiClientError } from '../common/atlas/apiClientError.js'; +import fs from 'fs/promises'; +import path from 'path'; + +const isTelemetryEnabled = config.telemetry === 'enabled'; +const CACHE_FILE = path.join(process.cwd(), '.telemetry-cache.json'); export class Telemetry { constructor(private readonly session: Session) {} @@ -23,10 +28,8 @@ export class Telemetry { os_version: config.os_version, }; - private readonly isTelemetryEnabled = config.telemetry === 'enabled'; - async emitToolEvent(command: string, category: string, startTime: number, result: 'success' | 'failure', error?: Error): Promise { - if (!this.isTelemetryEnabled) { + if (!isTelemetryEnabled) { logger.debug(mongoLogId(1_000_000), "telemetry", `Telemetry is disabled, skipping event.`); return; } @@ -50,26 +53,72 @@ export class Telemetry { event.properties.error_code = error?.message; } - await this.emit(event); + await this.emit([event]); } - private async emit(event: BaseEvent): Promise { + private async emit(events: BaseEvent[]): Promise { + // First try to read any cached events + const cachedEvents = await this.readCache(); + const allEvents = [...cachedEvents, ...events]; + + logger.debug(mongoLogId(1_000_000), "telemetry", `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`); + try { if (this.session.apiClient) { - await this.session.apiClient.sendEvents([event]); + await this.session.apiClient.sendEvents(allEvents); + // If successful, clear the cache + await this.clearCache(); + return; } } catch (error) { logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to authenticated client: ${error}`); + // Cache the events that failed to send + await this.cacheEvents(allEvents); } - // if it is unauthenticated, send to temp client + // Try unauthenticated client as fallback try { const tempApiClient = new ApiClient({ baseUrl: config.apiBaseUrl, }); - await tempApiClient.sendEvents([event]); + await tempApiClient.sendEvents(allEvents); + // If successful, clear the cache + await this.clearCache(); } catch (error) { logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to unauthenticated client: ${error}`); + // Cache the events that failed to send + await this.cacheEvents(allEvents); + } + } + + private async readCache(): Promise { + try { + const data = await fs.readFile(CACHE_FILE, 'utf-8'); + return JSON.parse(data); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + logger.warning(mongoLogId(1_000_000), "telemetry", `Error reading telemetry cache: ${error}`); + } + return []; + } + } + + private async cacheEvents(events: BaseEvent[]): Promise { + try { + await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2)); + logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`); + } catch (error) { + logger.warning(mongoLogId(1_000_000), "telemetry", `Failed to cache telemetry events: ${error}`); + } + } + + private async clearCache(): Promise { + try { + await fs.unlink(CACHE_FILE); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + logger.warning(mongoLogId(1_000_000), "telemetry", `Error clearing telemetry cache: ${error}`); + } } } } diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index ea6fdae9..74683d75 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -2,7 +2,6 @@ import { ToolBase, ToolCategory } from "../tool.js"; import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { - protected category = "atlas"; constructor(protected readonly session: Session) { super(session); } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 18d69f73..bbb61810 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -4,11 +4,8 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Session } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; -<<<<<<< HEAD import config from "../config.js"; -======= import { Telemetry } from "../telemetry/telemetry.js"; ->>>>>>> 61e295a (wip) export type ToolArgs = z.objectOutputType; @@ -16,7 +13,6 @@ export type OperationType = "metadata" | "read" | "create" | "delete" | "update" export type ToolCategory = "mongodb" | "atlas"; export abstract class ToolBase { -<<<<<<< HEAD protected abstract name: string; protected abstract category: ToolCategory; @@ -26,13 +22,7 @@ export abstract class ToolBase { protected abstract description: string; protected abstract argsShape: ZodRawShape; -======= - protected abstract readonly name: string; - protected abstract readonly description: string; - protected abstract readonly argsShape: ZodRawShape; ->>>>>>> 61e295a (wip) - protected abstract category: string; private readonly telemetry: Telemetry; protected abstract execute(...args: Parameters>): Promise; @@ -76,7 +66,6 @@ export abstract class ToolBase { server.tool(this.name, this.description, this.argsShape, callback); } -<<<<<<< HEAD // Checks if a tool is allowed to run based on the config private verifyAllowed(): boolean { let errorClarification: string | undefined; @@ -101,8 +90,6 @@ export abstract class ToolBase { return true; } -======= ->>>>>>> 61e295a (wip) // This method is intended to be overridden by subclasses to handle errors protected handleError(error: unknown): Promise | CallToolResult { return { From 8adc20dde9189e9fb189d4f396d89a7e182f328b Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 17 Apr 2025 09:35:31 +0100 Subject: [PATCH 05/46] add cache and client logic --- src/common/atlas/apiClient.ts | 8 ++ src/session.ts | 11 +- src/telemetry/telemetry.ts | 185 +++++++++++++++++++++++--------- src/tools/atlas/listProjects.ts | 3 + 4 files changed, 155 insertions(+), 52 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 81863514..bbee6f23 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -5,6 +5,8 @@ import { AccessToken, ClientCredentials } from "simple-oauth2"; import { ApiClientError } from "./apiClientError.js"; import { paths, operations } from "./openapi.js"; import { BaseEvent } from "../../telemetry/types.js"; +import { mongoLogId } from "mongodb-log-writer"; +import logger from "../../logger.js"; const ATLAS_API_VERSION = "2025-03-12"; @@ -91,6 +93,12 @@ export class ApiClient { this.client.use(this.authMiddleware); } this.client.use(this.errorMiddleware); + logger.info(mongoLogId(1_000_000), "api-client", `Initialized API client with credentials: ${this.hasCredentials()}`); + } + + public hasCredentials(): boolean { + logger.info(mongoLogId(1_000_000), "api-client", `Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}`); + return !!(this.oauth2Client && this.accessToken); } public async getIpInfo(): Promise<{ diff --git a/src/session.ts b/src/session.ts index 5173433f..7becf3d9 100644 --- a/src/session.ts +++ b/src/session.ts @@ -8,12 +8,12 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js"; export class Session { sessionId?: string; serviceProvider?: NodeDriverServiceProvider; - apiClient?: ApiClient; + apiClient: ApiClient; agentClientName?: string; agentClientVersion?: string; constructor() { - // configure api client if credentials are set + // Initialize API client with credentials if available if (config.apiClientId && config.apiClientSecret) { this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl, @@ -22,7 +22,11 @@ export class Session { clientSecret: config.apiClientSecret, }, }); + return; } + + // Initialize API client without credentials + this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl }); } setAgentClientData(agentClient: Implementation | undefined) { @@ -31,13 +35,14 @@ export class Session { } ensureAuthenticated(): asserts this is { apiClient: ApiClient } { - if (!this.apiClient) { + if (!this.apiClient || !(this.apiClient.hasCredentials())) { if (!config.apiClientId || !config.apiClientSecret) { throw new Error( "Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables." ); } + // Initialize or reinitialize API client with credentials this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl, credentials: { diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 2ab835db..781453a6 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -5,35 +5,86 @@ import config from '../config.js'; import logger from '../logger.js'; import { mongoLogId } from 'mongodb-log-writer'; import { ApiClient } from '../common/atlas/apiClient.js'; -import { ApiClientError } from '../common/atlas/apiClientError.js'; import fs from 'fs/promises'; import path from 'path'; -const isTelemetryEnabled = config.telemetry === 'enabled'; +const TELEMETRY_ENABLED = config.telemetry !== 'disabled'; const CACHE_FILE = path.join(process.cwd(), '.telemetry-cache.json'); +interface TelemetryError extends Error { + code?: string; +} + +type EventResult = { + success: boolean; + error?: Error; +}; + +type CommonProperties = { + device_id: string; + mcp_server_version: string; + mcp_server_name: string; + mcp_client_version?: string; + mcp_client_name?: string; + platform: string; + arch: string; + os_type: string; + os_version?: string; + session_id?: string; +}; + export class Telemetry { - constructor(private readonly session: Session) {} + private readonly commonProperties: CommonProperties; - private readonly commonProperties = { + constructor(private readonly session: Session) { + // Ensure all required properties are present + this.commonProperties = Object.freeze({ + device_id: config.device_id, mcp_server_version: pkg.version, mcp_server_name: config.mcpServerName, mcp_client_version: this.session.agentClientVersion, mcp_client_name: this.session.agentClientName, - session_id: this.session.sessionId, - device_id: config.device_id, platform: config.platform, arch: config.arch, os_type: config.os_type, os_version: config.os_version, - }; + }); + } - async emitToolEvent(command: string, category: string, startTime: number, result: 'success' | 'failure', error?: Error): Promise { - if (!isTelemetryEnabled) { - logger.debug(mongoLogId(1_000_000), "telemetry", `Telemetry is disabled, skipping event.`); + /** + * Emits a tool event with timing and error information + * @param command - The command being executed + * @param category - Category of the command + * @param startTime - Start time in milliseconds + * @param result - Whether the command succeeded or failed + * @param error - Optional error if the command failed + */ + public async emitToolEvent( + command: string, + category: string, + startTime: number, + result: 'success' | 'failure', + error?: Error + ): Promise { + if (!TELEMETRY_ENABLED) { + logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping event."); return; } + const event = this.createToolEvent(command, category, startTime, result, error); + await this.emit([event]); + } + + /** + * Creates a tool event with common properties and timing information + */ + private createToolEvent( + command: string, + category: string, + startTime: number, + result: 'success' | 'failure', + error?: Error + ): ToolEvent { const duration = Date.now() - startTime; const event: ToolEvent = { @@ -41,83 +92,119 @@ export class Telemetry { source: 'mdbmcp', properties: { ...this.commonProperties, - command: command, - category: category, + command, + category, duration_ms: duration, - result: result + session_id: this.session.sessionId, + result, + ...(error && { + error_type: error.name, + error_code: error.message + }) } }; - if (result === 'failure') { - event.properties.error_type = error?.name; - event.properties.error_code = error?.message; - } - - await this.emit([event]); + return event; } + /** + * Attempts to emit events through authenticated and unauthenticated clients + * Falls back to caching if both attempts fail + */ private async emit(events: BaseEvent[]): Promise { - // First try to read any cached events const cachedEvents = await this.readCache(); const allEvents = [...cachedEvents, ...events]; - logger.debug(mongoLogId(1_000_000), "telemetry", `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`); + logger.debug( + mongoLogId(1_000_000), + "telemetry", + `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)` + ); - try { - if (this.session.apiClient) { - await this.session.apiClient.sendEvents(allEvents); - // If successful, clear the cache - await this.clearCache(); - return; - } - } catch (error) { - logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to authenticated client: ${error}`); - // Cache the events that failed to send - await this.cacheEvents(allEvents); + const result = await this.sendEvents(this.session.apiClient, allEvents); + if (result.success) { + await this.clearCache(); + return; } - // Try unauthenticated client as fallback + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Error sending event to client: ${result.error}` + ); + await this.cacheEvents(allEvents); + } + + /** + * Attempts to send events through the provided API client + */ + private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise { try { - const tempApiClient = new ApiClient({ - baseUrl: config.apiBaseUrl, - }); - await tempApiClient.sendEvents(allEvents); - // If successful, clear the cache - await this.clearCache(); + await client.sendEvents(events); + return { success: true }; } catch (error) { - logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to unauthenticated client: ${error}`); - // Cache the events that failed to send - await this.cacheEvents(allEvents); + return { + success: false, + error: error instanceof Error ? error : new Error(String(error)) + }; } } + /** + * Reads cached events from disk + * Returns empty array if no cache exists or on read error + */ private async readCache(): Promise { try { const data = await fs.readFile(CACHE_FILE, 'utf-8'); - return JSON.parse(data); + return JSON.parse(data) as BaseEvent[]; } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - logger.warning(mongoLogId(1_000_000), "telemetry", `Error reading telemetry cache: ${error}`); + const typedError = error as TelemetryError; + if (typedError.code !== 'ENOENT') { + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Error reading telemetry cache: ${typedError.message}` + ); } return []; } } + /** + * Caches events to disk for later sending + */ private async cacheEvents(events: BaseEvent[]): Promise { try { await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2)); - logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`); + logger.debug( + mongoLogId(1_000_000), + "telemetry", + `Cached ${events.length} events for later sending` + ); } catch (error) { - logger.warning(mongoLogId(1_000_000), "telemetry", `Failed to cache telemetry events: ${error}`); + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Failed to cache telemetry events: ${error instanceof Error ? error.message : String(error)}` + ); } } + /** + * Clears the event cache after successful sending + */ private async clearCache(): Promise { try { await fs.unlink(CACHE_FILE); } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - logger.warning(mongoLogId(1_000_000), "telemetry", `Error clearing telemetry cache: ${error}`); + const typedError = error as TelemetryError; + if (typedError.code !== 'ENOENT') { + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Error clearing telemetry cache: ${typedError.message}` + ); } } } diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index adce9b5d..1483c35c 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -1,6 +1,8 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "./atlasTool.js"; import { OperationType } from "../tool.js"; +import { mongoLogId } from "mongodb-log-writer"; +import logger from "../../logger.js"; export class ListProjectsTool extends AtlasToolBase { protected name = "atlas-list-projects"; @@ -10,6 +12,7 @@ export class ListProjectsTool extends AtlasToolBase { protected async execute(): Promise { this.session.ensureAuthenticated(); + logger.info(mongoLogId(1_000_000), "session", `Called ensureAuthenticated! `); const data = await this.session.apiClient.listProjects(); From 5336e4a60425eec29b42728568ced5a421248ff1 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 10:58:01 +0100 Subject: [PATCH 06/46] update deps --- package-lock.json | 7 ------- package.json | 1 - 2 files changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99db0d1a..bf0be3f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", - "node-machine-id": "^1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", @@ -11469,12 +11468,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-machine-id": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", - "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "license": "MIT" - }, "node_modules/node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", diff --git a/package.json b/package.json index 6e4dfe4a..ca9d20d0 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", - "node-machine-id": "^1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", From 76e2a6fd0e761b71b5e7c479380aa8a205274acc Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 10:58:36 +0100 Subject: [PATCH 07/46] update --- src/tools/atlas/listProjects.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index 1483c35c..adce9b5d 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -1,8 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "./atlasTool.js"; import { OperationType } from "../tool.js"; -import { mongoLogId } from "mongodb-log-writer"; -import logger from "../../logger.js"; export class ListProjectsTool extends AtlasToolBase { protected name = "atlas-list-projects"; @@ -12,7 +10,6 @@ export class ListProjectsTool extends AtlasToolBase { protected async execute(): Promise { this.session.ensureAuthenticated(); - logger.info(mongoLogId(1_000_000), "session", `Called ensureAuthenticated! `); const data = await this.session.apiClient.listProjects(); From 1850676a492770f061c8f1f39b1498c226ebd365 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 11:13:38 +0100 Subject: [PATCH 08/46] update logs --- src/common/atlas/apiClient.ts | 1 - src/session.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 2054c041..4c0f74fd 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -93,7 +93,6 @@ export class ApiClient { this.client.use(this.authMiddleware); } this.client.use(this.errorMiddleware); - logger.info(mongoLogId(1_000_000), "api-client", `Initialized API client with credentials: ${this.hasCredentials()}`); } public hasCredentials(): boolean { diff --git a/src/session.ts b/src/session.ts index 7becf3d9..292d5b1c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,8 +1,6 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { ApiClient } from "./common/atlas/apiClient.js"; import config from "./config.js"; -import logger from "./logger.js"; -import { mongoLogId } from "mongodb-log-writer"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; export class Session { From 3a5d4c161a19ef63ed8af5a7a93573821473cac7 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 11:13:50 +0100 Subject: [PATCH 09/46] reformat --- src/common/atlas/apiClient.ts | 6 +++- src/config.ts | 7 ++--- src/index.ts | 7 +---- src/server.ts | 3 +- src/session.ts | 6 ++-- src/telemetry/telemetry.ts | 56 +++++++++++++++-------------------- src/telemetry/types.ts | 8 ++--- 7 files changed, 41 insertions(+), 52 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 4c0f74fd..e7f6a93d 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -96,7 +96,11 @@ export class ApiClient { } public hasCredentials(): boolean { - logger.info(mongoLogId(1_000_000), "api-client", `Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}`); + logger.info( + mongoLogId(1_000_000), + "api-client", + `Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}` + ); return !!(this.oauth2Client && this.accessToken); } diff --git a/src/config.ts b/src/config.ts index 2d8bb740..4f667d15 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,7 +11,7 @@ interface UserConfig { apiBaseUrl?: string; apiClientId?: string; apiClientSecret?: string; - telemetry?: 'enabled' | 'disabled'; + telemetry?: "enabled" | "disabled"; logPath: string; connectionString?: string; connectOptions: { @@ -40,21 +40,20 @@ const mergedUserConfig = { ...getCliConfig(), }; - const machineMetadata = { device_id: "id", // TODO: use @mongodb-js/machine-id platform: process.platform, arch: process.arch, os_type: process.platform, os_version: process.version, -} +}; const config = { ...mergedUserConfig, ...machineMetadata, version: packageJson.version, mcpServerName: "MdbMcpServer", - isTelemetryEnabled: true + isTelemetryEnabled: true, }; export default config; diff --git a/src/index.ts b/src/index.ts index 5a0c354e..806207f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import config from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; - async function main() { const session = new Session(); const mcpServer = new McpServer({ @@ -30,10 +29,6 @@ async function main() { try { await main(); } catch (error: unknown) { - logger.emergency( - mongoLogId(1_000_004), - "server", - `Fatal error running server: ${error as string}` - ); + logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`); process.exit(1); } diff --git a/src/server.ts b/src/server.ts index 4e51be9d..5da028ca 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,7 +6,6 @@ import { MongoDbTools } from "./tools/mongodb/tools.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; - export class Server { public readonly session: Session; private readonly mcpServer: McpServer; @@ -24,7 +23,7 @@ export class Server { await this.mcpServer.connect(transport); - this.mcpServer.server.oninitialized = () => { + this.mcpServer.server.oninitialized = () => { this.session.setAgentClientData(this.mcpServer.server.getClientVersion()); logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); }; diff --git a/src/session.ts b/src/session.ts index 292d5b1c..000a2682 100644 --- a/src/session.ts +++ b/src/session.ts @@ -8,7 +8,7 @@ export class Session { serviceProvider?: NodeDriverServiceProvider; apiClient: ApiClient; agentClientName?: string; - agentClientVersion?: string; + agentClientVersion?: string; constructor() { // Initialize API client with credentials if available @@ -26,14 +26,14 @@ export class Session { // Initialize API client without credentials this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl }); } - + setAgentClientData(agentClient: Implementation | undefined) { this.agentClientName = agentClient?.name; this.agentClientVersion = agentClient?.version; } ensureAuthenticated(): asserts this is { apiClient: ApiClient } { - if (!this.apiClient || !(this.apiClient.hasCredentials())) { + if (!this.apiClient || !this.apiClient.hasCredentials()) { if (!config.apiClientId || !config.apiClientSecret) { throw new Error( "Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables." diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 781453a6..fa1c0511 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -1,15 +1,15 @@ -import { Session } from '../session.js'; -import { BaseEvent, type ToolEvent } from './types.js'; -import pkg from '../../package.json' with { type: 'json' }; -import config from '../config.js'; -import logger from '../logger.js'; -import { mongoLogId } from 'mongodb-log-writer'; -import { ApiClient } from '../common/atlas/apiClient.js'; -import fs from 'fs/promises'; -import path from 'path'; - -const TELEMETRY_ENABLED = config.telemetry !== 'disabled'; -const CACHE_FILE = path.join(process.cwd(), '.telemetry-cache.json'); +import { Session } from "../session.js"; +import { BaseEvent, type ToolEvent } from "./types.js"; +import pkg from "../../package.json" with { type: "json" }; +import config from "../config.js"; +import logger from "../logger.js"; +import { mongoLogId } from "mongodb-log-writer"; +import { ApiClient } from "../common/atlas/apiClient.js"; +import fs from "fs/promises"; +import path from "path"; + +const TELEMETRY_ENABLED = config.telemetry !== "disabled"; +const CACHE_FILE = path.join(process.cwd(), ".telemetry-cache.json"); interface TelemetryError extends Error { code?: string; @@ -63,7 +63,7 @@ export class Telemetry { command: string, category: string, startTime: number, - result: 'success' | 'failure', + result: "success" | "failure", error?: Error ): Promise { if (!TELEMETRY_ENABLED) { @@ -82,14 +82,14 @@ export class Telemetry { command: string, category: string, startTime: number, - result: 'success' | 'failure', + result: "success" | "failure", error?: Error ): ToolEvent { const duration = Date.now() - startTime; const event: ToolEvent = { timestamp: new Date().toISOString(), - source: 'mdbmcp', + source: "mdbmcp", properties: { ...this.commonProperties, command, @@ -99,9 +99,9 @@ export class Telemetry { result, ...(error && { error_type: error.name, - error_code: error.message - }) - } + error_code: error.message, + }), + }, }; return event; @@ -127,11 +127,7 @@ export class Telemetry { return; } - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Error sending event to client: ${result.error}` - ); + logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to client: ${result.error}`); await this.cacheEvents(allEvents); } @@ -145,7 +141,7 @@ export class Telemetry { } catch (error) { return { success: false, - error: error instanceof Error ? error : new Error(String(error)) + error: error instanceof Error ? error : new Error(String(error)), }; } } @@ -156,11 +152,11 @@ export class Telemetry { */ private async readCache(): Promise { try { - const data = await fs.readFile(CACHE_FILE, 'utf-8'); + const data = await fs.readFile(CACHE_FILE, "utf-8"); return JSON.parse(data) as BaseEvent[]; } catch (error) { const typedError = error as TelemetryError; - if (typedError.code !== 'ENOENT') { + if (typedError.code !== "ENOENT") { logger.warning( mongoLogId(1_000_000), "telemetry", @@ -177,11 +173,7 @@ export class Telemetry { private async cacheEvents(events: BaseEvent[]): Promise { try { await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2)); - logger.debug( - mongoLogId(1_000_000), - "telemetry", - `Cached ${events.length} events for later sending` - ); + logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`); } catch (error) { logger.warning( mongoLogId(1_000_000), @@ -199,7 +191,7 @@ export class Telemetry { await fs.unlink(CACHE_FILE); } catch (error) { const typedError = error as TelemetryError; - if (typedError.code !== 'ENOENT') { + if (typedError.code !== "ENOENT") { logger.warning( mongoLogId(1_000_000), "telemetry", diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index b3ba9b9f..1d643db7 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -3,7 +3,7 @@ */ export interface Event { timestamp: string; - source: 'mdbmcp'; + source: "mdbmcp"; properties: Record; } @@ -19,7 +19,7 @@ export interface BaseEvent extends Event { os_type: string; os_version?: string; session_id?: string; - } & Event['properties']; + } & Event["properties"]; } /** @@ -30,12 +30,12 @@ export interface ToolEvent extends BaseEvent { command: string; category: string; duration_ms: number; - result: 'success' | 'failure'; + result: "success" | "failure"; error_code?: string; error_type?: string; project_id?: string; org_id?: string; cluster_name?: string; is_atlas?: boolean; - } & BaseEvent['properties']; + } & BaseEvent["properties"]; } From 23fff0582b0a8f7579a9005716e0a6d22d64d974 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 13:53:31 +0100 Subject: [PATCH 10/46] address comment: server main --- src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 806207f3..6bdcce20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import config from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; -async function main() { +try { const session = new Session(); const mcpServer = new McpServer({ name: "MongoDB Atlas", @@ -23,11 +23,6 @@ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); -} - -// Start the server -try { - await main(); } catch (error: unknown) { logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`); process.exit(1); From 754ce8dd41879a90ae1b133bffa73d6a3472658e Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 13:53:58 +0100 Subject: [PATCH 11/46] address comment: make dynamic config part of session --- src/session.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/session.ts b/src/session.ts index 000a2682..ef26e537 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,22 +9,29 @@ export class Session { apiClient: ApiClient; agentClientName?: string; agentClientVersion?: string; + private credentials?: { clientId: string; clientSecret: string }; + private baseUrl: string; constructor() { - // Initialize API client with credentials if available + this.baseUrl = config.apiBaseUrl ?? "https://cloud.mongodb.com/"; + + // Store credentials if available if (config.apiClientId && config.apiClientSecret) { + this.credentials = { + clientId: config.apiClientId, + clientSecret: config.apiClientSecret, + }; + + // Initialize API client with credentials this.apiClient = new ApiClient({ - baseUrl: config.apiBaseUrl, - credentials: { - clientId: config.apiClientId, - clientSecret: config.apiClientSecret, - }, + baseUrl: this.baseUrl, + credentials: this.credentials, }); return; } // Initialize API client without credentials - this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl }); + this.apiClient = new ApiClient({ baseUrl: this.baseUrl }); } setAgentClientData(agentClient: Implementation | undefined) { @@ -33,20 +40,18 @@ export class Session { } ensureAuthenticated(): asserts this is { apiClient: ApiClient } { - if (!this.apiClient || !this.apiClient.hasCredentials()) { - if (!config.apiClientId || !config.apiClientSecret) { + if (!this.apiClient.hasCredentials()) { + if (!this.credentials) { throw new Error( "Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables." ); } - // Initialize or reinitialize API client with credentials + // Reinitialize API client with the stored credentials + // This can happen if the server was configured without credentials but the env variables are later set this.apiClient = new ApiClient({ - baseUrl: config.apiBaseUrl, - credentials: { - clientId: config.apiClientId, - clientSecret: config.apiClientSecret, - }, + baseUrl: this.baseUrl, + credentials: this.credentials, }); } } From 8301e432c4203e8c0fef1eaa09ee29a07d70a48e Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 14:05:48 +0100 Subject: [PATCH 12/46] address comment: remove redundant method --- src/session.ts | 4 ++-- src/telemetry/telemetry.ts | 17 +---------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/session.ts b/src/session.ts index ef26e537..25dccfd1 100644 --- a/src/session.ts +++ b/src/session.ts @@ -14,14 +14,14 @@ export class Session { constructor() { this.baseUrl = config.apiBaseUrl ?? "https://cloud.mongodb.com/"; - + // Store credentials if available if (config.apiClientId && config.apiClientSecret) { this.credentials = { clientId: config.apiClientId, clientSecret: config.apiClientSecret, }; - + // Initialize API client with credentials this.apiClient = new ApiClient({ baseUrl: this.baseUrl, diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index fa1c0511..cbacf7d8 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -71,22 +71,7 @@ export class Telemetry { return; } - const event = this.createToolEvent(command, category, startTime, result, error); - await this.emit([event]); - } - - /** - * Creates a tool event with common properties and timing information - */ - private createToolEvent( - command: string, - category: string, - startTime: number, - result: "success" | "failure", - error?: Error - ): ToolEvent { const duration = Date.now() - startTime; - const event: ToolEvent = { timestamp: new Date().toISOString(), source: "mdbmcp", @@ -104,7 +89,7 @@ export class Telemetry { }, }; - return event; + await this.emit([event]); } /** From 994b6981c3d995e8bbc6375198a156d18434bfc8 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 14:12:34 +0100 Subject: [PATCH 13/46] address comment: move tool emission to tool.ts --- src/telemetry/telemetry.ts | 47 ++++++++++++-------------------------- src/telemetry/types.ts | 12 +++++++++- src/tools/tool.ts | 36 +++++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index cbacf7d8..2f34fd79 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -52,44 +52,27 @@ export class Telemetry { } /** - * Emits a tool event with timing and error information - * @param command - The command being executed - * @param category - Category of the command - * @param startTime - Start time in milliseconds - * @param result - Whether the command succeeded or failed - * @param error - Optional error if the command failed + * Emits events through the telemetry pipeline + * @param events - The events to emit */ - public async emitToolEvent( - command: string, - category: string, - startTime: number, - result: "success" | "failure", - error?: Error - ): Promise { + public async emitEvents(events: BaseEvent[]): Promise { if (!TELEMETRY_ENABLED) { - logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping event."); + logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping events."); return; } - const duration = Date.now() - startTime; - const event: ToolEvent = { - timestamp: new Date().toISOString(), - source: "mdbmcp", - properties: { - ...this.commonProperties, - command, - category, - duration_ms: duration, - session_id: this.session.sessionId, - result, - ...(error && { - error_type: error.name, - error_code: error.message, - }), - }, - }; + await this.emit(events); + } - await this.emit([event]); + /** + * Gets the common properties for events + * @returns Object containing common properties for all events + */ + public getCommonProperties(): CommonProperties { + return { + ...this.commonProperties, + session_id: this.session.sessionId, + }; } /** diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index 1d643db7..ae5bbb26 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -1,3 +1,13 @@ +/** + * Result type constants for telemetry events + */ +export const TELEMETRY_RESULT = { + SUCCESS: "success" as const, + FAILURE: "failure" as const, +}; + +export type TelemetryResult = (typeof TELEMETRY_RESULT)[keyof typeof TELEMETRY_RESULT]; + /** * Base interface for all events */ @@ -30,7 +40,7 @@ export interface ToolEvent extends BaseEvent { command: string; category: string; duration_ms: number; - result: "success" | "failure"; + result: TelemetryResult; error_code?: string; error_type?: string; project_id?: string; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index bbb61810..d4531ef8 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -6,6 +6,7 @@ import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; import config from "../config.js"; import { Telemetry } from "../telemetry/telemetry.js"; +import { type ToolEvent, TELEMETRY_RESULT, type TelemetryResult } from "../telemetry/types.js"; export type ToolArgs = z.objectOutputType; @@ -31,6 +32,33 @@ export abstract class ToolBase { this.telemetry = new Telemetry(session); } + /** + * Creates and emits a tool telemetry event + * @param startTime - Start time in milliseconds + * @param result - Whether the command succeeded or failed + * @param error - Optional error if the command failed + */ + private async emitToolEvent(startTime: number, result: TelemetryResult, error?: Error): Promise { + const duration = Date.now() - startTime; + const event: ToolEvent = { + timestamp: new Date().toISOString(), + source: "mdbmcp", + properties: { + ...this.telemetry.getCommonProperties(), + command: this.name, + category: this.category, + duration_ms: duration, + result, + ...(error && { + error_type: error.name, + error_code: error.message, + }), + }, + }; + + await this.telemetry.emitEvents([event]); + } + public register(server: McpServer): void { if (!this.verifyAllowed()) { return; @@ -46,16 +74,14 @@ export abstract class ToolBase { ); const result = await this.execute(...args); - await this.telemetry.emitToolEvent(this.name, this.category, startTime, "success"); + await this.emitToolEvent(startTime, TELEMETRY_RESULT.SUCCESS); return result; } catch (error: unknown) { logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); - await this.telemetry.emitToolEvent( - this.name, - this.category, + await this.emitToolEvent( startTime, - "failure", + TELEMETRY_RESULT.FAILURE, error instanceof Error ? error : new Error(String(error)) ); From 3bfc9f2028b497e1af3fbb72b1a94f4fb2ed471e Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 14:15:34 +0100 Subject: [PATCH 14/46] address comment: update agentClient to agentRunner --- src/session.ts | 14 ++++++++++---- src/telemetry/telemetry.ts | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/session.ts b/src/session.ts index 25dccfd1..756ce1be 100644 --- a/src/session.ts +++ b/src/session.ts @@ -7,8 +7,10 @@ export class Session { sessionId?: string; serviceProvider?: NodeDriverServiceProvider; apiClient: ApiClient; - agentClientName?: string; - agentClientVersion?: string; + agentRunner?: { + name: string; + version: string; + }; private credentials?: { clientId: string; clientSecret: string }; private baseUrl: string; @@ -35,8 +37,12 @@ export class Session { } setAgentClientData(agentClient: Implementation | undefined) { - this.agentClientName = agentClient?.name; - this.agentClientVersion = agentClient?.version; + if (agentClient?.name && agentClient?.version) { + this.agentRunner = { + name: agentClient.name, + version: agentClient.version, + }; + } } ensureAuthenticated(): asserts this is { apiClient: ApiClient } { diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 2f34fd79..97be2a3f 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -42,8 +42,8 @@ export class Telemetry { device_id: config.device_id, mcp_server_version: pkg.version, mcp_server_name: config.mcpServerName, - mcp_client_version: this.session.agentClientVersion, - mcp_client_name: this.session.agentClientName, + mcp_client_version: this.session.agentRunner?.version, + mcp_client_name: this.session.agentRunner?.name, platform: config.platform, arch: config.arch, os_type: config.os_type, From 6d920217b10af41733fa7d4842147d6fcf874d73 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 14:23:39 +0100 Subject: [PATCH 15/46] address comment: update agentClient to agentRunner --- src/server.ts | 2 +- src/session.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 5da028ca..107994ec 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ export class Server { await this.mcpServer.connect(transport); this.mcpServer.server.oninitialized = () => { - this.session.setAgentClientData(this.mcpServer.server.getClientVersion()); + this.session.setAgentRunner(this.mcpServer.server.getClientVersion()); logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); }; } diff --git a/src/session.ts b/src/session.ts index 756ce1be..0c38e7db 100644 --- a/src/session.ts +++ b/src/session.ts @@ -36,7 +36,7 @@ export class Session { this.apiClient = new ApiClient({ baseUrl: this.baseUrl }); } - setAgentClientData(agentClient: Implementation | undefined) { + setAgentRunner(agentClient: Implementation | undefined) { if (agentClient?.name && agentClient?.version) { this.agentRunner = { name: agentClient.name, From 7997c54f085aab38cd45c42008e9131f0f64edb6 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 14:49:05 +0100 Subject: [PATCH 16/46] address comment: isolate machine data --- src/config.ts | 4 ++-- src/telemetry/telemetry.ts | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/config.ts b/src/config.ts index 4f667d15..91659479 100644 --- a/src/config.ts +++ b/src/config.ts @@ -40,7 +40,8 @@ const mergedUserConfig = { ...getCliConfig(), }; -const machineMetadata = { +// Machine-specific metadata that isn't configurable +export const machineMetadata = { device_id: "id", // TODO: use @mongodb-js/machine-id platform: process.platform, arch: process.arch, @@ -50,7 +51,6 @@ const machineMetadata = { const config = { ...mergedUserConfig, - ...machineMetadata, version: packageJson.version, mcpServerName: "MdbMcpServer", isTelemetryEnabled: true, diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 97be2a3f..937b0d72 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -1,14 +1,13 @@ import { Session } from "../session.js"; import { BaseEvent, type ToolEvent } from "./types.js"; -import pkg from "../../package.json" with { type: "json" }; import config from "../config.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; import { ApiClient } from "../common/atlas/apiClient.js"; import fs from "fs/promises"; import path from "path"; +import { MACHINE_METADATA } from "./constants.js"; -const TELEMETRY_ENABLED = config.telemetry !== "disabled"; const CACHE_FILE = path.join(process.cwd(), ".telemetry-cache.json"); interface TelemetryError extends Error { @@ -39,24 +38,26 @@ export class Telemetry { constructor(private readonly session: Session) { // Ensure all required properties are present this.commonProperties = Object.freeze({ - device_id: config.device_id, - mcp_server_version: pkg.version, - mcp_server_name: config.mcpServerName, + ...MACHINE_METADATA, mcp_client_version: this.session.agentRunner?.version, mcp_client_name: this.session.agentRunner?.name, - platform: config.platform, - arch: config.arch, - os_type: config.os_type, - os_version: config.os_version, }); } + /** + * Checks if telemetry is currently enabled + * This is a method rather than a constant to capture runtime config changes + */ + private static isTelemetryEnabled(): boolean { + return config.telemetry !== "disabled"; + } + /** * Emits events through the telemetry pipeline * @param events - The events to emit */ public async emitEvents(events: BaseEvent[]): Promise { - if (!TELEMETRY_ENABLED) { + if (!Telemetry.isTelemetryEnabled()) { logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping events."); return; } From f18b91612991a53ea6d563c5042e17019ce73792 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 15:28:36 +0100 Subject: [PATCH 17/46] address comment: inject config to constructor --- src/session.ts | 14 ++++++++------ src/telemetry/constants.ts | 15 +++++++++++++++ src/telemetry/telemetry.ts | 1 + 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 src/telemetry/constants.ts diff --git a/src/session.ts b/src/session.ts index 0c38e7db..a744ed11 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,6 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { ApiClient } from "./common/atlas/apiClient.js"; -import config from "./config.js"; +import defaultConfig from "./config.js"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; export class Session { @@ -13,15 +13,17 @@ export class Session { }; private credentials?: { clientId: string; clientSecret: string }; private baseUrl: string; + private readonly config: any; - constructor() { - this.baseUrl = config.apiBaseUrl ?? "https://cloud.mongodb.com/"; + constructor(config = defaultConfig) { + this.config = config; + this.baseUrl = this.config.apiBaseUrl ?? "https://cloud.mongodb.com/"; // Store credentials if available - if (config.apiClientId && config.apiClientSecret) { + if (this.config.apiClientId && this.config.apiClientSecret) { this.credentials = { - clientId: config.apiClientId, - clientSecret: config.apiClientSecret, + clientId: this.config.apiClientId, + clientSecret: this.config.apiClientSecret, }; // Initialize API client with credentials diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts new file mode 100644 index 00000000..1b125180 --- /dev/null +++ b/src/telemetry/constants.ts @@ -0,0 +1,15 @@ +import pkg from "../../package.json" with { type: "json" }; +import config from "../config.js"; + +/** + * Machine-specific metadata formatted for telemetry + */ +export const MACHINE_METADATA = Object.freeze({ + device_id: "id", // TODO: use @mongodb-js/machine-id + mcp_server_version: pkg.version, + mcp_server_name: config.mcpServerName, + platform: process.platform, + arch: process.arch, + os_type: process.platform, + os_version: process.version, +}); diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 937b0d72..0e2ab92c 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -93,6 +93,7 @@ export class Telemetry { const result = await this.sendEvents(this.session.apiClient, allEvents); if (result.success) { await this.clearCache(); + logger.debug(mongoLogId(1_000_000), "telemetry", `Sent ${allEvents.length} events successfully`); return; } From a9cd835dce533d925ad5ecbbbf3d6ef38182522c Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 15:46:45 +0100 Subject: [PATCH 18/46] feat: use machineid library --- package-lock.json | 32 +++++++++++++++++++++++++++--- package.json | 1 + src/telemetry/constants.ts | 3 ++- src/types/native-machine-id.d.ts | 34 ++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/types/native-machine-id.d.ts diff --git a/package-lock.json b/package-lock.json index bf0be3f9..cf68e1e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "jest-environment-node": "^29.7.0", "jest-extended": "^4.0.2", "mongodb-runner": "^5.8.2", + "native-machine-id": "^0.0.8", "openapi-types": "^12.1.3", "openapi-typescript": "^7.6.1", "prettier": "^3.5.3", @@ -6643,8 +6644,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -8675,8 +8676,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/filelist": { "version": "1.0.4", @@ -11359,6 +11360,31 @@ "license": "MIT", "optional": true }, + "node_modules/native-machine-id": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/native-machine-id/-/native-machine-id-0.0.8.tgz", + "integrity": "sha512-0sMw6WHfG1A7N59C1odmge9K/F9uC+1dgXHjMW57w319ii/nI05FDFwlXSjPMAHHB7hU7OInpVuH+Sgjz5enog==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.0.0" + }, + "bin": { + "native-machine-id": "dist/bin/machine-id.js" + } + }, + "node_modules/native-machine-id/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index ca9d20d0..58a60a37 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "jest-environment-node": "^29.7.0", "jest-extended": "^4.0.2", "mongodb-runner": "^5.8.2", + "native-machine-id": "^0.0.8", "openapi-types": "^12.1.3", "openapi-typescript": "^7.6.1", "prettier": "^3.5.3", diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts index 1b125180..294417e1 100644 --- a/src/telemetry/constants.ts +++ b/src/telemetry/constants.ts @@ -1,11 +1,12 @@ import pkg from "../../package.json" with { type: "json" }; import config from "../config.js"; +import { getMachineIdSync } from 'native-machine-id'; /** * Machine-specific metadata formatted for telemetry */ export const MACHINE_METADATA = Object.freeze({ - device_id: "id", // TODO: use @mongodb-js/machine-id + device_id: getMachineIdSync(), mcp_server_version: pkg.version, mcp_server_name: config.mcpServerName, platform: process.platform, diff --git a/src/types/native-machine-id.d.ts b/src/types/native-machine-id.d.ts new file mode 100644 index 00000000..15911ca0 --- /dev/null +++ b/src/types/native-machine-id.d.ts @@ -0,0 +1,34 @@ +/** + * Type definitions for native-machine-id + * Provides functionality to retrieve the machine ID of the current device. + */ + +declare module 'native-machine-id' { + /** + * Gets the machine ID synchronously. + * @returns A string containing the machine ID. + */ + export function getMachineIdSync(): string; + + /** + * Gets the machine ID asynchronously. + * @returns A Promise that resolves to a string containing the machine ID. + */ + export function getMachineId(): Promise; + + /** + * Gets a machine ID that is based on the original ID but is "hashed" for privacy. + * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. + * @param {string} [type='md5'] - The hashing algorithm to use. + * @returns A Promise that resolves to a string containing the hashed machine ID. + */ + export function machineIdSync(original?: string, type?: string): string; + + /** + * Gets a machine ID that is based on the original ID but is "hashed" for privacy. + * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. + * @param {string} [type='md5'] - The hashing algorithm to use. + * @returns A Promise that resolves to a string containing the hashed machine ID. + */ + export function machineId(original?: string, type?: string): Promise; +} From 6813518947fadfd7a44aa4421fbdc62b4c95ff9b Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 15:47:01 +0100 Subject: [PATCH 19/46] chore: reformat --- src/config.ts | 9 ----- src/telemetry/constants.ts | 2 +- src/types/native-machine-id.d.ts | 56 ++++++++++++++++---------------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/config.ts b/src/config.ts index 91659479..ccb3a10e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -40,15 +40,6 @@ const mergedUserConfig = { ...getCliConfig(), }; -// Machine-specific metadata that isn't configurable -export const machineMetadata = { - device_id: "id", // TODO: use @mongodb-js/machine-id - platform: process.platform, - arch: process.arch, - os_type: process.platform, - os_version: process.version, -}; - const config = { ...mergedUserConfig, version: packageJson.version, diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts index 294417e1..78709405 100644 --- a/src/telemetry/constants.ts +++ b/src/telemetry/constants.ts @@ -1,6 +1,6 @@ import pkg from "../../package.json" with { type: "json" }; import config from "../config.js"; -import { getMachineIdSync } from 'native-machine-id'; +import { getMachineIdSync } from "native-machine-id"; /** * Machine-specific metadata formatted for telemetry diff --git a/src/types/native-machine-id.d.ts b/src/types/native-machine-id.d.ts index 15911ca0..153dbf38 100644 --- a/src/types/native-machine-id.d.ts +++ b/src/types/native-machine-id.d.ts @@ -3,32 +3,32 @@ * Provides functionality to retrieve the machine ID of the current device. */ -declare module 'native-machine-id' { - /** - * Gets the machine ID synchronously. - * @returns A string containing the machine ID. - */ - export function getMachineIdSync(): string; - - /** - * Gets the machine ID asynchronously. - * @returns A Promise that resolves to a string containing the machine ID. - */ - export function getMachineId(): Promise; - - /** - * Gets a machine ID that is based on the original ID but is "hashed" for privacy. - * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. - * @param {string} [type='md5'] - The hashing algorithm to use. - * @returns A Promise that resolves to a string containing the hashed machine ID. - */ - export function machineIdSync(original?: string, type?: string): string; - - /** - * Gets a machine ID that is based on the original ID but is "hashed" for privacy. - * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. - * @param {string} [type='md5'] - The hashing algorithm to use. - * @returns A Promise that resolves to a string containing the hashed machine ID. - */ - export function machineId(original?: string, type?: string): Promise; +declare module "native-machine-id" { + /** + * Gets the machine ID synchronously. + * @returns A string containing the machine ID. + */ + export function getMachineIdSync(): string; + + /** + * Gets the machine ID asynchronously. + * @returns A Promise that resolves to a string containing the machine ID. + */ + export function getMachineId(): Promise; + + /** + * Gets a machine ID that is based on the original ID but is "hashed" for privacy. + * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. + * @param {string} [type='md5'] - The hashing algorithm to use. + * @returns A Promise that resolves to a string containing the hashed machine ID. + */ + export function machineIdSync(original?: string, type?: string): string; + + /** + * Gets a machine ID that is based on the original ID but is "hashed" for privacy. + * @param {string} [original] - The original ID to hash. If not provided, gets the machine ID first. + * @param {string} [type='md5'] - The hashing algorithm to use. + * @returns A Promise that resolves to a string containing the hashed machine ID. + */ + export function machineId(original?: string, type?: string): Promise; } From 65fad1d3bc04830e1c5f1358eadf74f464b10aea Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 15:57:36 +0100 Subject: [PATCH 20/46] chore: reformat --- src/server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 107994ec..07aa4ec5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,7 +25,11 @@ export class Server { this.mcpServer.server.oninitialized = () => { this.session.setAgentRunner(this.mcpServer.server.getClientVersion()); - logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); + logger.info( + mongoLogId(1_000_004), + "server", + `Server started with transport ${transport.constructor.name} and agent runner ${this.session.agentRunner?.name}` + ); }; } From 7e98c33ff6344f635070fe63e814cd3f38866abd Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 16:02:32 +0100 Subject: [PATCH 21/46] address comment: fix misleading comment --- src/telemetry/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 0e2ab92c..c78eeed5 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -36,7 +36,7 @@ export class Telemetry { private readonly commonProperties: CommonProperties; constructor(private readonly session: Session) { - // Ensure all required properties are present + // Create an immutable object with all telemetry properties this.commonProperties = Object.freeze({ ...MACHINE_METADATA, mcp_client_version: this.session.agentRunner?.version, From 52090738f964bb253ac9fd3c148e57cf51c50085 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 16:10:04 +0100 Subject: [PATCH 22/46] address npm check --- src/session.ts | 12 ++++++++++-- src/telemetry/telemetry.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/session.ts b/src/session.ts index a744ed11..54726bc6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,6 +3,14 @@ import { ApiClient } from "./common/atlas/apiClient.js"; import defaultConfig from "./config.js"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; +// Define the type for configuration used by Session +interface SessionConfig { + apiBaseUrl?: string; + apiClientId?: string; + apiClientSecret?: string; + [key: string]: unknown; +} + export class Session { sessionId?: string; serviceProvider?: NodeDriverServiceProvider; @@ -13,9 +21,9 @@ export class Session { }; private credentials?: { clientId: string; clientSecret: string }; private baseUrl: string; - private readonly config: any; + private readonly config: SessionConfig; - constructor(config = defaultConfig) { + constructor(config: SessionConfig = defaultConfig as SessionConfig) { this.config = config; this.baseUrl = this.config.apiBaseUrl ?? "https://cloud.mongodb.com/"; diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index c78eeed5..dd784e81 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -1,5 +1,5 @@ import { Session } from "../session.js"; -import { BaseEvent, type ToolEvent } from "./types.js"; +import { BaseEvent } from "./types.js"; import config from "../config.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; From 2e33584dfb6be20447b9db2e0d1615f020a3a6ce Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Tue, 22 Apr 2025 16:48:28 +0100 Subject: [PATCH 23/46] address comment: support do_not_track --- src/telemetry/telemetry.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index dd784e81..fba91f72 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -47,9 +47,27 @@ export class Telemetry { /** * Checks if telemetry is currently enabled * This is a method rather than a constant to capture runtime config changes + * + * Follows the Console Do Not Track standard (https://consoledonottrack.com/) + * by respecting the DO_NOT_TRACK environment variable */ private static isTelemetryEnabled(): boolean { - return config.telemetry !== "disabled"; + // Check if telemetry is explicitly disabled in config + if (config.telemetry === "disabled") { + return false; + } + + // Check for DO_NOT_TRACK environment variable as per https://consoledonottrack.com/ + const doNotTrack = process.env.DO_NOT_TRACK; + if (doNotTrack) { + const value = doNotTrack.toLowerCase(); + // Telemetry should be disabled if DO_NOT_TRACK is "1", "true", or "yes" + if (value === "1" || value === "true" || value === "yes") { + return false; + } + } + + return true; } /** From 8ec7d9c751ce521b78af1fffd90cc6f26b78ec31 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 11:04:30 +0100 Subject: [PATCH 24/46] address comment: use in-memory cache --- src/telemetry/eventCache.ts | 0 src/telemetry/telemetry.ts | 54 ++++++++++++++----------------------- 2 files changed, 20 insertions(+), 34 deletions(-) create mode 100644 src/telemetry/eventCache.ts diff --git a/src/telemetry/eventCache.ts b/src/telemetry/eventCache.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index fba91f72..556e7670 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -4,15 +4,8 @@ import config from "../config.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; import { ApiClient } from "../common/atlas/apiClient.js"; -import fs from "fs/promises"; -import path from "path"; import { MACHINE_METADATA } from "./constants.js"; - -const CACHE_FILE = path.join(process.cwd(), ".telemetry-cache.json"); - -interface TelemetryError extends Error { - code?: string; -} +import { EventCache } from "./eventCache.js"; type EventResult = { success: boolean; @@ -57,7 +50,6 @@ export class Telemetry { return false; } - // Check for DO_NOT_TRACK environment variable as per https://consoledonottrack.com/ const doNotTrack = process.env.DO_NOT_TRACK; if (doNotTrack) { const value = doNotTrack.toLowerCase(); @@ -135,38 +127,34 @@ export class Telemetry { } /** - * Reads cached events from disk - * Returns empty array if no cache exists or on read error + * Reads cached events from memory + * Returns empty array if no cache exists */ private async readCache(): Promise { try { - const data = await fs.readFile(CACHE_FILE, "utf-8"); - return JSON.parse(data) as BaseEvent[]; + return EventCache.getInstance().getEvents(); } catch (error) { - const typedError = error as TelemetryError; - if (typedError.code !== "ENOENT") { - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Error reading telemetry cache: ${typedError.message}` - ); - } + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Error reading telemetry cache from memory: ${error instanceof Error ? error.message : String(error)}` + ); return []; } } /** - * Caches events to disk for later sending + * Caches events in memory for later sending */ private async cacheEvents(events: BaseEvent[]): Promise { try { - await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2)); - logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`); + EventCache.getInstance().setEvents(events); + logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events in memory for later sending`); } catch (error) { logger.warning( mongoLogId(1_000_000), "telemetry", - `Failed to cache telemetry events: ${error instanceof Error ? error.message : String(error)}` + `Failed to cache telemetry events in memory: ${error instanceof Error ? error.message : String(error)}` ); } } @@ -176,16 +164,14 @@ export class Telemetry { */ private async clearCache(): Promise { try { - await fs.unlink(CACHE_FILE); + EventCache.getInstance().clearEvents(); + logger.debug(mongoLogId(1_000_000), "telemetry", "In-memory telemetry cache cleared"); } catch (error) { - const typedError = error as TelemetryError; - if (typedError.code !== "ENOENT") { - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Error clearing telemetry cache: ${typedError.message}` - ); - } + logger.warning( + mongoLogId(1_000_000), + "telemetry", + `Error clearing in-memory telemetry cache: ${error instanceof Error ? error.message : String(error)}` + ); } } } From 6ad2a19f0cdd380e83e23a6f06ea34764324dec4 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 11:08:25 +0100 Subject: [PATCH 25/46] address comments: use const --- src/config.ts | 7 +++++-- src/telemetry/telemetry.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index ccb3a10e..41926674 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +5,9 @@ import argv from "yargs-parser"; import packageJson from "../package.json" with { type: "json" }; import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb"; +export const SERVER_NAME = "MdbMcpServer"; +export const SERVER_VERSION = packageJson.version; + // If we decide to support non-string config options, we'll need to extend the mechanism for parsing // env variables. interface UserConfig { @@ -42,8 +45,8 @@ const mergedUserConfig = { const config = { ...mergedUserConfig, - version: packageJson.version, - mcpServerName: "MdbMcpServer", + version: SERVER_VERSION, + mcpServerName: SERVER_NAME, isTelemetryEnabled: true, }; diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 556e7670..1d574a56 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -149,7 +149,11 @@ export class Telemetry { private async cacheEvents(events: BaseEvent[]): Promise { try { EventCache.getInstance().setEvents(events); - logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events in memory for later sending`); + logger.debug( + mongoLogId(1_000_000), + "telemetry", + `Cached ${events.length} events in memory for later sending` + ); } catch (error) { logger.warning( mongoLogId(1_000_000), From f9a46f9df574622378a0f0804a4f200b6cf662ff Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 11:29:09 +0100 Subject: [PATCH 26/46] chore: fix lint and tests --- src/telemetry/eventCache.ts | 46 +++++++++++++++++++++++++++++++++++++ src/telemetry/telemetry.ts | 12 +++++----- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/telemetry/eventCache.ts b/src/telemetry/eventCache.ts index e69de29b..d4300eac 100644 --- a/src/telemetry/eventCache.ts +++ b/src/telemetry/eventCache.ts @@ -0,0 +1,46 @@ +import { BaseEvent } from "./types.js"; + +/** + * Singleton class for in-memory telemetry event caching + * Provides a central storage for telemetry events that couldn't be sent + */ +export class EventCache { + private static instance: EventCache; + private events: BaseEvent[] = []; + + private constructor() {} + + /** + * Gets the singleton instance of EventCache + * @returns The EventCache instance + */ + public static getInstance(): EventCache { + if (!EventCache.instance) { + EventCache.instance = new EventCache(); + } + return EventCache.instance; + } + + /** + * Gets a copy of the currently cached events + * @returns Array of cached BaseEvent objects + */ + public getEvents(): BaseEvent[] { + return [...this.events]; + } + + /** + * Sets the cached events, replacing any existing events + * @param events - The events to cache + */ + public setEvents(events: BaseEvent[]): void { + this.events = [...events]; + } + + /** + * Clears all cached events + */ + public clearEvents(): void { + this.events = []; + } +} diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 1d574a56..87ad2c85 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -91,7 +91,7 @@ export class Telemetry { * Falls back to caching if both attempts fail */ private async emit(events: BaseEvent[]): Promise { - const cachedEvents = await this.readCache(); + const cachedEvents = this.readCache(); const allEvents = [...cachedEvents, ...events]; logger.debug( @@ -102,13 +102,13 @@ export class Telemetry { const result = await this.sendEvents(this.session.apiClient, allEvents); if (result.success) { - await this.clearCache(); + this.clearCache(); logger.debug(mongoLogId(1_000_000), "telemetry", `Sent ${allEvents.length} events successfully`); return; } logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to client: ${result.error}`); - await this.cacheEvents(allEvents); + this.cacheEvents(allEvents); } /** @@ -130,7 +130,7 @@ export class Telemetry { * Reads cached events from memory * Returns empty array if no cache exists */ - private async readCache(): Promise { + private readCache(): BaseEvent[] { try { return EventCache.getInstance().getEvents(); } catch (error) { @@ -146,7 +146,7 @@ export class Telemetry { /** * Caches events in memory for later sending */ - private async cacheEvents(events: BaseEvent[]): Promise { + private cacheEvents(events: BaseEvent[]): void { try { EventCache.getInstance().setEvents(events); logger.debug( @@ -166,7 +166,7 @@ export class Telemetry { /** * Clears the event cache after successful sending */ - private async clearCache(): Promise { + private clearCache(): void { try { EventCache.getInstance().clearEvents(); logger.debug(mongoLogId(1_000_000), "telemetry", "In-memory telemetry cache cleared"); From 599201d53c71856610b30c02e752f931f6010e2f Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 11:51:10 +0100 Subject: [PATCH 27/46] address comment: remove unused const --- src/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 41926674..6b44c849 100644 --- a/src/config.ts +++ b/src/config.ts @@ -47,7 +47,6 @@ const config = { ...mergedUserConfig, version: SERVER_VERSION, mcpServerName: SERVER_NAME, - isTelemetryEnabled: true, }; export default config; From 7a889b407d9cf32d8e0b46aa6dc1a25953d79269 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 11:59:30 +0100 Subject: [PATCH 28/46] address comment: do not use object freeze --- src/telemetry/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts index 78709405..73d4069b 100644 --- a/src/telemetry/constants.ts +++ b/src/telemetry/constants.ts @@ -5,7 +5,7 @@ import { getMachineIdSync } from "native-machine-id"; /** * Machine-specific metadata formatted for telemetry */ -export const MACHINE_METADATA = Object.freeze({ +export const MACHINE_METADATA = { device_id: getMachineIdSync(), mcp_server_version: pkg.version, mcp_server_name: config.mcpServerName, @@ -13,4 +13,4 @@ export const MACHINE_METADATA = Object.freeze({ arch: process.arch, os_type: process.platform, os_version: process.version, -}); +}; From eb360e2af6ca3b5a2e954466eeacb8e37fd80007 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 12:07:17 +0100 Subject: [PATCH 29/46] address comment: as const --- src/telemetry/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts index 73d4069b..94e63a0c 100644 --- a/src/telemetry/constants.ts +++ b/src/telemetry/constants.ts @@ -13,4 +13,4 @@ export const MACHINE_METADATA = { arch: process.arch, os_type: process.platform, os_version: process.version, -}; +} as const; From 710b1316010bcdbdbc5e21ed2981a0dade31afda Mon Sep 17 00:00:00 2001 From: Bianca Lisle <40155621+blva@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:32:07 +0100 Subject: [PATCH 30/46] Update src/common/atlas/apiClient.ts Co-authored-by: Filipe Constantinov Menezes --- src/common/atlas/apiClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 056ec650..b112f3af 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -66,7 +66,7 @@ export class ApiClient { constructor(options?: ApiClientOptions) { this.options = { ...options, - baseUrl: options?.baseUrl || "https://cloud-dev.mongodb.com/", + baseUrl: options?.baseUrl || "https://cloud.mongodb.com/", userAgent: options?.userAgent || `${config.mcpServerName}/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, From a15eeb64c5ef2ee09c1ef23cabd9eb138ed684d9 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 13:06:18 +0100 Subject: [PATCH 31/46] address comment: make common props readonly --- src/telemetry/telemetry.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 87ad2c85..d08f73b8 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -29,12 +29,11 @@ export class Telemetry { private readonly commonProperties: CommonProperties; constructor(private readonly session: Session) { - // Create an immutable object with all telemetry properties - this.commonProperties = Object.freeze({ + this.commonProperties = { ...MACHINE_METADATA, mcp_client_version: this.session.agentRunner?.version, mcp_client_name: this.session.agentRunner?.name, - }); + }; } /** From 89113a54ebba975660accf4b96dd5679c0803a74 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 14:36:12 +0100 Subject: [PATCH 32/46] address comment: do not use enum --- src/telemetry/types.ts | 7 +------ src/tools/tool.ts | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index ae5bbb26..41791479 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -1,12 +1,7 @@ /** * Result type constants for telemetry events */ -export const TELEMETRY_RESULT = { - SUCCESS: "success" as const, - FAILURE: "failure" as const, -}; - -export type TelemetryResult = (typeof TELEMETRY_RESULT)[keyof typeof TELEMETRY_RESULT]; +export type TelemetryResult = "success" | "failure"; /** * Base interface for all events diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 37dc58dc..dfaac096 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -74,14 +74,14 @@ export abstract class ToolBase { ); const result = await this.execute(...args); - await this.emitToolEvent(startTime, TELEMETRY_RESULT.SUCCESS); + await this.emitToolEvent(startTime, "success"); return result; } catch (error: unknown) { logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); await this.emitToolEvent( startTime, - TELEMETRY_RESULT.FAILURE, + "failure", error instanceof Error ? error : new Error(String(error)) ); From 143f8988503e0ef7411f14b68ded4de5d1048d29 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 14:55:19 +0100 Subject: [PATCH 33/46] fix --- src/tools/tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/tool.ts b/src/tools/tool.ts index dfaac096..a427d240 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -6,7 +6,7 @@ import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; import config from "../config.js"; import { Telemetry } from "../telemetry/telemetry.js"; -import { type ToolEvent, TELEMETRY_RESULT, type TelemetryResult } from "../telemetry/types.js"; +import { type ToolEvent, type TelemetryResult } from "../telemetry/types.js"; export type ToolArgs = z.objectOutputType; From f751a30e8cf6dcfc451dce64209d8e26e2ddad18 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:09:00 +0100 Subject: [PATCH 34/46] address comment: inject and use eventcache --- src/telemetry/telemetry.ts | 63 +++----------------------------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index d08f73b8..4d08a0d6 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -28,7 +28,7 @@ type CommonProperties = { export class Telemetry { private readonly commonProperties: CommonProperties; - constructor(private readonly session: Session) { + constructor(private readonly session: Session, private readonly eventCache: EventCache = EventCache.getInstance()) { this.commonProperties = { ...MACHINE_METADATA, mcp_client_version: this.session.agentRunner?.version, @@ -90,7 +90,7 @@ export class Telemetry { * Falls back to caching if both attempts fail */ private async emit(events: BaseEvent[]): Promise { - const cachedEvents = this.readCache(); + const cachedEvents = this.eventCache.getEvents(); const allEvents = [...cachedEvents, ...events]; logger.debug( @@ -101,13 +101,13 @@ export class Telemetry { const result = await this.sendEvents(this.session.apiClient, allEvents); if (result.success) { - this.clearCache(); + this.eventCache.clearEvents(); logger.debug(mongoLogId(1_000_000), "telemetry", `Sent ${allEvents.length} events successfully`); return; } logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to client: ${result.error}`); - this.cacheEvents(allEvents); + this.eventCache.setEvents(allEvents); } /** @@ -124,57 +124,4 @@ export class Telemetry { }; } } - - /** - * Reads cached events from memory - * Returns empty array if no cache exists - */ - private readCache(): BaseEvent[] { - try { - return EventCache.getInstance().getEvents(); - } catch (error) { - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Error reading telemetry cache from memory: ${error instanceof Error ? error.message : String(error)}` - ); - return []; - } - } - - /** - * Caches events in memory for later sending - */ - private cacheEvents(events: BaseEvent[]): void { - try { - EventCache.getInstance().setEvents(events); - logger.debug( - mongoLogId(1_000_000), - "telemetry", - `Cached ${events.length} events in memory for later sending` - ); - } catch (error) { - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Failed to cache telemetry events in memory: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - /** - * Clears the event cache after successful sending - */ - private clearCache(): void { - try { - EventCache.getInstance().clearEvents(); - logger.debug(mongoLogId(1_000_000), "telemetry", "In-memory telemetry cache cleared"); - } catch (error) { - logger.warning( - mongoLogId(1_000_000), - "telemetry", - `Error clearing in-memory telemetry cache: ${error instanceof Error ? error.message : String(error)}` - ); - } - } -} +} \ No newline at end of file From 0927d28d4ce62f3e1030c972ca4f37ae36d3dfc9 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:35:57 +0100 Subject: [PATCH 35/46] minor fixeS --- src/telemetry/telemetry.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 4d08a0d6..85e81924 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -31,8 +31,6 @@ export class Telemetry { constructor(private readonly session: Session, private readonly eventCache: EventCache = EventCache.getInstance()) { this.commonProperties = { ...MACHINE_METADATA, - mcp_client_version: this.session.agentRunner?.version, - mcp_client_name: this.session.agentRunner?.name, }; } @@ -66,12 +64,16 @@ export class Telemetry { * @param events - The events to emit */ public async emitEvents(events: BaseEvent[]): Promise { - if (!Telemetry.isTelemetryEnabled()) { - logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping events."); - return; - } - - await this.emit(events); + try { + if (!Telemetry.isTelemetryEnabled()) { + logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping events."); + return; + } + + await this.emit(events); + } catch (error) { + logger.debug(mongoLogId(1_000_002), "telemetry", `Error emitting telemetry events: ${error}`); + } } /** @@ -81,6 +83,8 @@ export class Telemetry { public getCommonProperties(): CommonProperties { return { ...this.commonProperties, + mcp_client_version: this.session.agentRunner?.version, + mcp_client_name: this.session.agentRunner?.name, session_id: this.session.sessionId, }; } @@ -94,7 +98,7 @@ export class Telemetry { const allEvents = [...cachedEvents, ...events]; logger.debug( - mongoLogId(1_000_000), + mongoLogId(1_000_003), "telemetry", `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)` ); @@ -102,11 +106,11 @@ export class Telemetry { const result = await this.sendEvents(this.session.apiClient, allEvents); if (result.success) { this.eventCache.clearEvents(); - logger.debug(mongoLogId(1_000_000), "telemetry", `Sent ${allEvents.length} events successfully`); + logger.debug(mongoLogId(1_000_004), "telemetry", `Sent ${allEvents.length} events successfully`); return; } - logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to client: ${result.error}`); + logger.warning(mongoLogId(1_000_005), "telemetry", `Error sending event to client: ${result.error}`); this.eventCache.setEvents(allEvents); } From fb0b8af84278e4a57bd4153acc8d950e24859737 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:36:31 +0100 Subject: [PATCH 36/46] chore: gen sessionId --- src/server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server.ts b/src/server.ts index 2f20520d..29290844 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,6 +6,7 @@ import { MongoDbTools } from "./tools/mongodb/tools.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; import config from "./config.js"; +import { ObjectId } from "mongodb"; export class Server { public readonly session: Session; @@ -27,6 +28,8 @@ export class Server { this.mcpServer.server.oninitialized = () => { this.session.setAgentRunner(this.mcpServer.server.getClientVersion()); + this.session.sessionId = new ObjectId().toString(); + logger.info( mongoLogId(1_000_004), "server", From 9e625aa963224ff3956028ae6ddc2c99af6c2612 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:37:03 +0100 Subject: [PATCH 37/46] add lru cache --- package-lock.json | 1 + package.json | 1 + src/telemetry/eventCache.ts | 32 ++++++++++++++++++++++++-------- src/telemetry/telemetry.ts | 8 ++++++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index fead3db9..25f3c06b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", + "lru-cache": "^11.1.0", "mongodb": "^6.15.0", "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", diff --git a/package.json b/package.json index 58a60a37..5c211101 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", + "lru-cache": "^11.1.0", "mongodb": "^6.15.0", "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", diff --git a/src/telemetry/eventCache.ts b/src/telemetry/eventCache.ts index d4300eac..49025227 100644 --- a/src/telemetry/eventCache.ts +++ b/src/telemetry/eventCache.ts @@ -1,14 +1,26 @@ import { BaseEvent } from "./types.js"; +import { LRUCache } from "lru-cache"; /** * Singleton class for in-memory telemetry event caching * Provides a central storage for telemetry events that couldn't be sent + * Uses LRU cache to automatically drop oldest events when limit is exceeded */ export class EventCache { private static instance: EventCache; - private events: BaseEvent[] = []; + private static readonly MAX_EVENTS = 1000; - private constructor() {} + private cache: LRUCache; + private nextId = 0; + + private constructor() { + this.cache = new LRUCache({ + max: EventCache.MAX_EVENTS, + // Using FIFO eviction strategy for events + allowStale: false, + updateAgeOnGet: false, + }); + } /** * Gets the singleton instance of EventCache @@ -26,21 +38,25 @@ export class EventCache { * @returns Array of cached BaseEvent objects */ public getEvents(): BaseEvent[] { - return [...this.events]; + return Array.from(this.cache.values()); } /** - * Sets the cached events, replacing any existing events - * @param events - The events to cache + * Appends new events to the cached events + * LRU cache automatically handles dropping oldest events when limit is exceeded + * @param events - The events to append */ - public setEvents(events: BaseEvent[]): void { - this.events = [...events]; + public appendEvents(events: BaseEvent[]): void { + for (const event of events) { + this.cache.set(this.nextId++, event); + } } /** * Clears all cached events */ public clearEvents(): void { - this.events = []; + this.cache.clear(); + this.nextId = 0; } } diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 85e81924..beaa2c64 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -110,8 +110,12 @@ export class Telemetry { return; } - logger.warning(mongoLogId(1_000_005), "telemetry", `Error sending event to client: ${result.error}`); - this.eventCache.setEvents(allEvents); + logger.warning( + mongoLogId(1_000_005), + "telemetry", + `Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}` + ); + this.eventCache.appendEvents(events); } /** From a61d9b43276616d19880d55a3bac10a9892209fd Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:45:57 +0100 Subject: [PATCH 38/46] chore: disable telemetry in tests --- tests/integration/helpers.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index e25ee222..d7e3b96e 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -69,6 +69,7 @@ export function setupIntegrationTest(): IntegrationTest { }); beforeEach(async () => { + config.telemetry = "disabled"; randomDbName = new ObjectId().toString(); }); @@ -242,3 +243,28 @@ export function describeAtlas(name: number | string | Function | jest.FunctionLi describe(name, fn); }); } + +// Telemetry control functions for tests +export function disableTelemetry(): void { + process.env.DO_NOT_TRACK = "1"; +} + +export function enableTelemetry(): void { + delete process.env.DO_NOT_TRACK; +} + +export function withTelemetryDisabled(fn: () => Promise | void): () => Promise { + return async () => { + const originalValue = process.env.DO_NOT_TRACK; + disableTelemetry(); + try { + await fn(); + } finally { + if (originalValue === undefined) { + delete process.env.DO_NOT_TRACK; + } else { + process.env.DO_NOT_TRACK = originalValue; + } + } + }; +} From 3636fdea9c88bc05b36133e67c3eacd8d99c69bf Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:47:03 +0100 Subject: [PATCH 39/46] chore: reformat --- src/telemetry/telemetry.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index beaa2c64..433dc0bf 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -28,7 +28,10 @@ type CommonProperties = { export class Telemetry { private readonly commonProperties: CommonProperties; - constructor(private readonly session: Session, private readonly eventCache: EventCache = EventCache.getInstance()) { + constructor( + private readonly session: Session, + private readonly eventCache: EventCache = EventCache.getInstance() + ) { this.commonProperties = { ...MACHINE_METADATA, }; @@ -69,11 +72,11 @@ export class Telemetry { logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping events."); return; } - + await this.emit(events); } catch (error) { logger.debug(mongoLogId(1_000_002), "telemetry", `Error emitting telemetry events: ${error}`); - } + } } /** @@ -111,8 +114,8 @@ export class Telemetry { } logger.warning( - mongoLogId(1_000_005), - "telemetry", + mongoLogId(1_000_005), + "telemetry", `Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}` ); this.eventCache.appendEvents(events); @@ -132,4 +135,4 @@ export class Telemetry { }; } } -} \ No newline at end of file +} From 0df870cd63dc8fe13f36e832fb26b3680cfe5c84 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:48:10 +0100 Subject: [PATCH 40/46] clean up --- tests/integration/helpers.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index dad19f7c..90fb0026 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -327,28 +327,3 @@ export function describeAtlas(name: number | string | Function | jest.FunctionLi describe(name, fn); }); } - -// Telemetry control functions for tests -export function disableTelemetry(): void { - process.env.DO_NOT_TRACK = "1"; -} - -export function enableTelemetry(): void { - delete process.env.DO_NOT_TRACK; -} - -export function withTelemetryDisabled(fn: () => Promise | void): () => Promise { - return async () => { - const originalValue = process.env.DO_NOT_TRACK; - disableTelemetry(); - try { - await fn(); - } finally { - if (originalValue === undefined) { - delete process.env.DO_NOT_TRACK; - } else { - process.env.DO_NOT_TRACK = originalValue; - } - } - }; -} From 4b7563dc2fb600588b7b47f86f8080a2c43425ad Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 15:55:52 +0100 Subject: [PATCH 41/46] fix check --- src/telemetry/telemetry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 433dc0bf..73ced0fe 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -74,8 +74,8 @@ export class Telemetry { } await this.emit(events); - } catch (error) { - logger.debug(mongoLogId(1_000_002), "telemetry", `Error emitting telemetry events: ${error}`); + } catch { + logger.debug(mongoLogId(1_000_002), "telemetry", `Error emitting telemetry events.`); } } From 6dd1f79093d6edee4d5ea011c9972792f8f0dede Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 17:14:01 +0100 Subject: [PATCH 42/46] address comment for category and update emit tool to get tool result object --- src/telemetry/types.ts | 7 ++++--- src/tools/tool.ts | 35 ++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index 41791479..4f24e545 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -22,6 +22,10 @@ export interface BaseEvent extends Event { platform: string; arch: string; os_type: string; + component: string; + duration_ms: number; + result: TelemetryResult; + category: string; os_version?: string; session_id?: string; } & Event["properties"]; @@ -33,9 +37,6 @@ export interface BaseEvent extends Event { export interface ToolEvent extends BaseEvent { properties: { command: string; - category: string; - duration_ms: number; - result: TelemetryResult; error_code?: string; error_type?: string; project_id?: string; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index c948ebc9..bc614ab1 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,6 +1,6 @@ import { z, type ZodRawShape, type ZodNever } from "zod"; import type { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; import { Session } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; @@ -38,8 +38,13 @@ export abstract class ToolBase { * @param result - Whether the command succeeded or failed * @param error - Optional error if the command failed */ - private async emitToolEvent(startTime: number, result: TelemetryResult, error?: Error): Promise { + private async emitToolEvent(startTime: number, result: CallToolResult): Promise { const duration = Date.now() - startTime; + logger.info( + mongoLogId(1_000_007), + "tool", + `Tool ${this.name} executed in ${duration}ms with result: ${result}` + ); const event: ToolEvent = { timestamp: new Date().toISOString(), source: "mdbmcp", @@ -47,15 +52,11 @@ export abstract class ToolBase { ...this.telemetry.getCommonProperties(), command: this.name, category: this.category, + component: "tool", duration_ms: duration, - result, - ...(error && { - error_type: error.name, - error_code: error.message, - }), + result: result.isError ? "failure" : "success", }, }; - await this.telemetry.emitEvents([event]); } @@ -74,17 +75,21 @@ export abstract class ToolBase { ); const result = await this.execute(...args); - await this.emitToolEvent(startTime, "success"); + await this.emitToolEvent(startTime, result); return result; } catch (error: unknown) { logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); - await this.emitToolEvent( - startTime, - "failure", - error instanceof Error ? error : new Error(String(error)) - ); - + const toolResult: CallToolResult = { + isError: true, + content: [ + { + type: "text", + text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + await this.emitToolEvent(startTime, toolResult); return await this.handleError(error, args[0] as ToolArgs); } }; From 807b2cab72c1738ba0271fce64672dfedf1d27cf Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 17:30:20 +0100 Subject: [PATCH 43/46] address comment: fix telemetry init --- src/server.ts | 5 ++++- src/tools/atlas/atlasTool.ts | 8 ++++++-- src/tools/mongodb/mongodbTool.ts | 5 +++-- src/tools/tool.ts | 18 ++++++------------ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/server.ts b/src/server.ts index 29290844..5bf6e345 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,14 +7,17 @@ import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; import config from "./config.js"; import { ObjectId } from "mongodb"; +import { Telemetry } from "./telemetry/telemetry.js"; export class Server { public readonly session: Session; private readonly mcpServer: McpServer; + private readonly telemetry: Telemetry; constructor({ mcpServer, session }: { mcpServer: McpServer; session: Session }) { this.mcpServer = mcpServer; this.session = session; + this.telemetry = new Telemetry(session); } async connect(transport: Transport) { @@ -45,7 +48,7 @@ export class Server { private registerTools() { for (const tool of [...AtlasTools, ...MongoDbTools]) { - new tool(this.session).register(this.mcpServer); + new tool(this.session, this.telemetry).register(this.mcpServer); } } diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 0c2cc0cb..70678f86 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,10 +1,14 @@ import { ToolBase, ToolCategory } from "../tool.js"; import { Session } from "../../session.js"; import config from "../../config.js"; +import { Telemetry } from "../../telemetry/telemetry.js"; export abstract class AtlasToolBase extends ToolBase { - constructor(protected readonly session: Session) { - super(session); + constructor( + protected readonly session: Session, + telemetry: Telemetry + ) { + super(session, telemetry); } protected category: ToolCategory = "atlas"; diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index b79c6b9f..61ddf42e 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -5,6 +5,7 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../errors.js"; import config from "../../config.js"; +import { Telemetry } from "../../telemetry/telemetry.js"; export const DbOperationArgs = { database: z.string().describe("Database name"), @@ -12,8 +13,8 @@ export const DbOperationArgs = { }; export abstract class MongoDBToolBase extends ToolBase { - constructor(session: Session) { - super(session); + constructor(session: Session, telemetry: Telemetry) { + super(session, telemetry); } protected category: ToolCategory = "mongodb"; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index bc614ab1..c283baba 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,12 +1,12 @@ import { z, type ZodRawShape, type ZodNever } from "zod"; import type { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Session } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; import config from "../config.js"; import { Telemetry } from "../telemetry/telemetry.js"; -import { type ToolEvent, type TelemetryResult } from "../telemetry/types.js"; +import { type ToolEvent } from "../telemetry/types.js"; export type ToolArgs = z.objectOutputType; @@ -24,13 +24,12 @@ export abstract class ToolBase { protected abstract argsShape: ZodRawShape; - private readonly telemetry: Telemetry; - protected abstract execute(...args: Parameters>): Promise; - protected constructor(protected session: Session) { - this.telemetry = new Telemetry(session); - } + protected constructor( + protected session: Session, + protected readonly telemetry: Telemetry + ) {} /** * Creates and emits a tool telemetry event @@ -40,11 +39,6 @@ export abstract class ToolBase { */ private async emitToolEvent(startTime: number, result: CallToolResult): Promise { const duration = Date.now() - startTime; - logger.info( - mongoLogId(1_000_007), - "tool", - `Tool ${this.name} executed in ${duration}ms with result: ${result}` - ); const event: ToolEvent = { timestamp: new Date().toISOString(), source: "mdbmcp", From 3a05d31c46ba35fe754a6a4fcac3ab96fd2c6967 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 17:45:03 +0100 Subject: [PATCH 44/46] format --- src/tools/atlas/atlasTool.ts | 2 +- src/tools/tool.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 3034dc10..b980a2bf 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -4,7 +4,7 @@ import { Telemetry } from "../../telemetry/telemetry.js"; export abstract class AtlasToolBase extends ToolBase { protected category: ToolCategory = "atlas"; - + protected verifyAllowed(): boolean { if (!this.config.apiClientId || !this.config.apiClientSecret) { return false; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 45ba9cac..67be02ec 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -54,7 +54,7 @@ export abstract class ToolBase { }; await this.telemetry.emitEvents([event]); } - + public register(server: McpServer): void { if (!this.verifyAllowed()) { return; From 8505d918315ff3eff88e30ee11f990cf5e4ef991 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 17:46:50 +0100 Subject: [PATCH 45/46] fix check --- src/tools/atlas/atlasTool.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index b980a2bf..6ca5282d 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,6 +1,4 @@ import { ToolBase, ToolCategory } from "../tool.js"; -import { Session } from "../../session.js"; -import { Telemetry } from "../../telemetry/telemetry.js"; export abstract class AtlasToolBase extends ToolBase { protected category: ToolCategory = "atlas"; From 024a3d1feeb7fb98494415b673bb8335ef0a5e56 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 23 Apr 2025 18:19:53 +0100 Subject: [PATCH 46/46] address comment --- src/tools/tool.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 67be02ec..5cbb8ac2 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -74,18 +74,9 @@ export abstract class ToolBase { return result; } catch (error: unknown) { logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); - - const toolResult: CallToolResult = { - isError: true, - content: [ - { - type: "text", - text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - await this.emitToolEvent(startTime, toolResult); - return await this.handleError(error, args[0] as ToolArgs); + const toolResult = await this.handleError(error, args[0] as ToolArgs); + await this.emitToolEvent(startTime, toolResult).catch(() => {}); + return toolResult; } }; @@ -129,7 +120,6 @@ export abstract class ToolBase { text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`, }, ], - isError: true, }; } }