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 4 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),
telmetryMetadataError: mongoLogId(1_002_005),

toolExecute: mongoLogId(1_003_001),
toolExecuteFailure: mongoLogId(1_003_002),
Expand Down
1 change: 1 addition & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class Session extends EventEmitter<{
username: string;
projectId: string;
clusterName: string;
organizationId?: string;
expiryDate: Date;
};

Expand Down
59 changes: 32 additions & 27 deletions src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ type EventResult = {
error?: Error;
};

/**
* 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
*/
export function 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;
}

export class Telemetry {
private readonly commonProperties: CommonProperties;

Expand All @@ -23,38 +48,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 (!isTelemetryEnabled()) {
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
return;
}

Expand Down Expand Up @@ -96,7 +97,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, ToolMetadata } 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 resolveToolMetadata(
args: ToolArgs<typeof this.argsShape>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
): ToolMetadata {
const metadata = super.resolveToolMetadata(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;
}
}
117 changes: 92 additions & 25 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import type { McpServer, RegisteredTool, ToolCallback } from "@modelcontextproto
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { Session } from "../session.js";
import logger, { LogId } from "../logger.js";
import { Telemetry } from "../telemetry/telemetry.js";
import { Telemetry, isTelemetryEnabled } from "../telemetry/telemetry.js";
import { type ToolEvent } from "../telemetry/types.js";
import { UserConfig } from "../config.js";

export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;

export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
export type ToolCategory = "mongodb" | "atlas";
export type ToolMetadata = {
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);
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,89 @@ 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 resolveToolMetadata(...args: Parameters<ToolCallback<typeof this.argsShape>>): ToolMetadata {
const toolMetadata: ToolMetadata = {};
try {
if (!args[0] || typeof args[0] !== "object") {
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.telmetryMetadataError,
"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;
}
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[q] when might catch happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hopefully never I just don't want telemetry to cause any issues but can remove

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see no method that might have side effects, which line might throw? perhaps argsShape.safeParse?

perhaps the try catch is better suited for emitToolEvent, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

can be! moving

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's just that we could still emit if this goes wrong, but no strong opinions

const errorMessage = error instanceof Error ? error.message : String(error);
logger.debug(LogId.telmetryMetadataError, "tool", `Error resolving tool metadata: ${errorMessage}`);
}
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 (!isTelemetryEnabled()) {
return;
}
const duration = Date.now() - startTime;
const metadata = this.resolveToolMetadata(...args);
const event: ToolEvent = {
timestamp: new Date().toISOString(),
source: "mdbmcp",
properties: {
...this.telemetry.getCommonProperties(),
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]);
}
}
Loading