Skip to content

Commit aaf96d1

Browse files
authored
chore: add orgId and projectid (#159)
1 parent 7eee5df commit aaf96d1

File tree

11 files changed

+155
-77
lines changed

11 files changed

+155
-77
lines changed

src/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const LogId = {
1717
telemetryEmitFailure: mongoLogId(1_002_002),
1818
telemetryEmitStart: mongoLogId(1_002_003),
1919
telemetryEmitSuccess: mongoLogId(1_002_004),
20+
telemetryMetadataError: mongoLogId(1_002_005),
2021

2122
toolExecute: mongoLogId(1_003_001),
2223
toolExecuteFailure: mongoLogId(1_003_002),

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class Server {
2828
constructor({ session, mcpServer, userConfig }: ServerOptions) {
2929
this.startTime = Date.now();
3030
this.session = session;
31-
this.telemetry = new Telemetry(session);
31+
this.telemetry = new Telemetry(session, userConfig);
3232
this.mcpServer = mcpServer;
3333
this.userConfig = userConfig;
3434
}

src/telemetry/telemetry.ts

Lines changed: 27 additions & 29 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";
@@ -16,45 +16,22 @@ export class Telemetry {
1616

1717
constructor(
1818
private readonly session: Session,
19+
private readonly userConfig: UserConfig,
1920
private readonly eventCache: EventCache = EventCache.getInstance()
2021
) {
2122
this.commonProperties = {
2223
...MACHINE_METADATA,
2324
};
2425
}
2526

26-
/**
27-
* Checks if telemetry is currently enabled
28-
* This is a method rather than a constant to capture runtime config changes
29-
*
30-
* Follows the Console Do Not Track standard (https://consoledonottrack.com/)
31-
* by respecting the DO_NOT_TRACK environment variable
32-
*/
33-
private static isTelemetryEnabled(): boolean {
34-
// Check if telemetry is explicitly disabled in config
35-
if (config.telemetry === "disabled") {
36-
return false;
37-
}
38-
39-
const doNotTrack = process.env.DO_NOT_TRACK;
40-
if (doNotTrack) {
41-
const value = doNotTrack.toLowerCase();
42-
// Telemetry should be disabled if DO_NOT_TRACK is "1", "true", or "yes"
43-
if (value === "1" || value === "true" || value === "yes") {
44-
return false;
45-
}
46-
}
47-
48-
return true;
49-
}
50-
5127
/**
5228
* Emits events through the telemetry pipeline
5329
* @param events - The events to emit
5430
*/
5531
public async emitEvents(events: BaseEvent[]): Promise<void> {
5632
try {
57-
if (!Telemetry.isTelemetryEnabled()) {
33+
if (!this.isTelemetryEnabled()) {
34+
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
5835
return;
5936
}
6037

@@ -75,10 +52,27 @@ export class Telemetry {
7552
mcp_client_name: this.session.agentRunner?.name,
7653
session_id: this.session.sessionId,
7754
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
78-
config_connection_string: config.connectionString ? "true" : "false",
55+
config_connection_string: this.userConfig.connectionString ? "true" : "false",
7956
};
8057
}
8158

59+
/**
60+
* Checks if telemetry is currently enabled
61+
* This is a method rather than a constant to capture runtime config changes
62+
*
63+
* Follows the Console Do Not Track standard (https://consoledonottrack.com/)
64+
* by respecting the DO_NOT_TRACK environment variable
65+
*/
66+
public isTelemetryEnabled(): boolean {
67+
// Check if telemetry is explicitly disabled in config
68+
if (this.userConfig.telemetry === "disabled") {
69+
return false;
70+
}
71+
72+
const doNotTrack = "DO_NOT_TRACK" in process.env;
73+
return !doNotTrack;
74+
}
75+
8276
/**
8377
* Attempts to emit events through authenticated and unauthenticated clients
8478
* Falls back to caching if both attempts fail
@@ -96,7 +90,11 @@ export class Telemetry {
9690
const result = await this.sendEvents(this.session.apiClient, allEvents);
9791
if (result.success) {
9892
this.eventCache.clearEvents();
99-
logger.debug(LogId.telemetryEmitSuccess, "telemetry", `Sent ${allEvents.length} events successfully`);
93+
logger.debug(
94+
LogId.telemetryEmitSuccess,
95+
"telemetry",
96+
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
97+
);
10098
return;
10199
}
102100

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: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { UserConfig } from "../../src/config.js";
55
import { McpError } from "@modelcontextprotocol/sdk/types.js";
66
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Session } from "../../src/session.js";
8+
import { config } from "../../src/config.js";
89

910
interface ParameterInfo {
1011
name: string;
@@ -19,6 +20,10 @@ export interface IntegrationTest {
1920
mcpClient: () => Client;
2021
mcpServer: () => Server;
2122
}
23+
export const defaultTestConfig: UserConfig = {
24+
...config,
25+
telemetry: "disabled",
26+
};
2227

2328
export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
2429
let mcpClient: Client | undefined;
@@ -51,25 +56,18 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
5156
apiClientSecret: userConfig.apiClientSecret,
5257
});
5358

54-
userConfig.telemetry = "disabled";
5559
mcpServer = new Server({
5660
session,
5761
userConfig,
5862
mcpServer: new McpServer({
5963
name: "test-server",
60-
version: "1.2.3",
64+
version: "5.2.3",
6165
}),
6266
});
6367
await mcpServer.connect(serverTransport);
6468
await mcpClient.connect(clientTransport);
6569
});
6670

67-
beforeEach(() => {
68-
if (mcpServer) {
69-
mcpServer.userConfig.telemetry = "disabled";
70-
}
71-
});
72-
7371
afterEach(async () => {
7472
if (mcpServer) {
7573
await mcpServer.session.close();

tests/integration/server.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { expectDefined, setupIntegrationTest } from "./helpers.js";
2-
import { config } from "../../src/config.js";
1+
import { defaultTestConfig, expectDefined, setupIntegrationTest } from "./helpers.js";
32
import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js";
43

54
describe("Server integration test", () => {
@@ -16,15 +15,15 @@ describe("Server integration test", () => {
1615
});
1716
},
1817
() => ({
19-
...config,
18+
...defaultTestConfig,
2019
apiClientId: undefined,
2120
apiClientSecret: undefined,
2221
})
2322
);
2423

2524
describe("with atlas", () => {
2625
const integration = setupIntegrationTest(() => ({
27-
...config,
26+
...defaultTestConfig,
2827
apiClientId: "test",
2928
apiClientSecret: "test",
3029
}));
@@ -59,7 +58,7 @@ describe("Server integration test", () => {
5958

6059
describe("with read-only mode", () => {
6160
const integration = setupIntegrationTest(() => ({
62-
...config,
61+
...defaultTestConfig,
6362
readOnly: true,
6463
apiClientId: "test",
6564
apiClientSecret: "test",

tests/integration/tools/atlas/atlasHelpers.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { ObjectId } from "mongodb";
22
import { Group } from "../../../../src/common/atlas/openapi.js";
33
import { ApiClient } from "../../../../src/common/atlas/apiClient.js";
4-
import { setupIntegrationTest, IntegrationTest } from "../../helpers.js";
5-
import { config } from "../../../../src/config.js";
4+
import { setupIntegrationTest, IntegrationTest, defaultTestConfig } from "../../helpers.js";
65

76
export type IntegrationTestFunction = (integration: IntegrationTest) => void;
87

98
export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
109
const testDefinition = () => {
1110
const integration = setupIntegrationTest(() => ({
12-
...config,
11+
...defaultTestConfig,
1312
apiClientId: process.env.MDB_MCP_API_CLIENT_ID,
1413
apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET,
1514
}));

0 commit comments

Comments
 (0)