• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 23497041495

24 Mar 2026 03:15PM UTC coverage: 81.237%. First build
23497041495

Pull #1001

github

web-flow
Merge d1d926345 into de536380f
Pull Request #1001: feat: add log level minimum config

2108 of 2723 branches covered (77.41%)

Branch coverage included in aggregate %.

25 of 41 new or added lines in 6 files covered. (60.98%)

10071 of 12269 relevant lines covered (82.08%)

103.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.32
/src/server.ts
1
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
import type { Session } from "./common/session.js";
3
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
import { Resources } from "./resources/resources.js";
3✔
5
import type { LogLevel } from "./common/logging/index.js";
6
import { LogId, McpLogger, MCP_LOG_LEVELS } from "./common/logging/index.js";
3✔
7
import type { Telemetry } from "./telemetry/telemetry.js";
8
import type { UserConfig } from "./common/config/userConfig.js";
9
import { type ServerEvent } from "./telemetry/types.js";
10
import { type ServerCommand } from "./telemetry/types.js";
11
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
12
import {
3✔
13
    CallToolRequestSchema,
14
    SetLevelRequestSchema,
15
    SubscribeRequestSchema,
16
    UnsubscribeRequestSchema,
17
} from "@modelcontextprotocol/sdk/types.js";
18
import type { AnyToolBase, ToolCategory, ToolClass } from "./tools/tool.js";
19
import { validateConnectionString } from "./helpers/connectionOptions.js";
3✔
20
import { packageInfo } from "./common/packageInfo.js";
3✔
21
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
22
import type { Elicitation } from "./elicitation.js";
23
import { AllTools } from "./tools/index.js";
3✔
24
import type { UIRegistry } from "./ui/registry/index.js";
25
import type { Metrics, DefaultMetrics } from "./common/metrics/index.js";
26

27
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28
export type AnyToolClass = ToolClass<any, any, any>;
29

30
export interface ServerOptions<
31
    TUserConfig extends UserConfig = UserConfig,
32
    TContext = unknown,
33
    TMetrics extends DefaultMetrics = DefaultMetrics,
34
> {
35
    session: Session;
36
    userConfig: TUserConfig;
37
    mcpServer: McpServer;
38
    telemetry: Telemetry;
39
    elicitation: Elicitation;
40
    /** @deprecated Will be removed in a future version. Use `SessionOptions.connectionErrorHandler` instead. */
41
    connectionErrorHandler: ConnectionErrorHandler;
42
    uiRegistry?: UIRegistry;
43
    metrics: Metrics<TMetrics>;
44
    /**
45
     * An optional list of tools constructors to be registered to the MongoDB
46
     * MCP Server.
47
     *
48
     * When not provided, MongoDB MCP Server will register all internal tools.
49
     * When specified, **only** the tools in this list will be registered.
50
     *
51
     * This allows you to:
52
     * - Register only custom tools (excluding all internal tools)
53
     * - Register a subset of internal tools alongside custom tools
54
     * - Register all internal tools plus custom tools
55
     *
56
     * To include internal tools, import them from `mongodb-mcp-server/tools`:
57
     *
58
     * ```typescript
59
     * import { AllTools, AggregateTool, FindTool } from "mongodb-mcp-server/tools";
60
     *
61
     * // Register all internal tools plus custom tools
62
     * tools: [...AllTools, MyCustomTool]
63
     *
64
     * // Register only specific MongoDB tools plus custom tools
65
     * tools: [AggregateTool, FindTool, MyCustomTool]
66
     *
67
     * // Register all internal tools of mongodb category
68
     * tools: [AllTools.filter((tool) => tool.category === "mongodb")]
69
     * ```
70
     *
71
     * Note: Ensure that each tool has unique names otherwise the server will
72
     * throw an error when initializing an MCP Client session. If you're using
73
     * only the internal tools, then you don't have to worry about it unless,
74
     * you've overridden the tool names.
75
     *
76
     * To ensure that you provide compliant tool implementations extend your
77
     * tool implementation using `ToolBase` class and ensure that they conform
78
     * to `ToolClass` type.
79
     *
80
     * @see {@link ToolClass} for the type that tool classes must conform to
81
     * @see {@link ToolBase} for base class for all the tools
82
     */
83
    tools?: AnyToolClass[];
84
    /**
85
     * This context is available to tools via `this.toolContext` and can contain
86
     * any data you want to pass to tools definitions.
87
     *
88
     * @example
89
     * ```typescript
90
     * interface MyContext {
91
     *   tenantId: string;
92
     *   userId: string;
93
     *   features: { newUI: boolean };
94
     * }
95
     *
96
     * const server = new Server<MyContext>({
97
     *   // ... other options
98
     *   toolContext: {
99
     *     tenantId: "my-tenant",
100
     *     userId: "user-123",
101
     *     features: { newUI: true },
102
     *   },
103
     * });
104
     * ```
105
     */
106
    toolContext?: TContext;
107
}
108

109
export class Server<
3✔
110
    TUserConfig extends UserConfig = UserConfig,
111
    TContext = unknown,
112
    TMetrics extends DefaultMetrics = DefaultMetrics,
113
> {
3✔
114
    public readonly session: Session;
115
    public readonly mcpServer: McpServer;
116
    private readonly telemetry: Telemetry;
117
    public readonly userConfig: TUserConfig;
118
    public readonly elicitation: Elicitation;
119
    private readonly toolConstructors: AnyToolClass[];
120
    public readonly tools: AnyToolBase[] = [];
3✔
121
    public readonly connectionErrorHandler: ConnectionErrorHandler;
122
    public readonly uiRegistry?: UIRegistry;
123
    public readonly toolContext?: TContext;
124
    public readonly metrics: Metrics<TMetrics>;
125

126
    private _mcpLogLevel: LogLevel;
127
    /** Lowest log level allowed to be sent to the MCP client. */
128
    private readonly mcpLogLevelFloor: LogLevel;
129

130
    public get mcpLogLevel(): LogLevel {
3✔
131
        return this._mcpLogLevel;
3✔
132
    }
3✔
133

134
    private readonly startTime: number;
135
    private readonly subscriptions = new Set<string>();
3✔
136

137
    constructor({
3✔
138
        session,
193✔
139
        mcpServer,
193✔
140
        userConfig,
193✔
141
        telemetry,
193✔
142
        connectionErrorHandler,
193✔
143
        elicitation,
193✔
144
        tools,
193✔
145
        uiRegistry,
193✔
146
        toolContext,
193✔
147
        metrics,
193✔
148
    }: ServerOptions<TUserConfig, TContext, TMetrics>) {
193✔
149
        this.startTime = Date.now();
193✔
150
        this.session = session;
193✔
151
        this.telemetry = telemetry;
193✔
152
        this.mcpServer = mcpServer;
193✔
153
        this.userConfig = userConfig;
193✔
154
        this.elicitation = elicitation;
193✔
155
        this.connectionErrorHandler = connectionErrorHandler;
193✔
156
        this.toolConstructors = tools ?? AllTools;
193✔
157
        this.uiRegistry = uiRegistry;
193✔
158
        this.toolContext = toolContext;
193✔
159
        this.metrics = metrics;
193✔
160

161
        this._mcpLogLevel = userConfig.logLevel;
193✔
162
        this.mcpLogLevelFloor = this._mcpLogLevel;
193✔
163
    }
193✔
164

165
    async connect(transport: Transport): Promise<void> {
3✔
166
        await this.validateConfig();
182✔
167
        // Register resources after the server is initialized so they can listen to events like
168
        // connection events.
169
        this.registerResources();
182✔
170
        this.mcpServer.server.registerCapabilities({
182✔
171
            logging: {},
182✔
172
            resources: { listChanged: true, subscribe: true },
182✔
173
        });
182✔
174

175
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
176
        this.registerTools();
182✔
177

178
        // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
179
        // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
180
        // the tool accepts any arguments, even if they're all optional.
181
        //
182
        // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/server/mcp.ts#L705
183
        // Since paramsSchema here is not undefined, the server will create a non-optional z.object from it.
184
        const existingHandler = (
182✔
185
            this.mcpServer.server["_requestHandlers"] as Map<
182✔
186
                string,
187
                (request: unknown, extra: unknown) => Promise<CallToolResult>
188
            >
189
        ).get(CallToolRequestSchema.shape.method.value);
182✔
190

191
        if (!existingHandler) {
182!
192
            throw new Error("No existing handler found for CallToolRequestSchema");
×
193
        }
✔
194

195
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
181✔
196
            if (!request.params.arguments) {
954!
197
                request.params.arguments = {};
1✔
198
            }
1✔
199

200
            return existingHandler(request, extra);
954✔
201
        });
181✔
202

203
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
181✔
204
            this.subscriptions.add(params.uri);
20✔
205
            this.session.logger.debug({
20✔
206
                id: LogId.serverInitialized,
20✔
207
                context: "resources",
20✔
208
                message: `Client subscribed to resource: ${params.uri}`,
20✔
209
            });
20✔
210
            return {};
20✔
211
        });
181✔
212

213
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
181✔
214
            this.subscriptions.delete(params.uri);
×
215
            this.session.logger.debug({
×
216
                id: LogId.serverInitialized,
×
217
                context: "resources",
×
218
                message: `Client unsubscribed from resource: ${params.uri}`,
×
219
            });
×
220
            return {};
×
221
        });
181✔
222

223
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
181✔
224
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
225
                throw new Error(`Invalid log level: ${params.level}`);
×
226
            }
×
227

NEW
228
            const requestedIdx = MCP_LOG_LEVELS.indexOf(params.level);
×
NEW
229
            const floorIdx = MCP_LOG_LEVELS.indexOf(this.mcpLogLevelFloor);
×
NEW
230
            this._mcpLogLevel = requestedIdx >= floorIdx ? params.level : this.mcpLogLevelFloor;
×
231
            return {};
×
232
        });
181✔
233

234
        this.mcpServer.server.oninitialized = (): void => {
181✔
235
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
161✔
236
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
237
            void this.connectToConfigConnectionString();
161✔
238
            this.session.logger.info({
161✔
239
                id: LogId.serverInitialized,
161✔
240
                context: "server",
161✔
241
                message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
161✔
242
            });
161✔
243

244
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
161✔
245
        };
161✔
246

247
        this.mcpServer.server.onclose = (): void => {
181✔
248
            const closeTime = Date.now();
177✔
249
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
177✔
250
        };
177✔
251

252
        this.mcpServer.server.onerror = (error: Error): void => {
181✔
253
            const closeTime = Date.now();
×
254
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
255
        };
×
256

257
        await this.mcpServer.connect(transport);
181✔
258
    }
182✔
259

260
    async close(): Promise<void> {
3✔
261
        await this.telemetry.close();
183✔
262
        await this.session.close();
183✔
263
        await this.mcpServer.close();
183✔
264
    }
183✔
265

266
    public sendResourceListChanged(): void {
3✔
267
        this.mcpServer.sendResourceListChanged();
1,003✔
268
    }
1,003✔
269

270
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
271
        return !!this.tools.filter((t) => t.category === name).length;
2✔
272
    }
2✔
273

274
    public sendResourceUpdated(uri: string): void {
3✔
275
        this.session.logger.info({
1,003✔
276
            id: LogId.resourceUpdateFailure,
1,003✔
277
            context: "resources",
1,003✔
278
            message: `Resource updated: ${uri}`,
1,003✔
279
        });
1,003✔
280

281
        if (this.subscriptions.has(uri)) {
1,003!
282
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
283
        }
20✔
284
    }
1,003✔
285

286
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
287
        const event: ServerEvent = {
338✔
288
            timestamp: new Date().toISOString(),
338✔
289
            source: "mdbmcp",
338✔
290
            properties: {
338✔
291
                result: "success",
338✔
292
                duration_ms: commandDuration,
338✔
293
                component: "server",
338✔
294
                category: "other",
338✔
295
                command: command,
338✔
296
            },
338✔
297
        };
338✔
298

299
        if (command === "start") {
338✔
300
            event.properties.startup_time_ms = commandDuration;
161✔
301
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
161!
302
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
161!
303
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
161!
304
            event.properties.previewFeatures = this.userConfig.previewFeatures;
161✔
305
        }
161✔
306
        if (command === "stop") {
338✔
307
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
177✔
308
            if (error) {
177!
309
                event.properties.result = "failure";
×
310
            }
×
311
        }
177✔
312

313
        this.telemetry.emitEvents([event]);
338✔
314
    }
338✔
315

316
    public registerTools(): void {
3✔
317
        for (const toolConstructor of this.toolConstructors) {
184✔
318
            const tool = new toolConstructor({
7,172✔
319
                name: toolConstructor.toolName,
7,172✔
320
                category: toolConstructor.category,
7,172✔
321
                operationType: toolConstructor.operationType,
7,172✔
322
                session: this.session,
7,172✔
323
                config: this.userConfig,
7,172✔
324
                telemetry: this.telemetry,
7,172✔
325
                elicitation: this.elicitation,
7,172✔
326
                metrics: this.metrics,
7,172✔
327
                uiRegistry: this.uiRegistry,
7,172✔
328
                context: this.toolContext,
7,172✔
329
            });
7,172✔
330
            if (tool.register(this)) {
7,172✔
331
                this.tools.push(tool);
4,902✔
332
            }
4,902✔
333
        }
7,172✔
334
    }
184✔
335

336
    public registerResources(): void {
3✔
337
        for (const resourceConstructor of Resources) {
182✔
338
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
546✔
339
            resource.register(this);
546✔
340
        }
546✔
341
    }
182✔
342

343
    private async validateConfig(): Promise<void> {
3✔
344
        // Validate connection string
345
        if (this.userConfig.connectionString) {
182!
346
            try {
8✔
347
                validateConnectionString(this.userConfig.connectionString, false);
8✔
348
            } catch (error) {
8!
349
                throw new Error(
×
350
                    "Connection string validation failed with error: " +
×
351
                        (error instanceof Error ? error.message : String(error))
×
352
                );
×
353
            }
×
354
        }
8✔
355

356
        // Validate API client credentials
357
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
182!
358
            try {
19✔
359
                if (!this.session.apiClient) {
19!
360
                    throw new Error("API client is not available.");
×
361
                }
×
362

363
                try {
19✔
364
                    const apiBaseUrl = new URL(this.userConfig.apiBaseUrl);
19✔
365
                    if (apiBaseUrl.protocol !== "https:") {
19!
366
                        // Log a warning, but don't error out. This is to allow for testing against local or non-HTTPS endpoints.
367
                        const message = `apiBaseUrl is configured to use ${apiBaseUrl.protocol}, which is not secure. It is strongly recommended to use HTTPS for secure communication.`;
1✔
368
                        this.session.logger.warning({
1✔
369
                            id: LogId.atlasApiBaseUrlInsecure,
1✔
370
                            context: "server",
1✔
371
                            message,
1✔
372
                        });
1✔
373
                    }
1✔
374
                } catch (error) {
19!
375
                    throw new Error(`Invalid apiBaseUrl: ${error instanceof Error ? error.message : String(error)}`);
×
376
                }
×
377

378
                await this.session.apiClient.validateAuthConfig();
19✔
379
            } catch (error) {
19!
380
                if (this.userConfig.connectionString === undefined) {
×
381
                    throw new Error(
×
382
                        `Failed to connect to MongoDB Atlas instance using the credentials from the config: ${error instanceof Error ? error.message : String(error)}`
×
383
                    );
×
384
                }
×
385

386
                this.session.logger.warning({
×
387
                    id: LogId.atlasCheckCredentials,
×
388
                    context: "server",
×
389
                    message: `Failed to validate MongoDB Atlas API client credentials from the config: ${error instanceof Error ? error.message : String(error)}. Continuing since a connection string is also provided.`,
×
390
                });
×
391
            }
×
392
        }
19✔
393
    }
182✔
394

395
    private async connectToConfigConnectionString(): Promise<void> {
3✔
396
        if (this.userConfig.connectionString) {
161!
397
            try {
8✔
398
                this.session.logger.info({
8✔
399
                    id: LogId.mongodbConnectTry,
8✔
400
                    context: "server",
8✔
401
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
8✔
402
                });
8✔
403
                await this.session.connectToConfiguredConnection();
8✔
404
            } catch (error) {
8✔
405
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
406
                this.session.logger.error({
2✔
407
                    id: LogId.mongodbConnectFailure,
2✔
408
                    context: "server",
2✔
409
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
2!
410
                });
2✔
411
            }
2✔
412
        }
8✔
413
    }
161✔
414
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc