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

mongodb-js / mongodb-mcp-server / 24529519282

16 Apr 2026 07:23PM UTC coverage: 81.766% (-0.02%) from 81.781%
24529519282

push

github

web-flow
chore(deps-dev): bump the ai-sdk group across 1 directory with 5 updates (#1058)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

1590 of 2169 branches covered (73.31%)

Branch coverage included in aggregate %.

3078 of 3540 relevant lines covered (86.95%)

160.3 hits per line

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

79.35
/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";
5
import type { LogLevel } from "./common/logging/index.js";
6
import { LogId, MCP_LOG_LEVELS } from "./common/logging/index.js";
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 {
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
export type { ToolCategory } from "./tools/tool.js";
20
import { validateConnectionString } from "./helpers/connectionOptions.js";
21
import { packageInfo } from "./common/packageInfo.js";
22
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
23
import type { Elicitation } from "./elicitation.js";
24
import { AllTools } from "./tools/index.js";
25
import type { UIRegistry } from "./ui/registry/index.js";
26
import type { Metrics, DefaultMetrics } from "./common/metrics/index.js";
27

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

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

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

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

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

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

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

159
        this._mcpLogLevel = userConfig.mcpClientLogLevel;
211✔
160
        this.mcpLogLevelFloor = this._mcpLogLevel;
211✔
161
    }
162

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

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

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

189
        if (!existingHandler) {
198!
190
            throw new Error("No existing handler found for CallToolRequestSchema");
×
191
        }
192

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

198
            return existingHandler(request, extra);
952✔
199
        });
200

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

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

221
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
197✔
222
            if (!MCP_LOG_LEVELS.includes(params.level)) {
4!
223
                throw new Error(`Invalid log level: ${params.level}`);
×
224
            }
225

226
            const requestedIdx = MCP_LOG_LEVELS.indexOf(params.level);
4✔
227
            const floorIdx = MCP_LOG_LEVELS.indexOf(this.mcpLogLevelFloor);
4✔
228
            this._mcpLogLevel = requestedIdx >= floorIdx ? params.level : this.mcpLogLevelFloor;
4✔
229
            return {};
4✔
230
        });
231

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

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

245
        this.mcpServer.server.onclose = (): void => {
197✔
246
            const closeTime = Date.now();
189✔
247
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
189✔
248
        };
249

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

255
        await this.mcpServer.connect(transport);
197✔
256
    }
257

258
    async close(): Promise<void> {
259
        await this.telemetry.close();
195✔
260
        await this.session.close();
195✔
261
        await this.mcpServer.close();
192✔
262
    }
263

264
    public sendResourceListChanged(): void {
265
        this.mcpServer.sendResourceListChanged();
1,018✔
266
    }
267

268
    public isToolCategoryAvailable(name: ToolCategory): boolean {
269
        return !!this.tools.filter((t) => t.category === name).length;
58✔
270
    }
271

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

279
        if (this.subscriptions.has(uri)) {
1,018✔
280
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
281
        }
282
    }
283

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

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

312
        this.telemetry.emitEvents([event]);
350✔
313
    }
314

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

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

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

356
        // Validate API client credentials
357
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
198✔
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,
370
                            context: "server",
371
                            message,
372
                        });
373
                    }
374
                } catch (error) {
375
                    throw new Error(`Invalid apiBaseUrl: ${error instanceof Error ? error.message : String(error)}`, {
×
376
                        cause: error,
377
                    });
378
                }
379

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

389
                this.session.logger.warning({
×
390
                    id: LogId.atlasCheckCredentials,
391
                    context: "server",
392
                    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.`,
×
393
                });
394
            }
395
        }
396
    }
397

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