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

mongodb-js / mongodb-mcp-server / 22143610548

18 Feb 2026 02:23PM UTC coverage: 83.0% (-0.3%) from 83.295%
22143610548

push

github

web-flow
chore: release v1.6.1-prerelease.3 (#916)

Co-authored-by: mongodb-devtools-bot[bot] <189715634+mongodb-devtools-bot[bot]@users.noreply.github.com>

1684 of 2149 branches covered (78.36%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

109 existing lines in 10 files now uncovered.

7646 of 9092 relevant lines covered (84.1%)

123.55 hits per line

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

79.31
/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/logger.js";
6
import { LogId, McpLogger } from "./common/logger.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

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

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

103
export class Server<TUserConfig extends UserConfig = UserConfig, TContext = unknown> {
3✔
104
    public readonly session: Session;
105
    public readonly mcpServer: McpServer;
106
    private readonly telemetry: Telemetry;
107
    public readonly userConfig: TUserConfig;
108
    public readonly elicitation: Elicitation;
109
    private readonly toolConstructors: AnyToolClass[];
110
    public readonly tools: AnyToolBase[] = [];
3✔
111
    public readonly connectionErrorHandler: ConnectionErrorHandler;
112
    public readonly uiRegistry?: UIRegistry;
113
    public readonly toolContext?: TContext;
114

115
    private _mcpLogLevel: LogLevel = "debug";
3✔
116

117
    public get mcpLogLevel(): LogLevel {
3✔
118
        return this._mcpLogLevel;
3✔
119
    }
3✔
120

121
    private readonly startTime: number;
122
    private readonly subscriptions = new Set<string>();
3✔
123

124
    constructor({
3✔
125
        session,
183✔
126
        mcpServer,
183✔
127
        userConfig,
183✔
128
        telemetry,
183✔
129
        connectionErrorHandler,
183✔
130
        elicitation,
183✔
131
        tools,
183✔
132
        uiRegistry,
183✔
133
        toolContext,
183✔
134
    }: ServerOptions<TUserConfig, TContext>) {
183✔
135
        this.startTime = Date.now();
183✔
136
        this.session = session;
183✔
137
        this.telemetry = telemetry;
183✔
138
        this.mcpServer = mcpServer;
183✔
139
        this.userConfig = userConfig;
183✔
140
        this.elicitation = elicitation;
183✔
141
        this.connectionErrorHandler = connectionErrorHandler;
183✔
142
        this.toolConstructors = tools ?? AllTools;
183✔
143
        this.uiRegistry = uiRegistry;
183✔
144
        this.toolContext = toolContext;
183✔
145
    }
183✔
146

147
    async connect(transport: Transport): Promise<void> {
3✔
148
        await this.validateConfig();
172✔
149
        // Register resources after the server is initialized so they can listen to events like
150
        // connection events.
151
        this.registerResources();
172✔
152
        this.mcpServer.server.registerCapabilities({
172✔
153
            logging: {},
172✔
154
            resources: { listChanged: true, subscribe: true },
172✔
155
        });
172✔
156

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

160
        // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
161
        // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
162
        // the tool accepts any arguments, even if they're all optional.
163
        //
164
        // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/server/mcp.ts#L705
165
        // Since paramsSchema here is not undefined, the server will create a non-optional z.object from it.
166
        const existingHandler = (
172✔
167
            this.mcpServer.server["_requestHandlers"] as Map<
172✔
168
                string,
169
                (request: unknown, extra: unknown) => Promise<CallToolResult>
170
            >
171
        ).get(CallToolRequestSchema.shape.method.value);
172✔
172

173
        if (!existingHandler) {
172!
UNCOV
174
            throw new Error("No existing handler found for CallToolRequestSchema");
×
UNCOV
175
        }
✔
176

177
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
171✔
178
            if (!request.params.arguments) {
949!
179
                request.params.arguments = {};
1✔
180
            }
1✔
181

182
            return existingHandler(request, extra);
949✔
183
        });
171✔
184

185
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
171✔
186
            this.subscriptions.add(params.uri);
20✔
187
            this.session.logger.debug({
20✔
188
                id: LogId.serverInitialized,
20✔
189
                context: "resources",
20✔
190
                message: `Client subscribed to resource: ${params.uri}`,
20✔
191
            });
20✔
192
            return {};
20✔
193
        });
171✔
194

195
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
171✔
196
            this.subscriptions.delete(params.uri);
×
197
            this.session.logger.debug({
×
198
                id: LogId.serverInitialized,
×
199
                context: "resources",
×
UNCOV
200
                message: `Client unsubscribed from resource: ${params.uri}`,
×
UNCOV
201
            });
×
UNCOV
202
            return {};
×
203
        });
171✔
204

205
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
171✔
UNCOV
206
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
207
                throw new Error(`Invalid log level: ${params.level}`);
×
208
            }
×
209

UNCOV
210
            this._mcpLogLevel = params.level;
×
UNCOV
211
            return {};
×
212
        });
171✔
213

214
        this.mcpServer.server.oninitialized = (): void => {
171✔
215
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
151✔
216
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
217
            void this.connectToConfigConnectionString();
151✔
218
            this.session.logger.info({
151✔
219
                id: LogId.serverInitialized,
151✔
220
                context: "server",
151✔
221
                message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
151✔
222
            });
151✔
223

224
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
151✔
225
        };
151✔
226

227
        this.mcpServer.server.onclose = (): void => {
171✔
228
            const closeTime = Date.now();
167✔
229
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
167✔
230
        };
167✔
231

232
        this.mcpServer.server.onerror = (error: Error): void => {
171✔
UNCOV
233
            const closeTime = Date.now();
×
UNCOV
234
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
UNCOV
235
        };
×
236

237
        await this.mcpServer.connect(transport);
171✔
238
    }
172✔
239

240
    async close(): Promise<void> {
3✔
241
        await this.telemetry.close();
173✔
242
        await this.session.close();
173✔
243
        await this.mcpServer.close();
173✔
244
    }
173✔
245

246
    public sendResourceListChanged(): void {
3✔
247
        this.mcpServer.sendResourceListChanged();
1,025✔
248
    }
1,025✔
249

250
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
251
        return !!this.tools.filter((t) => t.category === name).length;
2✔
252
    }
2✔
253

254
    public sendResourceUpdated(uri: string): void {
3✔
255
        this.session.logger.info({
1,025✔
256
            id: LogId.resourceUpdateFailure,
1,025✔
257
            context: "resources",
1,025✔
258
            message: `Resource updated: ${uri}`,
1,025✔
259
        });
1,025✔
260

261
        if (this.subscriptions.has(uri)) {
1,025!
262
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
263
        }
20✔
264
    }
1,025✔
265

266
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
267
        const event: ServerEvent = {
318✔
268
            timestamp: new Date().toISOString(),
318✔
269
            source: "mdbmcp",
318✔
270
            properties: {
318✔
271
                result: "success",
318✔
272
                duration_ms: commandDuration,
318✔
273
                component: "server",
318✔
274
                category: "other",
318✔
275
                command: command,
318✔
276
            },
318✔
277
        };
318✔
278

279
        if (command === "start") {
318✔
280
            event.properties.startup_time_ms = commandDuration;
151✔
281
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
151!
282
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
151!
283
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
151!
284
            event.properties.previewFeatures = this.userConfig.previewFeatures;
151✔
285
            event.properties.embeddingProviderConfigured = !!this.userConfig.voyageApiKey;
151✔
286
        }
151✔
287
        if (command === "stop") {
318✔
288
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
167✔
289
            if (error) {
167!
UNCOV
290
                event.properties.result = "failure";
×
UNCOV
291
                event.properties.reason = error.message;
×
UNCOV
292
            }
×
293
        }
167✔
294

295
        this.telemetry.emitEvents([event]);
318✔
296
    }
318✔
297

298
    public registerTools(): void {
3✔
299
        for (const toolConstructor of this.toolConstructors) {
174✔
300
            const tool = new toolConstructor({
6,169✔
301
                name: toolConstructor.toolName,
6,169✔
302
                category: toolConstructor.category,
6,169✔
303
                operationType: toolConstructor.operationType,
6,169✔
304
                session: this.session,
6,169✔
305
                config: this.userConfig,
6,169✔
306
                telemetry: this.telemetry,
6,169✔
307
                elicitation: this.elicitation,
6,169✔
308
                uiRegistry: this.uiRegistry,
6,169✔
309
                context: this.toolContext,
6,169✔
310
            });
6,169✔
311
            if (tool.register(this)) {
6,169✔
312
                this.tools.push(tool);
4,388✔
313
            }
4,388✔
314
        }
6,169✔
315
    }
174✔
316

317
    public registerResources(): void {
3✔
318
        for (const resourceConstructor of Resources) {
172✔
319
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
516✔
320
            resource.register(this);
516✔
321
        }
516✔
322
    }
172✔
323

324
    private async validateConfig(): Promise<void> {
3✔
325
        // Validate connection string
326
        if (this.userConfig.connectionString) {
172!
327
            try {
8✔
328
                validateConnectionString(this.userConfig.connectionString, false);
8✔
329
            } catch (error) {
8!
330
                throw new Error(
×
331
                    "Connection string validation failed with error: " +
×
UNCOV
332
                        (error instanceof Error ? error.message : String(error))
×
UNCOV
333
                );
×
UNCOV
334
            }
×
335
        }
8✔
336

337
        // Validate API client credentials
338
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
172!
339
            try {
14✔
340
                if (!this.session.apiClient) {
14!
UNCOV
341
                    throw new Error("API client is not available.");
×
UNCOV
342
                }
×
343

344
                try {
14✔
345
                    const apiBaseUrl = new URL(this.userConfig.apiBaseUrl);
14✔
346
                    if (apiBaseUrl.protocol !== "https:") {
14!
347
                        // Log a warning, but don't error out. This is to allow for testing against local or non-HTTPS endpoints.
348
                        const message = `apiBaseUrl is configured to use ${apiBaseUrl.protocol}, which is not secure. It is strongly recommended to use HTTPS for secure communication.`;
1✔
349
                        this.session.logger.warning({
1✔
350
                            id: LogId.atlasApiBaseUrlInsecure,
1✔
351
                            context: "server",
1✔
352
                            message,
1✔
353
                        });
1✔
354
                    }
1✔
355
                } catch (error) {
14!
UNCOV
356
                    throw new Error(`Invalid apiBaseUrl: ${error instanceof Error ? error.message : String(error)}`);
×
UNCOV
357
                }
×
358

359
                await this.session.apiClient.validateAuthConfig();
14✔
360
            } catch (error) {
14!
361
                if (this.userConfig.connectionString === undefined) {
×
362
                    throw new Error(
×
UNCOV
363
                        `Failed to connect to MongoDB Atlas instance using the credentials from the config: ${error instanceof Error ? error.message : String(error)}`
×
364
                    );
×
365
                }
×
366

367
                this.session.logger.warning({
×
368
                    id: LogId.atlasCheckCredentials,
×
369
                    context: "server",
×
UNCOV
370
                    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.`,
×
UNCOV
371
                });
×
UNCOV
372
            }
×
373
        }
14✔
374
    }
172✔
375

376
    private async connectToConfigConnectionString(): Promise<void> {
3✔
377
        if (this.userConfig.connectionString) {
151!
378
            try {
8✔
379
                this.session.logger.info({
8✔
380
                    id: LogId.mongodbConnectTry,
8✔
381
                    context: "server",
8✔
382
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
8✔
383
                });
8✔
384
                await this.session.connectToConfiguredConnection();
8✔
385
            } catch (error) {
8✔
386
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
387
                this.session.logger.error({
2✔
388
                    id: LogId.mongodbConnectFailure,
2✔
389
                    context: "server",
2✔
390
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
2!
391
                });
2✔
392
            }
2✔
393
        }
8✔
394
    }
151✔
395
}
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