Skip to content

chore: add orgId and projectid #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const LogId = {
telemetryEmitFailure: mongoLogId(1_002_002),
telemetryEmitStart: mongoLogId(1_002_003),
telemetryEmitSuccess: mongoLogId(1_002_004),
telemetryMetadataError: mongoLogId(1_002_005),

toolExecute: mongoLogId(1_003_001),
toolExecuteFailure: mongoLogId(1_003_002),
Expand Down
51 changes: 24 additions & 27 deletions src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,14 @@ 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 {
// Check if telemetry is explicitly disabled in config
if (config.telemetry === "disabled") {
return false;
}

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;
}

/**
* Emits events through the telemetry pipeline
* @param events - The events to emit
*/
public async emitEvents(events: BaseEvent[]): Promise<void> {
try {
if (!Telemetry.isTelemetryEnabled()) {
if (!this.isTelemetryEnabled()) {
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
return;
}

Expand All @@ -79,6 +55,23 @@ 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
*/
public isTelemetryEnabled(): boolean {
// Check if telemetry is explicitly disabled in config
if (config.telemetry === "disabled") {
return false;
}

const doNotTrack = "DO_NOT_TRACK" in process.env;
return !doNotTrack;
}

/**
* Attempts to emit events through authenticated and unauthenticated clients
* Falls back to caching if both attempts fail
Expand All @@ -96,7 +89,11 @@ export class Telemetry {
const result = await this.sendEvents(this.session.apiClient, allEvents);
if (result.success) {
this.eventCache.clearEvents();
logger.debug(LogId.telemetryEmitSuccess, "telemetry", `Sent ${allEvents.length} events successfully`);
logger.debug(
LogId.telemetryEmitSuccess,
"telemetry",
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
);
return;
}

Expand Down
19 changes: 17 additions & 2 deletions src/tools/mongodb/mongodbTool.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from "zod";
import { ToolArgs, ToolBase, ToolCategory } from "../tool.js";
import { ToolArgs, ToolBase, ToolCategory, TelemetryToolMetadata } from "../tool.js";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { CallToolResult, ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
import { ErrorCodes, MongoDBError } from "../../errors.js";
import logger, { LogId } from "../../logger.js";
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";

export const DbOperationArgs = {
database: z.string().describe("Database name"),
Expand Down Expand Up @@ -73,4 +74,18 @@ export abstract class MongoDBToolBase extends ToolBase {
protected connectToMongoDB(connectionString: string): Promise<void> {
return this.session.connectToMongoDB(connectionString, this.config.connectOptions);
}

protected resolveTelemetryMetadata(
args: ToolArgs<typeof this.argsShape>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
): TelemetryToolMetadata {
const metadata = super.resolveTelemetryMetadata(args, extra);

// Add projectId to the metadata if running a MongoDB operation to an Atlas cluster
if (this.session.connectedAtlasCluster?.projectId) {
metadata.projectId = this.session.connectedAtlasCluster.projectId;
}

return metadata;
}
}
111 changes: 87 additions & 24 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNev

export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
export type ToolCategory = "mongodb" | "atlas";
export type TelemetryToolMetadata = {
projectId?: string;
orgId?: string;
};

export abstract class ToolBase {
protected abstract name: string;
Expand All @@ -31,28 +35,6 @@ export abstract class ToolBase {
protected readonly telemetry: Telemetry
) {}

/**
* 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: CallToolResult): Promise<void> {
const duration = Date.now() - startTime;
const event: ToolEvent = {
timestamp: new Date().toISOString(),
source: "mdbmcp",
properties: {
command: this.name,
category: this.category,
component: "tool",
duration_ms: duration,
result: result.isError ? "failure" : "success",
},
};
await this.telemetry.emitEvents([event]);
}

public register(server: McpServer): void {
if (!this.verifyAllowed()) {
return;
Expand All @@ -64,12 +46,12 @@ export abstract class ToolBase {
logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);

const result = await this.execute(...args);
await this.emitToolEvent(startTime, result);
await this.emitToolEvent(startTime, result, ...args).catch(() => {});
return result;
} catch (error: unknown) {
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
await this.emitToolEvent(startTime, toolResult).catch(() => {});
await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
return toolResult;
}
};
Expand Down Expand Up @@ -149,4 +131,85 @@ export abstract class ToolBase {
],
};
}

/**
*
* Resolves the tool metadata from the arguments passed to the tool
*
* @param args - The arguments passed to the tool
* @returns The tool metadata
*/
protected resolveTelemetryMetadata(
...args: Parameters<ToolCallback<typeof this.argsShape>>
): TelemetryToolMetadata {
const toolMetadata: TelemetryToolMetadata = {};
if (!args.length) {
return toolMetadata;
}

// Create a typed parser for the exact shape we expect
const argsShape = z.object(this.argsShape);
const parsedResult = argsShape.safeParse(args[0]);

if (!parsedResult.success) {
logger.debug(
LogId.telemetryMetadataError,
"tool",
`Error parsing tool arguments: ${parsedResult.error.message}`
);
return toolMetadata;
}

const data = parsedResult.data;

// Extract projectId using type guard
if ("projectId" in data && typeof data.projectId === "string" && data.projectId.trim() !== "") {
toolMetadata.projectId = data.projectId;
}

// Extract orgId using type guard
if ("orgId" in data && typeof data.orgId === "string" && data.orgId.trim() !== "") {
toolMetadata.orgId = data.orgId;
}
return toolMetadata;
}

/**
* Creates and emits a tool telemetry event
* @param startTime - Start time in milliseconds
* @param result - Whether the command succeeded or failed
* @param args - The arguments passed to the tool
*/
private async emitToolEvent(
startTime: number,
result: CallToolResult,
...args: Parameters<ToolCallback<typeof this.argsShape>>
): Promise<void> {
if (!this.telemetry.isTelemetryEnabled()) {
return;
}
const duration = Date.now() - startTime;
const metadata = this.resolveTelemetryMetadata(...args);
const event: ToolEvent = {
timestamp: new Date().toISOString(),
source: "mdbmcp",
properties: {
command: this.name,
category: this.category,
component: "tool",
duration_ms: duration,
result: result.isError ? "failure" : "success",
},
};

if (metadata?.orgId) {
event.properties.org_id = metadata.orgId;
}

if (metadata?.projectId) {
event.properties.project_id = metadata.projectId;
}

await this.telemetry.emitEvents([event]);
}
}
2 changes: 2 additions & 0 deletions tests/integration/tools/mongodb/mongodbHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export function describeWithMongoDB(
const integration = setupIntegrationTest(() => ({
...getUserConfig(mdbIntegration),
connectionString: mdbIntegration.connectionString(),
telemetry: "disabled", // Explicitly disable telemetry
}));

beforeEach(() => {
integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
integration.mcpServer().userConfig.telemetry = "disabled"; // Ensure telemetry stays disabled
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] do we need to do this for atlas as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are changing this value on the fly, it would be better for the test that changes it to change it back.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been testing this, need to change it slightly, will udpate

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, and atlas is not emitting, only mdbtools were missing

});

fn({
Expand Down
Loading