Skip to content

Commit 4591c26

Browse files
committed
feat: update connectionString appName param - [MCP-68]
1 parent d2d935e commit 4591c26

File tree

9 files changed

+238
-74
lines changed

9 files changed

+238
-74
lines changed

src/common/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,4 @@ export class Session extends EventEmitter<SessionEvents> {
147147
get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined {
148148
return this.connectionManager.currentConnectionState.connectedAtlasCluster;
149149
}
150-
}
150+
}

src/helpers/connectionOptions.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,51 @@
11
import { MongoClientOptions } from "mongodb";
22
import ConnectionString from "mongodb-connection-string-url";
3+
import { getDeviceIdForConnection } from "./deviceId.js";
34

4-
export function setAppNameParamIfMissing({
5+
export interface AppNameComponents {
6+
appName: string;
7+
deviceId?: string;
8+
clientName?: string;
9+
}
10+
11+
/**
12+
* Sets the appName parameter with the extended format: appName--deviceId--clientName
13+
* Only sets the appName if it's not already present in the connection string
14+
* @param connectionString - The connection string to modify
15+
* @param components - The components to build the appName from
16+
* @returns The modified connection string
17+
*/
18+
export async function setAppNameParamIfMissing({
519
connectionString,
6-
defaultAppName,
20+
components,
721
}: {
822
connectionString: string;
9-
defaultAppName?: string;
10-
}): string {
23+
components: AppNameComponents;
24+
}): Promise<string> {
1125
const connectionStringUrl = new ConnectionString(connectionString);
12-
1326
const searchParams = connectionStringUrl.typedSearchParams<MongoClientOptions>();
1427

15-
if (!searchParams.has("appName") && defaultAppName !== undefined) {
16-
searchParams.set("appName", defaultAppName);
28+
// Only set appName if it's not already present
29+
if (searchParams.has("appName")) {
30+
return connectionStringUrl.toString();
31+
}
32+
33+
// Get deviceId if not provided
34+
let deviceId = components.deviceId;
35+
if (!deviceId) {
36+
deviceId = await getDeviceIdForConnection();
1737
}
1838

39+
// Get clientName if not provided
40+
let clientName = components.clientName;
41+
if (!clientName) {
42+
clientName = "unknown";
43+
}
44+
45+
// Build the extended appName format: appName--deviceId--clientName
46+
const extendedAppName = `${components.appName}--${deviceId}--${clientName}`;
47+
48+
searchParams.set("appName", extendedAppName);
49+
1950
return connectionStringUrl.toString();
2051
}

src/helpers/deviceId.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getDeviceId } from "@mongodb-js/device-id";
2+
import nodeMachineId from "node-machine-id";
3+
import logger, { LogId } from "../common/logger.js";
4+
5+
export const DEVICE_ID_TIMEOUT = 3000;
6+
7+
/**
8+
* Sets the appName parameter with the extended format: appName--deviceId--clientName
9+
* Only sets the appName if it's not already present in the connection string
10+
*
11+
* @param connectionString - The connection string to modify
12+
* @param components - The components to build the appName from
13+
* @returns Promise that resolves to the modified connection string
14+
*
15+
* @example
16+
* ```typescript
17+
* const result = await setExtendedAppNameParam({
18+
* connectionString: "mongodb://localhost:27017",
19+
* components: { appName: "MyApp", clientName: "Cursor" }
20+
* });
21+
* // Result: "mongodb://localhost:27017/?appName=MyApp--deviceId--Cursor"
22+
* ```
23+
*/
24+
export async function getDeviceIdForConnection(): Promise<string> {
25+
try {
26+
const deviceId = await getDeviceId({
27+
getMachineId: () => nodeMachineId.machineId(true),
28+
onError: (reason, error) => {
29+
switch (reason) {
30+
case "resolutionError":
31+
logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", String(error));
32+
break;
33+
case "timeout":
34+
logger.debug(LogId.telemetryDeviceIdTimeout, "deviceId", "Device ID retrieval timed out");
35+
break;
36+
case "abort":
37+
// No need to log in the case of aborts
38+
break;
39+
}
40+
},
41+
abortSignal: new AbortController().signal,
42+
});
43+
return deviceId;
44+
} catch (error) {
45+
logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", `Failed to get device ID: ${String(error)}`);
46+
return "unknown";
47+
}
48+
}

src/telemetry/telemetry.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,27 @@ import { LogId } from "../common/logger.js";
55
import { ApiClient } from "../common/atlas/apiClient.js";
66
import { MACHINE_METADATA } from "./constants.js";
77
import { EventCache } from "./eventCache.js";
8-
import nodeMachineId from "node-machine-id";
9-
import { getDeviceId } from "@mongodb-js/device-id";
8+
import { getDeviceIdForConnection } from "../helpers/deviceId.js";
109
import { detectContainerEnv } from "../helpers/container.js";
1110

1211
type EventResult = {
1312
success: boolean;
1413
error?: Error;
1514
};
1615

17-
export const DEVICE_ID_TIMEOUT = 3000;
18-
1916
export class Telemetry {
2017
private isBufferingEvents: boolean = true;
2118
/** Resolves when the setup is complete or a timeout occurs */
2219
public setupPromise: Promise<[string, boolean]> | undefined;
23-
private deviceIdAbortController = new AbortController();
2420
private eventCache: EventCache;
25-
private getRawMachineId: () => Promise<string>;
2621

2722
private constructor(
2823
private readonly session: Session,
2924
private readonly userConfig: UserConfig,
3025
private readonly commonProperties: CommonProperties,
31-
{ eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
26+
{ eventCache }: { eventCache: EventCache }
3227
) {
3328
this.eventCache = eventCache;
34-
this.getRawMachineId = getRawMachineId;
3529
}
3630

3731
static create(
@@ -40,14 +34,12 @@ export class Telemetry {
4034
{
4135
commonProperties = { ...MACHINE_METADATA },
4236
eventCache = EventCache.getInstance(),
43-
getRawMachineId = (): Promise<string> => nodeMachineId.machineId(true),
4437
}: {
4538
eventCache?: EventCache;
46-
getRawMachineId?: () => Promise<string>;
4739
commonProperties?: CommonProperties;
4840
} = {}
4941
): Telemetry {
50-
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
42+
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache });
5143

5244
void instance.setup();
5345
return instance;
@@ -57,35 +49,7 @@ export class Telemetry {
5749
if (!this.isTelemetryEnabled()) {
5850
return;
5951
}
60-
this.setupPromise = Promise.all([
61-
getDeviceId({
62-
getMachineId: () => this.getRawMachineId(),
63-
onError: (reason, error) => {
64-
switch (reason) {
65-
case "resolutionError":
66-
this.session.logger.debug({
67-
id: LogId.telemetryDeviceIdFailure,
68-
context: "telemetry",
69-
message: String(error),
70-
});
71-
break;
72-
case "timeout":
73-
this.session.logger.debug({
74-
id: LogId.telemetryDeviceIdTimeout,
75-
context: "telemetry",
76-
message: "Device ID retrieval timed out",
77-
noRedaction: true,
78-
});
79-
break;
80-
case "abort":
81-
// No need to log in the case of aborts
82-
break;
83-
}
84-
},
85-
abortSignal: this.deviceIdAbortController.signal,
86-
}),
87-
detectContainerEnv(),
88-
]);
52+
this.setupPromise = Promise.all([getDeviceIdForConnection(), detectContainerEnv()]);
8953

9054
const [deviceId, containerEnv] = await this.setupPromise;
9155

@@ -96,8 +60,6 @@ export class Telemetry {
9660
}
9761

9862
public async close(): Promise<void> {
99-
this.deviceIdAbortController.abort();
100-
this.isBufferingEvents = false;
10163
await this.emitEvents(this.eventCache.getEvents());
10264
}
10365

tests/integration/telemetry.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import { createHmac } from "crypto";
21
import { Telemetry } from "../../src/telemetry/telemetry.js";
32
import { Session } from "../../src/common/session.js";
43
import { config } from "../../src/common/config.js";
5-
import nodeMachineId from "node-machine-id";
4+
import { getDeviceIdForConnection } from "../../src/helpers/deviceId.js";
65
import { describe, expect, it } from "vitest";
76
import { CompositeLogger } from "../../src/common/logger.js";
87
import { ConnectionManager } from "../../src/common/connectionManager.js";
98
import { ExportsManager } from "../../src/common/exportsManager.js";
109

1110
describe("Telemetry", () => {
12-
it("should resolve the actual machine ID", async () => {
13-
const actualId: string = await nodeMachineId.machineId(true);
14-
15-
const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex");
11+
it("should resolve the actual device ID", async () => {
12+
const actualDeviceId = await getDeviceIdForConnection();
1613

1714
const logger = new CompositeLogger();
1815
const telemetry = Telemetry.create(
@@ -30,7 +27,7 @@ describe("Telemetry", () => {
3027

3128
await telemetry.setupPromise;
3229

33-
expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId);
30+
expect(telemetry.getCommonProperties().device_id).toBe(actualDeviceId);
3431
expect(telemetry["isBufferingEvents"]).toBe(false);
3532
});
3633
});

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
} from "../../../helpers.js";
88
import { config } from "../../../../../src/common/config.js";
99
import { defaultTestConfig, setupIntegrationTest } from "../../../helpers.js";
10-
import { beforeEach, describe, expect, it } from "vitest";
10+
import { beforeEach, describe, expect, it, vi } from "vitest";
11+
12+
// Mock the deviceId utility for consistent testing
13+
vi.mock("../../../../../src/helpers/deviceId.js", () => ({
14+
getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"),
15+
}));
1116

1217
describeWithMongoDB(
1318
"SwitchConnection tool",

tests/unit/common/session.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { ConnectionManager } from "../../../src/common/connectionManager.js";
77
import { ExportsManager } from "../../../src/common/exportsManager.js";
88

99
vi.mock("@mongosh/service-provider-node-driver");
10+
vi.mock("../../../src/helpers/deviceId.js", () => ({
11+
getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"),
12+
}));
13+
1014
const MockNodeDriverServiceProvider = vi.mocked(NodeDriverServiceProvider);
1115

1216
describe("Session", () => {
@@ -59,23 +63,51 @@ describe("Session", () => {
5963
expect(connectMock).toHaveBeenCalledOnce();
6064
const connectionString = connectMock.mock.calls[0]?.[0];
6165
if (testCase.expectAppName) {
62-
expect(connectionString).toContain("appName=MongoDB+MCP+Server");
66+
// Check for the extended appName format: appName--deviceId--clientName
67+
expect(connectionString).toContain("appName=MongoDB+MCP+Server+");
68+
expect(connectionString).toContain("--test-device-id--");
6369
} else {
6470
expect(connectionString).not.toContain("appName=MongoDB+MCP+Server");
6571
}
6672
});
6773
}
6874

75+
<<<<<<< HEAD
6976
it("should configure the proxy to use environment variables", async () => {
7077
await session.connectToMongoDB({ connectionString: "mongodb://localhost" });
78+
=======
79+
it("should include client name when agent runner is set", async () => {
80+
session.setAgentRunner({ name: "test-client", version: "1.0.0" });
81+
82+
await session.connectToMongoDB("mongodb://localhost:27017", config.connectOptions);
83+
>>>>>>> c5c91e9 (feat: update connectionString appName param - [MCP-68])
7184
expect(session.serviceProvider).toBeDefined();
7285

7386
const connectMock = MockNodeDriverServiceProvider.connect;
7487
expect(connectMock).toHaveBeenCalledOnce();
88+
<<<<<<< HEAD
7589

7690
const connectionConfig = connectMock.mock.calls[0]?.[1];
7791
expect(connectionConfig?.proxy).toEqual({ useEnvironmentVariableProxies: true });
7892
expect(connectionConfig?.applyProxyToOIDC).toEqual(true);
93+
=======
94+
const connectionString = connectMock.mock.calls[0]?.[0];
95+
96+
// Should include the client name in the appName
97+
expect(connectionString).toContain("--test-device-id--test-client");
98+
});
99+
100+
it("should use 'unknown' for client name when agent runner is not set", async () => {
101+
await session.connectToMongoDB("mongodb://localhost:27017", config.connectOptions);
102+
expect(session.serviceProvider).toBeDefined();
103+
104+
const connectMock = MockNodeDriverServiceProvider.connect;
105+
expect(connectMock).toHaveBeenCalledOnce();
106+
const connectionString = connectMock.mock.calls[0]?.[0];
107+
108+
// Should use 'unknown' for client name when agent runner is not set
109+
expect(connectionString).toContain("--test-device-id--unknown");
110+
>>>>>>> c5c91e9 (feat: update connectionString appName param - [MCP-68])
79111
});
80112
});
81113
});

0 commit comments

Comments
 (0)