Skip to content

Commit 80bc86e

Browse files
committed
Merge branch 'gagik/esm-jest' of github.com:mongodb-js/mongodb-mcp-server into gagik/use-machine-id
2 parents c869d63 + e0339a4 commit 80bc86e

File tree

14 files changed

+176
-85
lines changed

14 files changed

+176
-85
lines changed

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default defineConfig([
4848
"coverage",
4949
"global.d.ts",
5050
"eslint.config.js",
51-
"jest.config.js",
51+
"jest.config.ts",
5252
]),
5353
eslintPluginPrettierRecommended,
5454
]);

jest.config.js renamed to jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default {
1212
"ts-jest",
1313
{
1414
useESM: true,
15-
tsconfig: "tsconfig.jest.json", // Use specific tsconfig file for Jest
15+
tsconfig: "tsconfig.jest.json",
1616
},
1717
],
1818
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"check:types": "tsc --noEmit --project tsconfig.json",
3030
"reformat": "prettier --write .",
3131
"generate": "./scripts/generate.sh",
32-
"test": "jest --coverage"
32+
"test": "node --experimental-vm-modules ./node_modules/.bin/jest --coverage"
3333
},
3434
"license": "Apache-2.0",
3535
"devDependencies": {

src/logger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export const LogId = {
1717
telemetryEmitFailure: mongoLogId(1_002_002),
1818
telemetryEmitStart: mongoLogId(1_002_003),
1919
telemetryEmitSuccess: mongoLogId(1_002_004),
20-
telemetryDeviceIdFailure: mongoLogId(1_002_005),
21-
telemetryDeviceIdTimeout: mongoLogId(1_002_006),
20+
telemetryMetadataError: mongoLogId(1_002_005),
21+
telemetryDeviceIdFailure: mongoLogId(1_002_006),
22+
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
2223

2324
toolExecute: mongoLogId(1_003_001),
2425
toolExecuteFailure: mongoLogId(1_003_002),

src/telemetry/telemetry.ts

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Session } from "../session.js";
22
import { BaseEvent, CommonProperties } from "./types.js";
3-
import { config } from "../config.js";
3+
import { UserConfig } from "../config.js";
44
import logger, { LogId } from "../logger.js";
55
import { ApiClient } from "../common/atlas/apiClient.js";
66
import { MACHINE_METADATA } from "./constants.js";
@@ -23,23 +23,25 @@ export class Telemetry {
2323

2424
private constructor(
2525
private readonly session: Session,
26+
private readonly userConfig: UserConfig,
2627
private readonly commonProperties: CommonProperties,
2728
private readonly eventCache: EventCache
2829
) {}
2930

3031
static create(
3132
session: Session,
33+
userConfig: UserConfig,
3234
commonProperties: CommonProperties = { ...MACHINE_METADATA },
3335
eventCache: EventCache = EventCache.getInstance()
3436
): Telemetry {
35-
const instance = new Telemetry(session, commonProperties, eventCache);
37+
const instance = new Telemetry(session, userConfig, commonProperties, eventCache);
3638

3739
void instance.start();
3840
return instance;
3941
}
4042

4143
private async start(): Promise<void> {
42-
if (!Telemetry.isTelemetryEnabled()) {
44+
if (!this.isTelemetryEnabled()) {
4345
return;
4446
}
4547
this.deviceIdPromise = DeferredPromise.fromPromise(this.getDeviceId(), {
@@ -86,38 +88,14 @@ export class Telemetry {
8688
}
8789
}
8890

89-
/**
90-
* Checks if telemetry is currently enabled
91-
* This is a method rather than a constant to capture runtime config changes
92-
*
93-
* Follows the Console Do Not Track standard (https://consoledonottrack.com/)
94-
* by respecting the DO_NOT_TRACK environment variable
95-
*/
96-
private static isTelemetryEnabled(): boolean {
97-
// Check if telemetry is explicitly disabled in config
98-
if (config.telemetry === "disabled") {
99-
return false;
100-
}
101-
102-
const doNotTrack = process.env.DO_NOT_TRACK;
103-
if (doNotTrack) {
104-
const value = doNotTrack.toLowerCase();
105-
// Telemetry should be disabled if DO_NOT_TRACK is "1", "true", or "yes"
106-
if (value === "1" || value === "true" || value === "yes") {
107-
return false;
108-
}
109-
}
110-
111-
return true;
112-
}
113-
11491
/**
11592
* Emits events through the telemetry pipeline
11693
* @param events - The events to emit
11794
*/
11895
public async emitEvents(events: BaseEvent[]): Promise<void> {
11996
try {
120-
if (!Telemetry.isTelemetryEnabled()) {
97+
if (!this.isTelemetryEnabled()) {
98+
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
12199
return;
122100
}
123101

@@ -139,10 +117,27 @@ export class Telemetry {
139117
mcp_client_name: this.session.agentRunner?.name,
140118
session_id: this.session.sessionId,
141119
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
142-
config_connection_string: config.connectionString ? "true" : "false",
120+
config_connection_string: this.userConfig.connectionString ? "true" : "false",
143121
};
144122
}
145123

124+
/**
125+
* Checks if telemetry is currently enabled
126+
* This is a method rather than a constant to capture runtime config changes
127+
*
128+
* Follows the Console Do Not Track standard (https://consoledonottrack.com/)
129+
* by respecting the DO_NOT_TRACK environment variable
130+
*/
131+
public isTelemetryEnabled(): boolean {
132+
// Check if telemetry is explicitly disabled in config
133+
if (this.userConfig.telemetry === "disabled") {
134+
return false;
135+
}
136+
137+
const doNotTrack = "DO_NOT_TRACK" in process.env;
138+
return !doNotTrack;
139+
}
140+
146141
/**
147142
* Attempts to emit events through authenticated and unauthenticated clients
148143
* Falls back to caching if both attempts fail
@@ -165,7 +160,11 @@ export class Telemetry {
165160
const result = await this.sendEvents(this.session.apiClient, allEvents);
166161
if (result.success) {
167162
this.eventCache.clearEvents();
168-
logger.debug(LogId.telemetryEmitSuccess, "telemetry", `Sent ${allEvents.length} events successfully`);
163+
logger.debug(
164+
LogId.telemetryEmitSuccess,
165+
"telemetry",
166+
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
167+
);
169168
return;
170169
}
171170

src/tools/atlas/atlasTool.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { ToolBase, ToolCategory } from "../tool.js";
1+
import { ToolBase, ToolCategory, TelemetryToolMetadata } from "../tool.js";
2+
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import logger, { LogId } from "../../logger.js";
4+
import { z } from "zod";
25

36
export abstract class AtlasToolBase extends ToolBase {
47
protected category: ToolCategory = "atlas";
@@ -9,4 +12,46 @@ export abstract class AtlasToolBase extends ToolBase {
912
}
1013
return super.verifyAllowed();
1114
}
15+
16+
/**
17+
*
18+
* Resolves the tool metadata from the arguments passed to the tool
19+
*
20+
* @param args - The arguments passed to the tool
21+
* @returns The tool metadata
22+
*/
23+
protected resolveTelemetryMetadata(
24+
...args: Parameters<ToolCallback<typeof this.argsShape>>
25+
): TelemetryToolMetadata {
26+
const toolMetadata: TelemetryToolMetadata = {};
27+
if (!args.length) {
28+
return toolMetadata;
29+
}
30+
31+
// Create a typed parser for the exact shape we expect
32+
const argsShape = z.object(this.argsShape);
33+
const parsedResult = argsShape.safeParse(args[0]);
34+
35+
if (!parsedResult.success) {
36+
logger.debug(
37+
LogId.telemetryMetadataError,
38+
"tool",
39+
`Error parsing tool arguments: ${parsedResult.error.message}`
40+
);
41+
return toolMetadata;
42+
}
43+
44+
const data = parsedResult.data;
45+
46+
// Extract projectId using type guard
47+
if ("projectId" in data && typeof data.projectId === "string" && data.projectId.trim() !== "") {
48+
toolMetadata.projectId = data.projectId;
49+
}
50+
51+
// Extract orgId using type guard
52+
if ("orgId" in data && typeof data.orgId === "string" && data.orgId.trim() !== "") {
53+
toolMetadata.orgId = data.orgId;
54+
}
55+
return toolMetadata;
56+
}
1257
}

src/tools/mongodb/mongodbTool.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import { ToolArgs, ToolBase, ToolCategory } from "../tool.js";
2+
import { ToolArgs, ToolBase, ToolCategory, TelemetryToolMetadata } from "../tool.js";
33
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55
import { ErrorCodes, MongoDBError } from "../../errors.js";
@@ -73,4 +73,18 @@ export abstract class MongoDBToolBase extends ToolBase {
7373
protected connectToMongoDB(connectionString: string): Promise<void> {
7474
return this.session.connectToMongoDB(connectionString, this.config.connectOptions);
7575
}
76+
77+
protected resolveTelemetryMetadata(
78+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
79+
args: ToolArgs<typeof this.argsShape>
80+
): TelemetryToolMetadata {
81+
const metadata: TelemetryToolMetadata = {};
82+
83+
// Add projectId to the metadata if running a MongoDB operation to an Atlas cluster
84+
if (this.session.connectedAtlasCluster?.projectId) {
85+
metadata.projectId = this.session.connectedAtlasCluster.projectId;
86+
}
87+
88+
return metadata;
89+
}
7690
}

src/tools/tool.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNev
1111

1212
export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
1313
export type ToolCategory = "mongodb" | "atlas";
14+
export type TelemetryToolMetadata = {
15+
projectId?: string;
16+
orgId?: string;
17+
};
1418

1519
export abstract class ToolBase {
1620
protected abstract name: string;
@@ -31,28 +35,6 @@ export abstract class ToolBase {
3135
protected readonly telemetry: Telemetry
3236
) {}
3337

34-
/**
35-
* Creates and emits a tool telemetry event
36-
* @param startTime - Start time in milliseconds
37-
* @param result - Whether the command succeeded or failed
38-
* @param error - Optional error if the command failed
39-
*/
40-
private async emitToolEvent(startTime: number, result: CallToolResult): Promise<void> {
41-
const duration = Date.now() - startTime;
42-
const event: ToolEvent = {
43-
timestamp: new Date().toISOString(),
44-
source: "mdbmcp",
45-
properties: {
46-
command: this.name,
47-
category: this.category,
48-
component: "tool",
49-
duration_ms: duration,
50-
result: result.isError ? "failure" : "success",
51-
},
52-
};
53-
await this.telemetry.emitEvents([event]);
54-
}
55-
5638
public register(server: McpServer): void {
5739
if (!this.verifyAllowed()) {
5840
return;
@@ -64,12 +46,12 @@ export abstract class ToolBase {
6446
logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
6547

6648
const result = await this.execute(...args);
67-
await this.emitToolEvent(startTime, result);
49+
await this.emitToolEvent(startTime, result, ...args).catch(() => {});
6850
return result;
6951
} catch (error: unknown) {
7052
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
7153
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
72-
await this.emitToolEvent(startTime, toolResult).catch(() => {});
54+
await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
7355
return toolResult;
7456
}
7557
};
@@ -149,4 +131,47 @@ export abstract class ToolBase {
149131
],
150132
};
151133
}
134+
135+
protected abstract resolveTelemetryMetadata(
136+
...args: Parameters<ToolCallback<typeof this.argsShape>>
137+
): TelemetryToolMetadata;
138+
139+
/**
140+
* Creates and emits a tool telemetry event
141+
* @param startTime - Start time in milliseconds
142+
* @param result - Whether the command succeeded or failed
143+
* @param args - The arguments passed to the tool
144+
*/
145+
private async emitToolEvent(
146+
startTime: number,
147+
result: CallToolResult,
148+
...args: Parameters<ToolCallback<typeof this.argsShape>>
149+
): Promise<void> {
150+
if (!this.telemetry.isTelemetryEnabled()) {
151+
return;
152+
}
153+
const duration = Date.now() - startTime;
154+
const metadata = this.resolveTelemetryMetadata(...args);
155+
const event: ToolEvent = {
156+
timestamp: new Date().toISOString(),
157+
source: "mdbmcp",
158+
properties: {
159+
command: this.name,
160+
category: this.category,
161+
component: "tool",
162+
duration_ms: duration,
163+
result: result.isError ? "failure" : "success",
164+
},
165+
};
166+
167+
if (metadata?.orgId) {
168+
event.properties.org_id = metadata.orgId;
169+
}
170+
171+
if (metadata?.projectId) {
172+
event.properties.project_id = metadata.projectId;
173+
}
174+
175+
await this.telemetry.emitEvents([event]);
176+
}
152177
}

tests/integration/helpers.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { McpError } from "@modelcontextprotocol/sdk/types.js";
66
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Session } from "../../src/session.js";
88
import { Telemetry } from "../../src/telemetry/telemetry.js";
9+
import { config } from "../../src/config.js";
910

1011
interface ParameterInfo {
1112
name: string;
@@ -20,6 +21,10 @@ export interface IntegrationTest {
2021
mcpClient: () => Client;
2122
mcpServer: () => Server;
2223
}
24+
export const defaultTestConfig: UserConfig = {
25+
...config,
26+
telemetry: "disabled",
27+
};
2328

2429
export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
2530
let mcpClient: Client | undefined;
@@ -54,27 +59,21 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
5459

5560
userConfig.telemetry = "disabled";
5661

57-
const telemetry = Telemetry.create(session);
62+
const telemetry = Telemetry.create(session, userConfig);
5863

5964
mcpServer = new Server({
6065
session,
6166
userConfig,
6267
telemetry,
6368
mcpServer: new McpServer({
6469
name: "test-server",
65-
version: "1.2.3",
70+
version: "5.2.3",
6671
}),
6772
});
6873
await mcpServer.connect(serverTransport);
6974
await mcpClient.connect(clientTransport);
7075
});
7176

72-
beforeEach(() => {
73-
if (mcpServer) {
74-
mcpServer.userConfig.telemetry = "disabled";
75-
}
76-
});
77-
7877
afterEach(async () => {
7978
if (mcpServer) {
8079
await mcpServer.session.close();

0 commit comments

Comments
 (0)