Skip to content
2 changes: 2 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface UserConfig extends CliOptions {
transport: "stdio" | "http";
httpPort: number;
httpHost: string;
httpHeaders: Record<string, string>;
loggers: Array<"stderr" | "disk" | "mcp">;
idleTimeoutMs: number;
notificationTimeoutMs: number;
Expand All @@ -137,6 +138,7 @@ export const defaultUserConfig: UserConfig = {
loggers: ["disk", "mcp"],
idleTimeoutMs: 600000, // 10 minutes
notificationTimeoutMs: 540000, // 9 minutes
httpHeaders: {},
};

export const config = setupUserConfig({
Expand Down
48 changes: 26 additions & 22 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const LogId = {
oidcFlow: mongoLogId(1_008_001),
} as const;

interface LogPayload {
export interface LogPayload {
id: MongoLogId;
context: string;
message: string;
Expand Down Expand Up @@ -152,6 +152,26 @@ export abstract class LoggerBase<T extends EventMap<T> = DefaultEventMap> extend
public emergency(payload: LogPayload): void {
this.log("emergency", payload);
}

protected mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
switch (level) {
case "info":
return "info";
case "warning":
return "warn";
case "error":
return "error";
case "notice":
case "debug":
return "debug";
case "critical":
case "alert":
case "emergency":
return "fatal";
default:
return "info";
}
}
}

export class ConsoleLogger extends LoggerBase {
Expand Down Expand Up @@ -225,26 +245,6 @@ export class DiskLogger extends LoggerBase<{ initialized: [] }> {

this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message, payload.attributes);
}

private mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
switch (level) {
case "info":
return "info";
case "warning":
return "warn";
case "error":
return "error";
case "notice":
case "debug":
return "debug";
case "critical":
case "alert":
case "emergency":
return "fatal";
default:
return "info";
}
}
}

export class McpLogger extends LoggerBase {
Expand Down Expand Up @@ -286,7 +286,11 @@ export class CompositeLogger extends LoggerBase {
public log(level: LogLevel, payload: LogPayload): void {
// Override the public method to avoid the base logger redacting the message payload
for (const logger of this.loggers) {
logger.log(level, { ...payload, attributes: { ...this.attributes, ...payload.attributes } });
const attributes =
Object.keys(this.attributes).length > 0 || payload.attributes
? { ...this.attributes, ...payload.attributes }
: undefined;
logger.log(level, { ...payload, attributes });
}
}

Expand Down
75 changes: 22 additions & 53 deletions src/helpers/deviceId.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,63 @@
import { getDeviceId } from "@mongodb-js/device-id";
import nodeMachineId from "node-machine-id";
import * as nodeMachineId from "node-machine-id";
import type { LoggerBase } from "../common/logger.js";
import { LogId } 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 static readonly UnknownDeviceId = Promise.resolve("unknown");

private deviceIdPromise: Promise<string>;
private abortController: AbortController;
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);
this.abortController = new AbortController();

this.deviceIdPromise = DeviceId.UnknownDeviceId;
Comment on lines 17 to +23
Copy link
Collaborator

@gagik gagik Aug 27, 2025

Choose a reason for hiding this comment

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

Suggested change
private constructor(logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT) {
this.logger = logger;
this.timeout = timeout;
this.getMachineId = (): Promise<string> => nodeMachineId.machineId(true);
this.abortController = new AbortController();
this.deviceIdPromise = DeviceId.UnknownDeviceId;
private constructor({logger, timeout, deviceIdPromise, abortController}: {logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT, deviceIdPromise: Promise<string>, abortController: AbortController}) {
this.logger = logger;
this.timeout = timeout;
this.abortController = abortController;
this.deviceIdPromise = deviceIdPromise;

I think this is better because it'd be good to have the Promise resolve to unknown only in the end so we can always have a state of pending -> unknown and not unknown -> pending -> unknown (even though in practice we instantly override this in create right now

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately, we can't supply the promise through the constructor since we need the DeviceId instance to construct the promise itself. Technically, the only reason to have it assigned here is to make the types nicer and ensure we don't have to write ? all over the place when we know for sure the field is initialized at this point. So we have 3 options (with no preference which one to go for):

  1. Keep it as is
  2. Suppress the error that we're not initializing a required field
  3. Move the getDeviceId call to the constructor (and close our eyes for the fact it's introducing a side effect)

}

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");
}
private initialize(): void {
this.deviceIdPromise = getDeviceId({
getMachineId: this.getMachineId,
onError: (reason, error) => {
this.handleDeviceIdError(reason, String(error));
},
timeout: this.timeout,
abortSignal: this.abortController.signal,
});
}

public static create(logger: LoggerBase, timeout?: number): DeviceId {
Copy link
Collaborator

@gagik gagik Aug 27, 2025

Choose a reason for hiding this comment

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

Suggested change
public static create(logger: LoggerBase, timeout?: number): DeviceId {
public static create(logger: LoggerBase, { timeout = DEVICE_ID_TIMEOUT } : { timeout: number} = {}): DeviceId {
const abortController = new AbortController();
const deviceIdPromise = () => getDeviceId({
getMachineId: () => nodeMachineId.machineId(true),
onError: (reason, error) => {
this.handleDeviceIdError(reason, String(error));
},
timeout: this.timeout,
abortSignal: abortController.signal,
});
const instance = new DeviceId({logger, timeout, deviceIdPromise, abortController});

personal preference re: timeout as object because it's one of these things which we basically largely have only for test DI purposes

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

this.instance = instance;
instance.initialize();

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;
this.abortController.abort();
}

/**
* 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");
this.deviceIdPromise = DeviceId.UnknownDeviceId;

switch (reason) {
case "resolutionError":
Expand Down
5 changes: 4 additions & 1 deletion src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { Server, type ServerOptions } from "./server.js";
export { Telemetry } from "./telemetry/telemetry.js";
export { Session, type SessionOptions } from "./common/session.js";
export type { UserConfig } from "./common/config.js";
export { type UserConfig, defaultUserConfig } from "./common/config.js";
export { StreamableHttpRunner } from "./transports/streamableHttp.js";
export { LoggerBase } from "./common/logger.js";
export type { LogPayload, LoggerType, LogLevel } from "./common/logger.js";
5 changes: 3 additions & 2 deletions src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export abstract class TransportRunnerBase {

protected constructor(
protected readonly userConfig: UserConfig,
private readonly driverOptions: DriverOptions
private readonly driverOptions: DriverOptions,
additionalLoggers: LoggerBase[]
) {
const loggers: LoggerBase[] = [];
const loggers: LoggerBase[] = [...additionalLoggers];
if (this.userConfig.loggers.includes("stderr")) {
loggers.push(new ConsoleLogger());
}
Expand Down
5 changes: 3 additions & 2 deletions src/transports/stdio.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LoggerBase } from "../common/logger.js";
import { LogId } from "../common/logger.js";
import type { Server } from "../server.js";
import { TransportRunnerBase } from "./base.js";
Expand Down Expand Up @@ -54,8 +55,8 @@ export function createStdioTransport(): StdioServerTransport {
export class StdioRunner extends TransportRunnerBase {
private server: Server | undefined;

constructor(userConfig: UserConfig, driverOptions: DriverOptions) {
super(userConfig, driverOptions);
constructor(userConfig: UserConfig, driverOptions: DriverOptions, additionalLoggers: LoggerBase[] = []) {
super(userConfig, driverOptions, additionalLoggers);
}

async start(): Promise<void> {
Expand Down
30 changes: 27 additions & 3 deletions src/transports/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { TransportRunnerBase } from "./base.js";
import type { DriverOptions, UserConfig } from "../common/config.js";
import type { LoggerBase } from "../common/logger.js";
import { LogId } from "../common/logger.js";
import { randomUUID } from "crypto";
import { SessionStore } from "../common/sessionStore.js";
Expand All @@ -18,8 +19,20 @@ export class StreamableHttpRunner extends TransportRunnerBase {
private httpServer: http.Server | undefined;
private sessionStore!: SessionStore;

constructor(userConfig: UserConfig, driverOptions: DriverOptions) {
super(userConfig, driverOptions);
public get serverAddress(): string {
const result = this.httpServer?.address();
if (typeof result === "string") {
return result;
}
if (typeof result === "object" && result) {
return `http://${result.address}:${result.port}`;
}

throw new Error("Server is not started yet");
}

constructor(userConfig: UserConfig, driverOptions: DriverOptions, additionalLoggers: LoggerBase[] = []) {
super(userConfig, driverOptions, additionalLoggers);
}

async start(): Promise<void> {
Expand All @@ -32,6 +45,17 @@ export class StreamableHttpRunner extends TransportRunnerBase {

app.enable("trust proxy"); // needed for reverse proxy support
app.use(express.json());
app.use((req, res, next) => {
for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) {
const header = req.headers[key.toLowerCase()];
if (!header || header !== value) {
res.status(403).send({ error: `Invalid value for header "${key}"` });
return;
}
}

next();
});

const handleSessionRequest = async (req: express.Request, res: express.Response): Promise<void> => {
const sessionId = req.headers["mcp-session-id"];
Expand Down Expand Up @@ -142,7 +166,7 @@ export class StreamableHttpRunner extends TransportRunnerBase {
this.logger.info({
id: LogId.streamableHttpTransportStarted,
context: "streamableHttpTransport",
message: `Server started on http://${this.userConfig.httpHost}:${this.userConfig.httpPort}`,
message: `Server started on ${this.serverAddress}`,
noRedaction: true,
});
}
Expand Down
9 changes: 8 additions & 1 deletion tests/integration/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ describe("Build Test", () => {
const esmKeys = Object.keys(esmModule).sort();

expect(cjsKeys).toEqual(esmKeys);
expect(cjsKeys).toEqual(["Server", "Session", "Telemetry"]);
expect(cjsKeys).toIncludeSameMembers([
"Server",
"Session",
"Telemetry",
"StreamableHttpRunner",
"defaultUserConfig",
"LoggerBase",
]);
});
});
Loading
Loading