diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b4288eef3..74e0a9210 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,4 +55,4 @@ jobs: rm -rf node_modules npm pkg set scripts.prepare="exit 0" npm install --omit=dev - - run: npx -y @modelcontextprotocol/inspector@0.16.2 --cli --method tools/list -- node dist/esm/index.js + - run: npx -y @modelcontextprotocol/inspector@0.16.5 --cli --method tools/list -- node dist/esm/index.js diff --git a/package-lock.json b/package-lock.json index 488d9ad2c..d31481819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@ai-sdk/google": "^1.2.22", "@ai-sdk/openai": "^1.3.23", "@eslint/js": "^9.30.1", - "@modelcontextprotocol/inspector": "^0.16.0", + "@modelcontextprotocol/inspector": "^0.16.5", "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.7", "@types/express": "^5.0.1", diff --git a/package.json b/package.json index e943a8277..938c8ae25 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@ai-sdk/google": "^1.2.22", "@ai-sdk/openai": "^1.3.23", "@eslint/js": "^9.30.1", - "@modelcontextprotocol/inspector": "^0.16.0", + "@modelcontextprotocol/inspector": "^0.16.5", "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.7", "@types/express": "^5.0.1", diff --git a/src/common/logger.ts b/src/common/logger.ts index 0bfafbb2c..40b54cc66 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -2,9 +2,9 @@ import fs from "fs/promises"; import type { MongoLogId, MongoLogWriter } from "mongodb-log-writer"; import { mongoLogId, MongoLogManager } from "mongodb-log-writer"; import redact from "mongodb-redact"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js"; import { EventEmitter } from "events"; +import type { Server } from "../lib.js"; export type LogLevel = LoggingMessageNotification["params"]["level"]; @@ -250,7 +250,18 @@ export class DiskLogger extends LoggerBase<{ initialized: [] }> { } export class McpLogger extends LoggerBase { - public constructor(private readonly server: McpServer) { + private static readonly LOG_LEVELS: LogLevel[] = [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency", + ]; + + public constructor(private readonly server: Server) { super(); } @@ -258,11 +269,18 @@ export class McpLogger extends LoggerBase { protected logCore(level: LogLevel, payload: LogPayload): void { // Only log if the server is connected - if (!this.server?.isConnected()) { + if (!this.server.mcpServer.isConnected()) { return; } - void this.server.server.sendLoggingMessage({ + const minimumLevel = McpLogger.LOG_LEVELS.indexOf(this.server.mcpLogLevel); + const currentLevel = McpLogger.LOG_LEVELS.indexOf(level); + if (minimumLevel > currentLevel) { + // Don't log if the requested level is lower than the minimum level + return; + } + + void this.server.mcpServer.server.sendLoggingMessage({ level, data: `[${payload.context}]: ${payload.message}`, }); diff --git a/src/server.ts b/src/server.ts index d036968a3..06a216948 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; import { Resources } from "./resources/resources.js"; +import type { LogLevel } from "./common/logger.js"; import { LogId } from "./common/logger.js"; import type { Telemetry } from "./telemetry/telemetry.js"; import type { UserConfig } from "./common/config.js"; @@ -12,6 +13,7 @@ import { type ServerCommand } from "./telemetry/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { CallToolRequestSchema, + SetLevelRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; @@ -32,6 +34,13 @@ export class Server { private readonly telemetry: Telemetry; public readonly userConfig: UserConfig; public readonly tools: ToolBase[] = []; + + private _mcpLogLevel: LogLevel = "debug"; + + public get mcpLogLevel(): LogLevel { + return this._mcpLogLevel; + } + private readonly startTime: number; private readonly subscriptions = new Set(); @@ -97,6 +106,11 @@ export class Server { return {}; }); + this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => { + this._mcpLogLevel = params.level; + return {}; + }); + this.mcpServer.server.oninitialized = (): void => { 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. diff --git a/src/transports/base.ts b/src/transports/base.ts index 4cbcc293e..485752e7b 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -44,12 +44,7 @@ export abstract class TransportRunnerBase { version: packageInfo.version, }); - const loggers = [this.logger]; - if (this.userConfig.loggers.includes("mcp")) { - loggers.push(new McpLogger(mcpServer)); - } - - const logger = new CompositeLogger(...loggers); + const logger = new CompositeLogger(this.logger); const exportsManager = ExportsManager.init(this.userConfig, logger); const connectionManager = new ConnectionManager(this.userConfig, this.driverOptions, logger, this.deviceId); @@ -64,12 +59,20 @@ export abstract class TransportRunnerBase { const telemetry = Telemetry.create(session, this.userConfig, this.deviceId); - return new Server({ + const result = new Server({ mcpServer, session, telemetry, userConfig: this.userConfig, }); + + // We need to create the MCP logger after the server is constructed + // because it needs the server instance + if (this.userConfig.loggers.includes("mcp")) { + logger.addLogger(new McpLogger(result)); + } + + return result; } abstract start(): Promise; diff --git a/tests/unit/logger.test.ts b/tests/unit/logger.test.ts index 32f5181e6..6bddc782d 100644 --- a/tests/unit/logger.test.ts +++ b/tests/unit/logger.test.ts @@ -1,12 +1,12 @@ import type { MockInstance } from "vitest"; import { describe, beforeEach, afterEach, vi, it, expect } from "vitest"; -import type { LoggerType } from "../../src/common/logger.js"; +import type { LoggerType, LogLevel } from "../../src/common/logger.js"; import { CompositeLogger, ConsoleLogger, DiskLogger, LogId, McpLogger } from "../../src/common/logger.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import os from "os"; import * as path from "path"; import * as fs from "fs/promises"; import { once } from "events"; +import type { Server } from "../../src/server.js"; describe("Logger", () => { let consoleErrorSpy: MockInstance; @@ -14,6 +14,7 @@ describe("Logger", () => { let mcpLoggerSpy: MockInstance; let mcpLogger: McpLogger; + let minimumMcpLogLevel: LogLevel; beforeEach(() => { // Mock console.error before creating the ConsoleLogger @@ -22,12 +23,18 @@ describe("Logger", () => { consoleLogger = new ConsoleLogger(); mcpLoggerSpy = vi.fn(); + minimumMcpLogLevel = "debug"; mcpLogger = new McpLogger({ - server: { - sendLoggingMessage: mcpLoggerSpy, + mcpServer: { + server: { + sendLoggingMessage: mcpLoggerSpy, + }, + isConnected: () => true, }, - isConnected: () => true, - } as unknown as McpServer); + get mcpLogLevel() { + return minimumMcpLogLevel; + }, + } as unknown as Server); }); afterEach(() => { @@ -306,4 +313,24 @@ describe("Logger", () => { }); }); }); + + describe("mcp logger", () => { + it("filters out messages below the minimum log level", () => { + minimumMcpLogLevel = "debug"; + mcpLogger.log("debug", { id: LogId.serverInitialized, context: "test", message: "Debug message" }); + + expect(mcpLoggerSpy).toHaveBeenCalledOnce(); + expect(getLastMcpLogMessage()).toContain("Debug message"); + + minimumMcpLogLevel = "info"; + mcpLogger.log("debug", { id: LogId.serverInitialized, context: "test", message: "Debug message 2" }); + + expect(mcpLoggerSpy).toHaveBeenCalledTimes(1); + + mcpLogger.log("alert", { id: LogId.serverInitialized, context: "test", message: "Alert message" }); + + expect(mcpLoggerSpy).toHaveBeenCalledTimes(2); + expect(getLastMcpLogMessage()).toContain("Alert message"); + }); + }); });