Skip to content

Commit c3044df

Browse files
authored
chore: add connection metadata to telemetry (#716)
1 parent d462ff9 commit c3044df

File tree

10 files changed

+196
-27
lines changed

10 files changed

+196
-27
lines changed

src/common/session.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
ConnectionSettings,
1212
ConnectionStateConnected,
1313
ConnectionStateErrored,
14+
ConnectionStringAuthType,
1415
} from "./connectionManager.js";
1516
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1617
import { ErrorCodes, MongoDBError } from "./errors.js";
@@ -182,4 +183,8 @@ export class Session extends EventEmitter<SessionEvents> {
182183
get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined {
183184
return this.connectionManager.currentConnectionState.connectedAtlasCluster;
184185
}
186+
187+
get connectionStringAuthType(): ConnectionStringAuthType | undefined {
188+
return this.connectionManager.currentConnectionState.connectionStringAuthType;
189+
}
185190
}

src/telemetry/types.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,22 @@ export type CommonProperties = {
141141
* For MongoDB tools, this is typically empty, while for Atlas tools, this should include
142142
* the project and organization IDs if available.
143143
*/
144-
export type TelemetryToolMetadata = AtlasLocalToolMetadata | AtlasToolMetadata | PerfAdvisorToolMetadata;
144+
export type TelemetryToolMetadata = AtlasMetadata | PerfAdvisorToolMetadata | ConnectionMetadata;
145145

146-
export type AtlasLocalToolMetadata = {
147-
atlas_local_deployment_id?: string;
148-
};
149-
150-
export type AtlasToolMetadata = {
146+
export type AtlasMetadata = {
151147
project_id?: string;
152148
org_id?: string;
153149
};
154150

155-
export type PerfAdvisorToolMetadata = AtlasToolMetadata & {
151+
export type PerfAdvisorToolMetadata = AtlasMetadata & {
156152
operations: string[];
157153
};
154+
155+
export type ConnectionMetadata = AtlasMetadata &
156+
AtlasLocalToolMetadata & {
157+
connection_auth_type?: string;
158+
};
159+
160+
type AtlasLocalToolMetadata = {
161+
atlas_local_deployment_id?: string;
162+
};

src/tools/atlas/atlasTool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import type { AtlasToolMetadata } from "../../telemetry/types.js";
2+
import type { AtlasMetadata } from "../../telemetry/types.js";
33
import { ToolBase, type ToolArgs, type ToolCategory } from "../tool.js";
44
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55
import { LogId } from "../../common/logger.js";
@@ -85,8 +85,8 @@ For more information on Atlas API access roles, visit: https://www.mongodb.com/d
8585
protected resolveTelemetryMetadata(
8686
result: CallToolResult,
8787
...args: Parameters<ToolCallback<typeof this.argsShape>>
88-
): AtlasToolMetadata {
89-
const toolMetadata: AtlasToolMetadata = {};
88+
): AtlasMetadata {
89+
const toolMetadata: AtlasMetadata = {};
9090
if (!args.length) {
9191
return toolMetadata;
9292
}

src/tools/atlas/connect/connectCluster.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUti
88
import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js";
99
import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js";
1010
import { AtlasArgs } from "../../args.js";
11+
import type { ConnectionMetadata } from "../../../telemetry/types.js";
12+
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
1113

1214
const addedIpAccessListMessage =
1315
"Note: Your current IP address has been added to the Atlas project's IP access list to enable secure connection.";
@@ -317,4 +319,17 @@ export class ConnectClusterTool extends AtlasToolBase {
317319

318320
return { content };
319321
}
322+
323+
protected override resolveTelemetryMetadata(
324+
result: CallToolResult,
325+
...args: Parameters<ToolCallback<typeof this.argsShape>>
326+
): ConnectionMetadata {
327+
const parentMetadata = super.resolveTelemetryMetadata(result, ...args);
328+
const connectionMetadata = this.getConnectionInfoMetadata();
329+
if (connectionMetadata && connectionMetadata.project_id !== undefined) {
330+
// delete the project_id from the parent metadata to avoid duplication
331+
delete parentMetadata.project_id;
332+
}
333+
return { ...parentMetadata, ...connectionMetadata };
334+
}
320335
}

src/tools/atlasLocal/atlasLocalTool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ToolBase } from "../tool.js";
44
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
55
import type { Client } from "@mongodb-js/atlas-local";
66
import { LogId } from "../../common/logger.js";
7-
import type { AtlasLocalToolMetadata } from "../../telemetry/types.js";
7+
import type { ConnectionMetadata } from "../../telemetry/types.js";
88

99
export const AtlasLocalToolMetadataDeploymentIdKey = "deploymentId";
1010

@@ -119,8 +119,8 @@ please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issue
119119
return super.handleError(error, args);
120120
}
121121

122-
protected resolveTelemetryMetadata(result: CallToolResult): AtlasLocalToolMetadata {
123-
const toolMetadata: AtlasLocalToolMetadata = {};
122+
protected resolveTelemetryMetadata(result: CallToolResult): ConnectionMetadata {
123+
const toolMetadata: ConnectionMetadata = {};
124124

125125
// Atlas Local tools set the deployment ID in the result metadata for telemetry
126126
// If the deployment ID is set, we use it for telemetry

src/tools/atlasLocal/connect/connectDeployment.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AtlasLocalToolBase } from "../atlasLocalTool.js";
33
import type { OperationType, ToolArgs } from "../../tool.js";
44
import type { Client } from "@mongodb-js/atlas-local";
55
import { CommonArgs } from "../../args.js";
6+
import type { ConnectionMetadata } from "../../../telemetry/types.js";
67

78
export class ConnectDeploymentTool extends AtlasLocalToolBase {
89
public name = "atlas-local-connect-deployment";
@@ -34,4 +35,8 @@ export class ConnectDeploymentTool extends AtlasLocalToolBase {
3435
},
3536
};
3637
}
38+
39+
protected override resolveTelemetryMetadata(result: CallToolResult): ConnectionMetadata {
40+
return { ...super.resolveTelemetryMetadata(result), ...this.getConnectionInfoMetadata() };
41+
}
3742
}

src/tools/mongodb/mongodbTool.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
66
import { ErrorCodes, MongoDBError } from "../../common/errors.js";
77
import { LogId } from "../../common/logger.js";
88
import type { Server } from "../../server.js";
9-
import type { AtlasToolMetadata } from "../../telemetry/types.js";
9+
import type { ConnectionMetadata } from "../../telemetry/types.js";
10+
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
1011

1112
export const DbOperationArgs = {
1213
database: z.string().describe("Database name"),
@@ -111,19 +112,21 @@ export abstract class MongoDBToolBase extends ToolBase {
111112
return this.session.connectToMongoDB({ connectionString });
112113
}
113114

115+
/**
116+
* Resolves the tool metadata from the arguments passed to the mongoDB tools.
117+
*
118+
* Since MongoDB tools are executed against a MongoDB instance, the tool calls will always have the connection information.
119+
*
120+
* @param result - The result of the tool call.
121+
* @param args - The arguments passed to the tool
122+
* @returns The tool metadata
123+
*/
114124
protected resolveTelemetryMetadata(
115125
// eslint-disable-next-line @typescript-eslint/no-unused-vars
116-
result: CallToolResult,
126+
_result: CallToolResult,
117127
// eslint-disable-next-line @typescript-eslint/no-unused-vars
118-
args: ToolArgs<typeof this.argsShape>
119-
): AtlasToolMetadata {
120-
const metadata: AtlasToolMetadata = {};
121-
122-
// Add projectId to the metadata if running a MongoDB operation to an Atlas cluster
123-
if (this.session.connectedAtlasCluster?.projectId) {
124-
metadata.project_id = this.session.connectedAtlasCluster.projectId;
125-
}
126-
127-
return metadata;
128+
_args: Parameters<ToolCallback<typeof this.argsShape>>
129+
): ConnectionMetadata {
130+
return this.getConnectionInfoMetadata();
128131
}
129132
}

src/tools/tool.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/
55
import type { Session } from "../common/session.js";
66
import { LogId } from "../common/logger.js";
77
import type { Telemetry } from "../telemetry/telemetry.js";
8-
import type { TelemetryToolMetadata, ToolEvent } from "../telemetry/types.js";
8+
import type { ConnectionMetadata, TelemetryToolMetadata, ToolEvent } from "../telemetry/types.js";
99
import type { UserConfig } from "../common/config.js";
1010
import type { Server } from "../server.js";
1111
import type { Elicitation } from "../elicitation.js";
@@ -303,6 +303,20 @@ export abstract class ToolBase {
303303
protected isFeatureEnabled(feature: PreviewFeature): boolean {
304304
return this.config.previewFeatures.includes(feature);
305305
}
306+
307+
protected getConnectionInfoMetadata(): ConnectionMetadata {
308+
const metadata: ConnectionMetadata = {};
309+
if (this.session.connectedAtlasCluster?.projectId) {
310+
metadata.project_id = this.session.connectedAtlasCluster.projectId;
311+
}
312+
313+
const connectionStringAuthType = this.session.connectionStringAuthType;
314+
if (connectionStringAuthType !== undefined) {
315+
metadata.connection_auth_type = connectionStringAuthType;
316+
}
317+
318+
return metadata;
319+
}
306320
}
307321

308322
/**

tests/integration/tools/mongodb/mongodbTool.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { InMemoryTransport } from "../../inMemoryTransport.js";
1414
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
1515
import { Server } from "../../../../src/server.js";
1616
import { type ConnectionErrorHandler, connectionErrorHandler } from "../../../../src/common/connectionErrorHandler.js";
17-
import { defaultTestConfig } from "../../helpers.js";
17+
import { defaultTestConfig, expectDefined } from "../../helpers.js";
1818
import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js";
1919
import { ErrorCodes } from "../../../../src/common/errors.js";
2020
import { Keychain } from "../../../../src/common/keychain.js";
@@ -325,4 +325,42 @@ describe("MongoDBTool implementations", () => {
325325
expect(tools?.tools.find((tool) => tool.name === "UnusableVoyageTool")).toBeUndefined();
326326
});
327327
});
328+
329+
describe("resolveTelemetryMetadata", () => {
330+
it("should return empty metadata when not connected", async () => {
331+
await cleanupAndStartServer();
332+
const tool = mcpServer?.tools.find((t) => t.name === "Random");
333+
expectDefined(tool);
334+
const randomTool = tool as RandomTool;
335+
336+
const result: CallToolResult = { content: [{ type: "text", text: "test" }] };
337+
const metadata = randomTool["resolveTelemetryMetadata"](result, {} as never);
338+
339+
expect(metadata).toEqual({});
340+
expect(metadata).not.toHaveProperty("project_id");
341+
expect(metadata).not.toHaveProperty("connection_auth_type");
342+
});
343+
344+
it("should return metadata with connection_auth_type when connected via connection string", async () => {
345+
await cleanupAndStartServer({ connectionString: mdbIntegration.connectionString() });
346+
// Connect to MongoDB to set the connection state
347+
await mcpClient?.callTool({
348+
name: "Random",
349+
arguments: {},
350+
});
351+
352+
const tool = mcpServer?.tools.find((t) => t.name === "Random");
353+
expectDefined(tool);
354+
const randomTool = tool as RandomTool;
355+
356+
const result: CallToolResult = { content: [{ type: "text", text: "test" }] };
357+
const metadata = randomTool["resolveTelemetryMetadata"](result, {} as never);
358+
359+
// When connected via connection string, connection_auth_type should be set
360+
// The actual value depends on the connection string, but it should be present
361+
expect(metadata).toHaveProperty("connection_auth_type");
362+
expect(typeof metadata.connection_auth_type).toBe("string");
363+
expect(metadata.connection_auth_type).toBe("scram");
364+
});
365+
});
328366
});

tests/unit/toolBase.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,90 @@ describe("ToolBase", () => {
175175
expect(event.properties).toHaveProperty("test_param2", "three");
176176
});
177177
});
178+
179+
describe("getConnectionInfoMetadata", () => {
180+
it("should return empty metadata when neither connectedAtlasCluster nor connectionStringAuthType are set", () => {
181+
(mockSession as { connectedAtlasCluster?: unknown }).connectedAtlasCluster = undefined;
182+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = undefined;
183+
184+
const metadata = testTool["getConnectionInfoMetadata"]();
185+
186+
expect(metadata).toEqual({});
187+
expect(metadata).not.toHaveProperty("project_id");
188+
expect(metadata).not.toHaveProperty("connection_auth_type");
189+
});
190+
191+
it("should return metadata with project_id when connectedAtlasCluster.projectId is set", () => {
192+
(mockSession as { connectedAtlasCluster?: unknown }).connectedAtlasCluster = {
193+
projectId: "test-project-id",
194+
username: "test-user",
195+
clusterName: "test-cluster",
196+
expiryDate: new Date(),
197+
};
198+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = undefined;
199+
200+
const metadata = testTool["getConnectionInfoMetadata"]();
201+
202+
expect(metadata).toEqual({
203+
project_id: "test-project-id",
204+
});
205+
expect(metadata).not.toHaveProperty("connection_auth_type");
206+
});
207+
208+
it("should return empty metadata when connectedAtlasCluster exists but projectId is falsy", () => {
209+
(mockSession as { connectedAtlasCluster?: unknown }).connectedAtlasCluster = {
210+
projectId: "",
211+
username: "test-user",
212+
clusterName: "test-cluster",
213+
expiryDate: new Date(),
214+
};
215+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = undefined;
216+
217+
const metadata = testTool["getConnectionInfoMetadata"]();
218+
219+
expect(metadata).toEqual({});
220+
expect(metadata).not.toHaveProperty("project_id");
221+
});
222+
223+
it("should return metadata with connection_auth_type when connectionStringAuthType is set", () => {
224+
(mockSession as { connectedAtlasCluster?: unknown }).connectedAtlasCluster = undefined;
225+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = "scram";
226+
227+
const metadata = testTool["getConnectionInfoMetadata"]();
228+
229+
expect(metadata).toEqual({
230+
connection_auth_type: "scram",
231+
});
232+
expect(metadata).not.toHaveProperty("project_id");
233+
});
234+
235+
it("should return metadata with both project_id and connection_auth_type when both are set", () => {
236+
(mockSession as { connectedAtlasCluster?: unknown }).connectedAtlasCluster = {
237+
projectId: "test-project-id",
238+
username: "test-user",
239+
clusterName: "test-cluster",
240+
expiryDate: new Date(),
241+
};
242+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = "oidc-auth-flow";
243+
244+
const metadata = testTool["getConnectionInfoMetadata"]();
245+
246+
expect(metadata).toEqual({
247+
project_id: "test-project-id",
248+
connection_auth_type: "oidc-auth-flow",
249+
});
250+
});
251+
252+
it("should handle different connectionStringAuthType values", () => {
253+
const authTypes = ["scram", "ldap", "kerberos", "oidc-auth-flow", "oidc-device-flow", "x.509"] as const;
254+
255+
for (const authType of authTypes) {
256+
(mockSession as { connectionStringAuthType?: unknown }).connectionStringAuthType = authType;
257+
const metadata = testTool["getConnectionInfoMetadata"]();
258+
expect(metadata.connection_auth_type).toBe(authType);
259+
}
260+
});
261+
});
178262
});
179263

180264
class TestTool extends ToolBase {

0 commit comments

Comments
 (0)