Skip to content

feat: update connectionString appName param - [MCP-68] #406

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 40 commits into from
Aug 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c5c91e9
feat: update connectionString appName param - [MCP-68]
blva Jul 28, 2025
4cf78c2
Merge remote-tracking branch 'origin/main' into MCP-68
blva Jul 28, 2025
026b91a
add removed test
blva Jul 28, 2025
680e1e1
update tests
blva Jul 28, 2025
92aab61
Merge remote-tracking branch 'origin/main' into MCP-68
blva Jul 31, 2025
eca40e1
add timeout test
blva Jul 31, 2025
6cf0ae6
Merge remote-tracking branch 'origin/main' into MCP-68
blva Jul 31, 2025
524d965
fix
blva Jul 31, 2025
72f1ab8
Update src/helpers/deviceId.ts
blva Jul 31, 2025
c3f0928
add buffering update back
blva Jul 31, 2025
f8de877
squashed commits
blva Jul 31, 2025
8048cf6
Merge remote-tracking branch 'origin/main' into MCP-68
blva Aug 18, 2025
0c620b2
move appName setting to connection manager
blva Aug 18, 2025
d148376
chore: rename agentRunner to mcpClient
blva Aug 18, 2025
13a0349
refactor and address some comments
blva Aug 18, 2025
bb97041
keep getMachineId under deviceId
blva Aug 18, 2025
5fb0284
chore: lint and test fix
blva Aug 18, 2025
a690850
fix typo
blva Aug 18, 2025
a2e1091
fix test
blva Aug 18, 2025
f964e12
fix: update appName and set it to unknown if not available
blva Aug 18, 2025
6e922e6
fix: fix test
blva Aug 18, 2025
4ba3a16
decouple error handling into it's own method
blva Aug 18, 2025
78d430a
more linting
blva Aug 18, 2025
ae1d6d0
reformat
blva Aug 19, 2025
68a462e
reformat and add integration test
blva Aug 19, 2025
720be15
Revert "reformat"
blva Aug 19, 2025
565ca2b
decouple config validation from connection
blva Aug 19, 2025
69609b4
lint
blva Aug 19, 2025
bdf1272
Merge remote-tracking branch 'origin/main' into MCP-68
blva Aug 19, 2025
658cdc8
new device id
blva Aug 20, 2025
dae4f06
simplify device id
blva Aug 20, 2025
858ce1e
fix linting
blva Aug 20, 2025
0bc1a9f
update test
blva Aug 21, 2025
b0972bd
address comment: inject deviceId
blva Aug 22, 2025
e570562
chore: update transport to close deviceId last
blva Aug 22, 2025
8e62d2e
Merge branch 'main' into MCP-68
blva Aug 22, 2025
0ed8d23
chore: add deviceId close to integration
blva Aug 22, 2025
abf285a
fix check
blva Aug 22, 2025
e7727ee
fix check
blva Aug 22, 2025
f9c22d2
Merge branch 'main' into MCP-68
blva Aug 22, 2025
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
22 changes: 20 additions & 2 deletions src/common/connectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { packageInfo } from "./packageInfo.js";
import ConnectionString from "mongodb-connection-string-url";
import { MongoClientOptions } from "mongodb";
import { ErrorCodes, MongoDBError } from "./errors.js";
import { DeviceId } from "../helpers/deviceId.js";
import { AppNameComponents } from "../helpers/connectionOptions.js";
import { CompositeLogger, LogId } from "./logger.js";
import { ConnectionInfo, generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";

Expand Down Expand Up @@ -69,12 +71,15 @@ export interface ConnectionManagerEvents {

export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
private state: AnyConnectionState;
private deviceId: DeviceId;
private clientName: string;
private bus: EventEmitter;

constructor(
private userConfig: UserConfig,
private driverOptions: DriverOptions,
private logger: CompositeLogger,
deviceId: DeviceId,
bus?: EventEmitter
) {
super();
Expand All @@ -84,6 +89,13 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {

this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));

this.deviceId = deviceId;
this.clientName = "unknown";
}

setClientName(clientName: string): void {
this.clientName = clientName;
}

async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
Expand All @@ -98,9 +110,15 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {

try {
settings = { ...settings };
settings.connectionString = setAppNameParamIfMissing({
const appNameComponents: AppNameComponents = {
appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
deviceId: this.deviceId.get(),
clientName: this.clientName,
};

settings.connectionString = await setAppNameParamIfMissing({
connectionString: settings.connectionString,
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
components: appNameComponents,
});

connectionInfo = generateConnectionInfoFromCliArgs({
Expand Down
5 changes: 3 additions & 2 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const LogId = {
serverClosed: mongoLogId(1_000_004),
serverCloseFailure: mongoLogId(1_000_005),
serverDuplicateLoggers: mongoLogId(1_000_006),
serverMcpClientSet: mongoLogId(1_000_007),

atlasCheckCredentials: mongoLogId(1_001_001),
atlasDeleteDatabaseUserFailure: mongoLogId(1_001_002),
Expand All @@ -30,8 +31,8 @@ export const LogId = {
telemetryEmitStart: mongoLogId(1_002_003),
telemetryEmitSuccess: mongoLogId(1_002_004),
telemetryMetadataError: mongoLogId(1_002_005),
telemetryDeviceIdFailure: mongoLogId(1_002_006),
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
deviceIdResolutionError: mongoLogId(1_002_006),
deviceIdTimeout: mongoLogId(1_002_007),

toolExecute: mongoLogId(1_003_001),
toolExecuteFailure: mongoLogId(1_003_002),
Expand Down
30 changes: 21 additions & 9 deletions src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export class Session extends EventEmitter<SessionEvents> {
readonly exportsManager: ExportsManager;
readonly connectionManager: ConnectionManager;
readonly apiClient: ApiClient;
agentRunner?: {
name: string;
version: string;
mcpClient?: {
name?: string;
version?: string;
title?: string;
};

public logger: CompositeLogger;
Expand Down Expand Up @@ -69,13 +70,24 @@ export class Session extends EventEmitter<SessionEvents> {
this.connectionManager.on("connection-errored", (error) => this.emit("connection-error", error.errorReason));
}

setAgentRunner(agentRunner: Implementation | undefined): void {
if (agentRunner?.name && agentRunner?.version) {
this.agentRunner = {
name: agentRunner.name,
version: agentRunner.version,
};
setMcpClient(mcpClient: Implementation | undefined): void {
if (!mcpClient) {
this.connectionManager.setClientName("unknown");
this.logger.debug({
id: LogId.serverMcpClientSet,
context: "session",
message: "MCP client info not found",
});
}

this.mcpClient = {
name: mcpClient?.name || "unknown",
version: mcpClient?.version || "unknown",
title: mcpClient?.title || "unknown",
};

// Set the client name on the connection manager for appName generation
this.connectionManager.setClientName(this.mcpClient.name || "unknown");
}

async disconnect(): Promise<void> {
Expand Down
53 changes: 46 additions & 7 deletions src/helpers/connectionOptions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
import { MongoClientOptions } from "mongodb";
import ConnectionString from "mongodb-connection-string-url";

export function setAppNameParamIfMissing({
export interface AppNameComponents {
appName: string;
deviceId?: Promise<string>;
clientName?: string;
}

/**
* Sets the appName parameter with the extended format: appName--deviceId--clientName
* Only sets the appName if it's not already present in the connection string
* @param connectionString - The connection string to modify
* @param components - The components to build the appName from
* @returns The modified connection string
*/
export async function setAppNameParamIfMissing({
connectionString,
defaultAppName,
components,
}: {
connectionString: string;
defaultAppName?: string;
}): string {
components: AppNameComponents;
}): Promise<string> {
const connectionStringUrl = new ConnectionString(connectionString);

const searchParams = connectionStringUrl.typedSearchParams<MongoClientOptions>();

if (!searchParams.has("appName") && defaultAppName !== undefined) {
searchParams.set("appName", defaultAppName);
// Only set appName if it's not already present
if (searchParams.has("appName")) {
return connectionStringUrl.toString();
}

const appName = components.appName || "unknown";
const deviceId = components.deviceId ? await components.deviceId : "unknown";
const clientName = components.clientName || "unknown";

// Build the extended appName format: appName--deviceId--clientName
const extendedAppName = `${appName}--${deviceId}--${clientName}`;

searchParams.set("appName", extendedAppName);

return connectionStringUrl.toString();
}

/**
* Validates the connection string
* @param connectionString - The connection string to validate
* @param looseValidation - Whether to allow loose validation
* @returns void
* @throws Error if the connection string is invalid
*/
export function validateConnectionString(connectionString: string, looseValidation: boolean): void {
try {
new ConnectionString(connectionString, { looseValidation });
} catch (error) {
throw new Error(
`Invalid connection string with error: ${error instanceof Error ? error.message : String(error)}`
);
}
}
113 changes: 113 additions & 0 deletions src/helpers/deviceId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { getDeviceId } from "@mongodb-js/device-id";
import nodeMachineId from "node-machine-id";
import { LogId, LoggerBase } from "../common/logger.js";

export const DEVICE_ID_TIMEOUT = 3000;

export class DeviceId {
private deviceId: string | undefined = undefined;
private deviceIdPromise: Promise<string> | undefined = undefined;
private abortController: AbortController | undefined = undefined;
private logger: LoggerBase;
private readonly getMachineId: () => Promise<string>;
private timeout: number;
private static instance: DeviceId | undefined = undefined;

private constructor(logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT) {
this.logger = logger;
this.timeout = timeout;
this.getMachineId = (): Promise<string> => nodeMachineId.machineId(true);
}

public static create(logger: LoggerBase, timeout?: number): DeviceId {
if (this.instance) {
throw new Error("DeviceId instance already exists, use get() to retrieve the device ID");
}

const instance = new DeviceId(logger, timeout ?? DEVICE_ID_TIMEOUT);
instance.setup();

this.instance = instance;

return instance;
}

private setup(): void {
this.deviceIdPromise = this.calculateDeviceId();
}

/**
* Closes the device ID calculation promise and abort controller.
*/
public close(): void {
if (this.abortController) {
this.abortController.abort();
this.abortController = undefined;
}

this.deviceId = undefined;
this.deviceIdPromise = undefined;
DeviceId.instance = undefined;
}

/**
* Gets the device ID, waiting for the calculation to complete if necessary.
* @returns Promise that resolves to the device ID string
*/
public get(): Promise<string> {
if (this.deviceId) {
return Promise.resolve(this.deviceId);
}

if (this.deviceIdPromise) {
return this.deviceIdPromise;
}

return this.calculateDeviceId();
}

/**
* Internal method that performs the actual device ID calculation.
*/
private async calculateDeviceId(): Promise<string> {
if (!this.abortController) {
this.abortController = new AbortController();
}

this.deviceIdPromise = getDeviceId({
getMachineId: this.getMachineId,
onError: (reason, error) => {
this.handleDeviceIdError(reason, String(error));
},
timeout: this.timeout,
abortSignal: this.abortController.signal,
});

return this.deviceIdPromise;
}

private handleDeviceIdError(reason: string, error: string): void {
this.deviceIdPromise = Promise.resolve("unknown");

switch (reason) {
case "resolutionError":
this.logger.debug({
id: LogId.deviceIdResolutionError,
context: "deviceId",
message: `Resolution error: ${String(error)}`,
});
break;
case "timeout":
this.logger.debug({
id: LogId.deviceIdTimeout,
context: "deviceId",
message: "Device ID retrieval timed out",
noRedaction: true,
});
break;
case "abort":
// No need to log in the case of 'abort' errors
break;
}
}
}
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ async function main(): Promise<void> {
assertVersionMode();

const transportRunner = config.transport === "stdio" ? new StdioRunner(config) : new StreamableHttpRunner(config);

const shutdown = (): void => {
transportRunner.logger.info({
id: LogId.serverCloseRequested,
Expand Down
37 changes: 28 additions & 9 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import assert from "assert";
import { ToolBase } from "./tools/tool.js";
import { validateConnectionString } from "./helpers/connectionOptions.js";

export interface ServerOptions {
session: Session;
Expand Down Expand Up @@ -97,12 +98,14 @@ export class Server {
});

this.mcpServer.server.oninitialized = (): void => {
this.session.setAgentRunner(this.mcpServer.server.getClientVersion());
this.session.setMcpClient(this.mcpServer.server.getClientVersion());
// Placed here to start the connection to the config connection string as soon as the server is initialized.
void this.connectToConfigConnectionString();

this.session.logger.info({
id: LogId.serverInitialized,
context: "server",
message: `Server started with transport ${transport.constructor.name} and agent runner ${this.session.agentRunner?.name}`,
message: `Server started with transport ${transport.constructor.name} and agent runner ${this.session.mcpClient?.name}`,
});

this.emitServerEvent("start", Date.now() - this.startTime);
Expand Down Expand Up @@ -188,20 +191,20 @@ export class Server {
}

private async validateConfig(): Promise<void> {
// Validate connection string
if (this.userConfig.connectionString) {
try {
await this.session.connectToMongoDB({
connectionString: this.userConfig.connectionString,
});
validateConnectionString(this.userConfig.connectionString, false);
} catch (error) {
console.error(
"Failed to connect to MongoDB instance using the connection string from the config: ",
error
console.error("Connection string validation failed with error: ", error);
throw new Error(
"Connection string validation failed with error: " +
(error instanceof Error ? error.message : String(error))
);
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
}
}

// Validate API client credentials
if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
try {
await this.session.apiClient.validateAccessToken();
Expand All @@ -219,4 +222,20 @@ export class Server {
}
}
}

private async connectToConfigConnectionString(): Promise<void> {
if (this.userConfig.connectionString) {
try {
await this.session.connectToMongoDB({
connectionString: this.userConfig.connectionString,
});
} catch (error) {
console.error(
"Failed to connect to MongoDB instance using the connection string from the config: ",
error
);
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
}
}
}
}
Loading
Loading