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

mongodb-js / mongodb-mcp-server / 23295186729

19 Mar 2026 12:38PM UTC coverage: 84.695% (-0.3%) from 84.953%
23295186729

push

github

web-flow
chore: skip coverage if tests fail (#989)

2084 of 2702 branches covered (77.13%)

Branch coverage included in aggregate %.

9902 of 11450 relevant lines covered (86.48%)

110.22 hits per line

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

79.56
/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 } 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 = "debug";
3✔
127

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

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

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

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

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

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

186
        if (!existingHandler) {
182!
187
            throw new Error("No existing handler found for CallToolRequestSchema");
×
188
        }
✔
189

190
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
181✔
191
            if (!request.params.arguments) {
956!
192
                request.params.arguments = {};
1✔
193
            }
1✔
194

195
            return existingHandler(request, extra);
956✔
196
        });
181✔
197

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

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

218
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
181✔
219
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
220
                throw new Error(`Invalid log level: ${params.level}`);
×
221
            }
×
222

223
            this._mcpLogLevel = params.level;
×
224
            return {};
×
225
        });
181✔
226

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

237
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
161✔
238
        };
161✔
239

240
        this.mcpServer.server.onclose = (): void => {
181✔
241
            const closeTime = Date.now();
177✔
242
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
177✔
243
        };
177✔
244

245
        this.mcpServer.server.onerror = (error: Error): void => {
181✔
246
            const closeTime = Date.now();
×
247
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
248
        };
×
249

250
        await this.mcpServer.connect(transport);
181✔
251
    }
182✔
252

253
    async close(): Promise<void> {
3✔
254
        await this.telemetry.close();
183✔
255
        await this.session.close();
183✔
256
        await this.mcpServer.close();
183✔
257
    }
183✔
258

259
    public sendResourceListChanged(): void {
3✔
260
        this.mcpServer.sendResourceListChanged();
1,010✔
261
    }
1,010✔
262

263
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
264
        return !!this.tools.filter((t) => t.category === name).length;
2✔
265
    }
2✔
266

267
    public sendResourceUpdated(uri: string): void {
3✔
268
        this.session.logger.info({
1,010✔
269
            id: LogId.resourceUpdateFailure,
1,010✔
270
            context: "resources",
1,010✔
271
            message: `Resource updated: ${uri}`,
1,010✔
272
        });
1,010✔
273

274
        if (this.subscriptions.has(uri)) {
1,010!
275
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
276
        }
20✔
277
    }
1,010✔
278

279
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
280
        const event: ServerEvent = {
338✔
281
            timestamp: new Date().toISOString(),
338✔
282
            source: "mdbmcp",
338✔
283
            properties: {
338✔
284
                result: "success",
338✔
285
                duration_ms: commandDuration,
338✔
286
                component: "server",
338✔
287
                category: "other",
338✔
288
                command: command,
338✔
289
            },
338✔
290
        };
338✔
291

292
        if (command === "start") {
338✔
293
            event.properties.startup_time_ms = commandDuration;
161✔
294
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
161!
295
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
161!
296
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
161!
297
            event.properties.previewFeatures = this.userConfig.previewFeatures;
161✔
298
        }
161✔
299
        if (command === "stop") {
338✔
300
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
177✔
301
            if (error) {
177!
302
                event.properties.result = "failure";
×
303
                event.properties.reason = error.message;
×
304
            }
×
305
        }
177✔
306

307
        this.telemetry.emitEvents([event]);
338✔
308
    }
338✔
309

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

330
    public registerResources(): void {
3✔
331
        for (const resourceConstructor of Resources) {
182✔
332
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
546✔
333
            resource.register(this);
546✔
334
        }
546✔
335
    }
182✔
336

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

350
        // Validate API client credentials
351
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
182!
352
            try {
19✔
353
                if (!this.session.apiClient) {
19!
354
                    throw new Error("API client is not available.");
×
355
                }
×
356

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

372
                await this.session.apiClient.validateAuthConfig();
19✔
373
            } catch (error) {
19!
374
                if (this.userConfig.connectionString === undefined) {
×
375
                    throw new Error(
×
376
                        `Failed to connect to MongoDB Atlas instance using the credentials from the config: ${error instanceof Error ? error.message : String(error)}`
×
377
                    );
×
378
                }
×
379

380
                this.session.logger.warning({
×
381
                    id: LogId.atlasCheckCredentials,
×
382
                    context: "server",
×
383
                    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.`,
×
384
                });
×
385
            }
×
386
        }
19✔
387
    }
182✔
388

389
    private async connectToConfigConnectionString(): Promise<void> {
3✔
390
        if (this.userConfig.connectionString) {
161!
391
            try {
8✔
392
                this.session.logger.info({
8✔
393
                    id: LogId.mongodbConnectTry,
8✔
394
                    context: "server",
8✔
395
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
8✔
396
                });
8✔
397
                await this.session.connectToConfiguredConnection();
8✔
398
            } catch (error) {
8✔
399
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
400
                this.session.logger.error({
2✔
401
                    id: LogId.mongodbConnectFailure,
2✔
402
                    context: "server",
2✔
403
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
2!
404
                });
2✔
405
            }
2✔
406
        }
8✔
407
    }
161✔
408
}
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