From 524926672670ee6ac3a72107870a059929c8973f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 21 Aug 2025 13:45:06 +0200 Subject: [PATCH 01/11] fix: minor tweaks to enable vscode integration --- src/common/config.ts | 1 + src/lib.ts | 3 ++- src/transports/streamableHttp.ts | 25 +++++++++++++++++++ .../transports/streamableHttp.test.ts | 3 ++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index aebd6e73a..81bfd5c03 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -116,6 +116,7 @@ export interface UserConfig extends CliOptions { transport: "stdio" | "http"; httpPort: number; httpHost: string; + httpHeaders?: Record; loggers: Array<"stderr" | "disk" | "mcp">; idleTimeoutMs: number; notificationTimeoutMs: number; diff --git a/src/lib.ts b/src/lib.ts index 7843a9cda..059582e6e 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,4 +1,5 @@ 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"; diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index e6e93ba7c..515296a59 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -18,6 +18,18 @@ export class StreamableHttpRunner extends TransportRunnerBase { private httpServer: http.Server | undefined; private sessionStore!: SessionStore; + public get address(): 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) { super(userConfig); } @@ -32,6 +44,19 @@ export class StreamableHttpRunner extends TransportRunnerBase { app.enable("trust proxy"); // needed for reverse proxy support app.use(express.json()); + app.use((req, res, next) => { + if (this.userConfig.httpHeaders) { + for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) { + const header = req.headers[key]; + if (!header || header !== value) { + res.sendStatus(403).json({ error: `Invalid ${key} header` }); + return; + } + } + } + + next(); + }); const handleSessionRequest = async (req: express.Request, res: express.Response): Promise => { const sessionId = req.headers["mcp-session-id"]; diff --git a/tests/integration/transports/streamableHttp.test.ts b/tests/integration/transports/streamableHttp.test.ts index d5b6e0be7..0904737f5 100644 --- a/tests/integration/transports/streamableHttp.test.ts +++ b/tests/integration/transports/streamableHttp.test.ts @@ -14,6 +14,7 @@ describe("StreamableHttpRunner", () => { oldLoggers = config.loggers; config.telemetry = "disabled"; config.loggers = ["stderr"]; + config.httpPort = 0; // Use a random port for testing runner = new StreamableHttpRunner(config); await runner.start(); }); @@ -28,7 +29,7 @@ describe("StreamableHttpRunner", () => { let client: Client; let transport: StreamableHTTPClientTransport; beforeAll(async () => { - transport = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:3000/mcp")); + transport = new StreamableHTTPClientTransport(new URL(`${runner.address}/mcp`)); client = new Client({ name: "test", From 5f1e1b25c0926a9606e34410783b2da9db802397 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 21 Aug 2025 15:15:40 +0200 Subject: [PATCH 02/11] add tests --- src/common/config.ts | 3 +- src/transports/streamableHttp.ts | 12 +-- .../transports/streamableHttp.test.ts | 98 ++++++++++++++----- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 81bfd5c03..414e91589 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -116,7 +116,7 @@ export interface UserConfig extends CliOptions { transport: "stdio" | "http"; httpPort: number; httpHost: string; - httpHeaders?: Record; + httpHeaders: Record; loggers: Array<"stderr" | "disk" | "mcp">; idleTimeoutMs: number; notificationTimeoutMs: number; @@ -138,6 +138,7 @@ export const defaultUserConfig: UserConfig = { loggers: ["disk", "mcp"], idleTimeoutMs: 600000, // 10 minutes notificationTimeoutMs: 540000, // 9 minutes + httpHeaders: {}, }; export const config = setupUserConfig({ diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index 515296a59..4faa18ea4 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -45,13 +45,11 @@ export class StreamableHttpRunner extends TransportRunnerBase { app.enable("trust proxy"); // needed for reverse proxy support app.use(express.json()); app.use((req, res, next) => { - if (this.userConfig.httpHeaders) { - for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) { - const header = req.headers[key]; - if (!header || header !== value) { - res.sendStatus(403).json({ error: `Invalid ${key} header` }); - return; - } + for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) { + const header = req.headers[key]; + if (!header || header !== value) { + res.sendStatus(403).json({ error: `Invalid ${key} header` }); + return; } } diff --git a/tests/integration/transports/streamableHttp.test.ts b/tests/integration/transports/streamableHttp.test.ts index 0904737f5..44929e6e1 100644 --- a/tests/integration/transports/streamableHttp.test.ts +++ b/tests/integration/transports/streamableHttp.test.ts @@ -9,14 +9,12 @@ describe("StreamableHttpRunner", () => { let oldTelemetry: "enabled" | "disabled"; let oldLoggers: ("stderr" | "disk" | "mcp")[]; - beforeAll(async () => { + beforeAll(() => { oldTelemetry = config.telemetry; oldLoggers = config.loggers; config.telemetry = "disabled"; config.loggers = ["stderr"]; config.httpPort = 0; // Use a random port for testing - runner = new StreamableHttpRunner(config); - await runner.start(); }); afterAll(async () => { @@ -25,33 +23,81 @@ describe("StreamableHttpRunner", () => { config.loggers = oldLoggers; }); - describe("client connects successfully", () => { - let client: Client; - let transport: StreamableHTTPClientTransport; - beforeAll(async () => { - transport = new StreamableHTTPClientTransport(new URL(`${runner.address}/mcp`)); + const headerTestCases: { headers: Record; description: string }[] = [ + { headers: {}, description: "without headers" }, + { headers: { "x-custom-header": "test-value" }, description: "with headers" }, + ]; - client = new Client({ - name: "test", - version: "0.0.0", + for (const { headers, description } of headerTestCases) { + describe(description, () => { + beforeAll(async () => { + config.httpHeaders = headers; + runner = new StreamableHttpRunner(config); + await runner.start(); }); - await client.connect(transport); - }); - afterAll(async () => { - await client.close(); - await transport.close(); - }); + const clientHeaderTestCases = [ + { + headers: {}, + description: "without client headers", + expectSuccess: Object.keys(headers).length === 0, + }, + { headers, description: "with matching client headers", expectSuccess: true }, + { headers: { ...headers, foo: "bar" }, description: "with extra client headers", expectSuccess: true }, + { + headers: { foo: "bar" }, + description: "with non-matching client headers", + expectSuccess: Object.keys(headers).length === 0, + }, + ]; + + for (const { + headers: clientHeaders, + description: clientDescription, + expectSuccess, + } of clientHeaderTestCases) { + describe(clientDescription, () => { + let client: Client; + let transport: StreamableHTTPClientTransport; + beforeAll(() => { + client = new Client({ + name: "test", + version: "0.0.0", + }); + transport = new StreamableHTTPClientTransport(new URL(`${runner.address}/mcp`), { + requestInit: { + headers: clientHeaders, + }, + }); + }); - it("handles requests and sends responses", async () => { - const response = await client.listTools(); - expect(response).toBeDefined(); - expect(response.tools).toBeDefined(); - expect(response.tools.length).toBeGreaterThan(0); + afterAll(async () => { + await client.close(); + await transport.close(); + }); - const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); - expect(sortedTools[0]?.name).toBe("aggregate"); - expect(sortedTools[0]?.description).toBe("Run an aggregation against a MongoDB collection"); + it(`should ${expectSuccess ? "succeed" : "fail"}`, async () => { + try { + await client.connect(transport); + const response = await client.listTools(); + expect(response).toBeDefined(); + expect(response.tools).toBeDefined(); + expect(response.tools.length).toBeGreaterThan(0); + + const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); + expect(sortedTools[0]?.name).toBe("aggregate"); + expect(sortedTools[0]?.description).toBe("Run an aggregation against a MongoDB collection"); + } catch (err) { + if (expectSuccess) { + throw err; + } else { + expect(err).toBeDefined(); + expect(err?.toString()).toContain("HTTP 403"); + } + } + }); + }); + } }); - }); + } }); From 2ab7f427442fceed67b8b9b12abf881a4aca323f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 21 Aug 2025 16:02:30 +0200 Subject: [PATCH 03/11] Better formatted error --- src/transports/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index 4faa18ea4..d5bc36720 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -48,7 +48,7 @@ export class StreamableHttpRunner extends TransportRunnerBase { for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) { const header = req.headers[key]; if (!header || header !== value) { - res.sendStatus(403).json({ error: `Invalid ${key} header` }); + res.status(403).send({ error: `Invalid value for header "${key}"` }); return; } } From 8c3c968d8e00e38a53662f1576eae4920f6e986c Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 21 Aug 2025 18:43:08 +0200 Subject: [PATCH 04/11] lowercase the key --- src/transports/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index d5bc36720..9e66e18d4 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -46,7 +46,7 @@ export class StreamableHttpRunner extends TransportRunnerBase { app.use(express.json()); app.use((req, res, next) => { for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) { - const header = req.headers[key]; + const header = req.headers[key.toLowerCase()]; if (!header || header !== value) { res.status(403).send({ error: `Invalid value for header "${key}"` }); return; From f29226f0e1c2f589f5d8818ca2c3f538ee1733ba Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 21 Aug 2025 20:36:50 +0200 Subject: [PATCH 05/11] fix build tests --- tests/integration/build.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index 7282efe45..451db3db5 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -41,6 +41,6 @@ describe("Build Test", () => { const esmKeys = Object.keys(esmModule).sort(); expect(cjsKeys).toEqual(esmKeys); - expect(cjsKeys).toEqual(["Server", "Session", "Telemetry"]); + expect(cjsKeys).toEqual(["Server", "Session", "Telemetry", "StreamableHttpRunner", "defaultUserConfig"]); }); }); From e85b2eb6414cd4987d091d8948c5badd685ff25a Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 22 Aug 2025 11:37:21 +0200 Subject: [PATCH 06/11] fix tests again --- tests/integration/build.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index 451db3db5..1d9a5d0ea 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -41,6 +41,12 @@ describe("Build Test", () => { const esmKeys = Object.keys(esmModule).sort(); expect(cjsKeys).toEqual(esmKeys); - expect(cjsKeys).toEqual(["Server", "Session", "Telemetry", "StreamableHttpRunner", "defaultUserConfig"]); + expect(cjsKeys).toIncludeSameMembers([ + "Server", + "Session", + "Telemetry", + "StreamableHttpRunner", + "defaultUserConfig", + ]); }); }); From 3c111b076576ba57ab90699940917f6f344f2da1 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 22 Aug 2025 22:59:56 +0200 Subject: [PATCH 07/11] fix tests, refactor deviceId --- src/helpers/deviceId.ts | 73 ++++++------------- .../transports/streamableHttp.test.ts | 32 ++++++-- tests/unit/helpers/deviceId.test.ts | 13 +++- 3 files changed, 56 insertions(+), 62 deletions(-) diff --git a/src/helpers/deviceId.ts b/src/helpers/deviceId.ts index 246b0bd1a..e6ddbebbd 100644 --- a/src/helpers/deviceId.ts +++ b/src/helpers/deviceId.ts @@ -5,49 +5,46 @@ import { LogId, LoggerBase } from "../common/logger.js"; export const DEVICE_ID_TIMEOUT = 3000; export class DeviceId { - private deviceId: string | undefined = undefined; - private deviceIdPromise: Promise | undefined = undefined; - private abortController: AbortController | undefined = undefined; + private static readonly UnknownDeviceId = Promise.resolve("unknown"); + + private deviceIdPromise: Promise; + private abortController: AbortController; private logger: LoggerBase; private readonly getMachineId: () => Promise; 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 => nodeMachineId.machineId(true); + this.abortController = new AbortController(); + + this.deviceIdPromise = DeviceId.UnknownDeviceId; } - 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 { 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(); } /** @@ -55,39 +52,11 @@ export class DeviceId { * @returns Promise that resolves to the device ID string */ public get(): Promise { - 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 { - 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": diff --git a/tests/integration/transports/streamableHttp.test.ts b/tests/integration/transports/streamableHttp.test.ts index 44929e6e1..d421039ab 100644 --- a/tests/integration/transports/streamableHttp.test.ts +++ b/tests/integration/transports/streamableHttp.test.ts @@ -17,12 +17,6 @@ describe("StreamableHttpRunner", () => { config.httpPort = 0; // Use a random port for testing }); - afterAll(async () => { - await runner.close(); - config.telemetry = oldTelemetry; - config.loggers = oldLoggers; - }); - const headerTestCases: { headers: Record; description: string }[] = [ { headers: {}, description: "without headers" }, { headers: { "x-custom-header": "test-value" }, description: "with headers" }, @@ -36,6 +30,13 @@ describe("StreamableHttpRunner", () => { await runner.start(); }); + afterAll(async () => { + await runner.close(); + config.telemetry = oldTelemetry; + config.loggers = oldLoggers; + config.httpHeaders = {}; + }); + const clientHeaderTestCases = [ { headers: {}, @@ -100,4 +101,23 @@ describe("StreamableHttpRunner", () => { } }); } + + it("can create multiple runners", async () => { + const runners: StreamableHttpRunner[] = []; + try { + for (let i = 0; i < 3; i++) { + config.httpPort = 0; // Use a random port for each runner + const runner = new StreamableHttpRunner(config); + await runner.start(); + runners.push(runner); + } + + const addresses = new Set(runners.map((r) => r.address)); + expect(addresses.size).toBe(runners.length); + } finally { + for (const runner of runners) { + await runner.close(); + } + } + }); }); diff --git a/tests/unit/helpers/deviceId.test.ts b/tests/unit/helpers/deviceId.test.ts index 68fd54e08..3b6112f72 100644 --- a/tests/unit/helpers/deviceId.test.ts +++ b/tests/unit/helpers/deviceId.test.ts @@ -22,11 +22,16 @@ describe("deviceId", () => { deviceId.close(); }); - it("should fail to create separate instances", () => { + it("should return different instance from create", async () => { deviceId = DeviceId.create(testLogger); - - // try to create a new device id and see it raises an error - expect(() => DeviceId.create(testLogger)).toThrow("DeviceId instance already exists"); + let second: DeviceId | undefined; + try { + second = DeviceId.create(testLogger); + expect(second === deviceId).toBe(false); + expect(await second.get()).toBe(await deviceId.get()); + } finally { + second?.close(); + } }); it("should successfully retrieve device ID", async () => { From f5481fddcd2947ed8b940be1fdff218e99549c46 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 26 Aug 2025 21:25:35 +0200 Subject: [PATCH 08/11] more tweaks --- src/common/logger.ts | 48 ++++++++++--------- src/helpers/deviceId.ts | 5 +- src/lib.ts | 2 + src/transports/base.ts | 5 +- src/transports/stdio.ts | 5 +- src/transports/streamableHttp.ts | 5 +- .../transports/streamableHttp.test.ts | 35 +++++++++++++- 7 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/common/logger.ts b/src/common/logger.ts index b172ec54c..1cdd0c4a3 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -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; @@ -152,6 +152,26 @@ export abstract class LoggerBase = 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 { @@ -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 { @@ -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 }); } } diff --git a/src/helpers/deviceId.ts b/src/helpers/deviceId.ts index 628a5bcda..655b265b2 100644 --- a/src/helpers/deviceId.ts +++ b/src/helpers/deviceId.ts @@ -1,5 +1,6 @@ import { getDeviceId } from "@mongodb-js/device-id"; -import nodeMachineId from "node-machine-id"; +// Important: don't import the default module as that doesn't work for cjs module resolution. +import { machineId } from "node-machine-id"; import type { LoggerBase } from "../common/logger.js"; import { LogId } from "../common/logger.js"; @@ -17,7 +18,7 @@ export class DeviceId { private constructor(logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT) { this.logger = logger; this.timeout = timeout; - this.getMachineId = (): Promise => nodeMachineId.machineId(true); + this.getMachineId = (): Promise => machineId(true); this.abortController = new AbortController(); this.deviceIdPromise = DeviceId.UnknownDeviceId; diff --git a/src/lib.ts b/src/lib.ts index 059582e6e..9fd921e4c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -3,3 +3,5 @@ export { Telemetry } from "./telemetry/telemetry.js"; export { Session, type SessionOptions } from "./common/session.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"; diff --git a/src/transports/base.ts b/src/transports/base.ts index 17a0ff5e7..4cbcc293e 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -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()); } diff --git a/src/transports/stdio.ts b/src/transports/stdio.ts index d0619da6c..0751cac7b 100644 --- a/src/transports/stdio.ts +++ b/src/transports/stdio.ts @@ -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"; @@ -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 { diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index b4f352415..945c51565 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -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"; @@ -30,8 +31,8 @@ export class StreamableHttpRunner extends TransportRunnerBase { throw new Error("Server is not started yet"); } - constructor(userConfig: UserConfig, driverOptions: DriverOptions) { - super(userConfig, driverOptions); + constructor(userConfig: UserConfig, driverOptions: DriverOptions, additionalLoggers: LoggerBase[] = []) { + super(userConfig, driverOptions, additionalLoggers); } async start(): Promise { diff --git a/tests/integration/transports/streamableHttp.test.ts b/tests/integration/transports/streamableHttp.test.ts index f7e11d20e..47bd4019a 100644 --- a/tests/integration/transports/streamableHttp.test.ts +++ b/tests/integration/transports/streamableHttp.test.ts @@ -1,8 +1,10 @@ import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { describe, expect, it, beforeAll, afterAll } from "vitest"; +import { describe, expect, it, beforeAll, afterAll, beforeEach } from "vitest"; import { config, driverOptions } from "../../../src/common/config.js"; +import type { LoggerType, LogLevel, LogPayload } from "../../../src/common/logger.js"; +import { LoggerBase, LogId } from "../../../src/common/logger.js"; describe("StreamableHttpRunner", () => { let runner: StreamableHttpRunner; @@ -120,4 +122,35 @@ describe("StreamableHttpRunner", () => { } } }); + + describe("with custom logger", () => { + beforeEach(() => { + config.loggers = []; + }); + + class CustomLogger extends LoggerBase { + protected type?: LoggerType = "console"; + public messages: { level: LogLevel; payload: LogPayload }[] = []; + protected logCore(level: LogLevel, payload: LogPayload): void { + this.messages.push({ level, payload }); + } + } + + it("can provide custom logger", async () => { + const logger = new CustomLogger(); + const runner = new StreamableHttpRunner(config, driverOptions, [logger]); + await runner.start(); + + const messages = logger.messages; + expect(messages.length).toBeGreaterThan(0); + + const serverStartedMessage = messages.filter( + (m) => m.payload.id === LogId.streamableHttpTransportStarted + )[0]; + expect(serverStartedMessage).toBeDefined(); + expect(serverStartedMessage?.payload.message).toContain("Server started on"); + expect(serverStartedMessage?.payload.context).toBe("streamableHttpTransport"); + expect(serverStartedMessage?.level).toBe("info"); + }); + }); }); From 2b77ed890a948ad3400bf41143c22c9b3fb75352 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 26 Aug 2025 21:41:10 +0200 Subject: [PATCH 09/11] fix tests --- src/helpers/deviceId.ts | 5 ++--- src/transports/streamableHttp.ts | 2 +- tests/integration/build.test.ts | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/deviceId.ts b/src/helpers/deviceId.ts index 655b265b2..1282e1b79 100644 --- a/src/helpers/deviceId.ts +++ b/src/helpers/deviceId.ts @@ -1,6 +1,5 @@ import { getDeviceId } from "@mongodb-js/device-id"; -// Important: don't import the default module as that doesn't work for cjs module resolution. -import { machineId } from "node-machine-id"; +import * as nodeMachineId from "node-machine-id"; import type { LoggerBase } from "../common/logger.js"; import { LogId } from "../common/logger.js"; @@ -18,7 +17,7 @@ export class DeviceId { private constructor(logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT) { this.logger = logger; this.timeout = timeout; - this.getMachineId = (): Promise => machineId(true); + this.getMachineId = (): Promise => nodeMachineId.machineId(true); this.abortController = new AbortController(); this.deviceIdPromise = DeviceId.UnknownDeviceId; diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index 945c51565..897972acc 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -166,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.address}`, noRedaction: true, }); } diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index 1d9a5d0ea..f5b26827e 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -47,6 +47,7 @@ describe("Build Test", () => { "Telemetry", "StreamableHttpRunner", "defaultUserConfig", + "LoggerBase", ]); }); }); From 8b2e7c74355d32f1d3ec6df643e47e3fd2d522bf Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 26 Aug 2025 22:08:31 +0200 Subject: [PATCH 10/11] run permissions monitor on ubuntu only --- .github/workflows/code_health.yaml | 2 +- .github/workflows/code_health_fork.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code_health.yaml b/.github/workflows/code_health.yaml index f59a84524..9a0b87dd9 100644 --- a/.github/workflows/code_health.yaml +++ b/.github/workflows/code_health.yaml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - if: matrix.os != 'windows-latest' + if: matrix.os == 'ubuntu-latest' - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/code_health_fork.yaml b/.github/workflows/code_health_fork.yaml index 8a852ca07..3bd34cd4f 100644 --- a/.github/workflows/code_health_fork.yaml +++ b/.github/workflows/code_health_fork.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - if: matrix.os != 'windows-latest' + if: matrix.os == 'ubuntu-latest' - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: From 9faea5d523fb33396ce4d6723fbb9949dd592a0e Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 27 Aug 2025 16:52:10 +0200 Subject: [PATCH 11/11] address -> serverAddress --- src/transports/streamableHttp.ts | 4 ++-- tests/integration/transports/streamableHttp.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index 897972acc..74ad3062c 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -19,7 +19,7 @@ export class StreamableHttpRunner extends TransportRunnerBase { private httpServer: http.Server | undefined; private sessionStore!: SessionStore; - public get address(): string { + public get serverAddress(): string { const result = this.httpServer?.address(); if (typeof result === "string") { return result; @@ -166,7 +166,7 @@ export class StreamableHttpRunner extends TransportRunnerBase { this.logger.info({ id: LogId.streamableHttpTransportStarted, context: "streamableHttpTransport", - message: `Server started on ${this.address}`, + message: `Server started on ${this.serverAddress}`, noRedaction: true, }); } diff --git a/tests/integration/transports/streamableHttp.test.ts b/tests/integration/transports/streamableHttp.test.ts index 47bd4019a..f45ce3cd3 100644 --- a/tests/integration/transports/streamableHttp.test.ts +++ b/tests/integration/transports/streamableHttp.test.ts @@ -67,7 +67,7 @@ describe("StreamableHttpRunner", () => { name: "test", version: "0.0.0", }); - transport = new StreamableHTTPClientTransport(new URL(`${runner.address}/mcp`), { + transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`), { requestInit: { headers: clientHeaders, }, @@ -114,7 +114,7 @@ describe("StreamableHttpRunner", () => { runners.push(runner); } - const addresses = new Set(runners.map((r) => r.address)); + const addresses = new Set(runners.map((r) => r.serverAddress)); expect(addresses.size).toBe(runners.length); } finally { for (const runner of runners) {