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

mongodb-js / mongodb-mcp-server / 20934316151

12 Jan 2026 08:42PM UTC coverage: 79.702% (-0.06%) from 79.762%
20934316151

push

github

web-flow
feat: Add configurable api / auth client support (#836)

1532 of 1994 branches covered (76.83%)

Branch coverage included in aggregate %.

196 of 202 new or added lines in 8 files covered. (97.03%)

25 existing lines in 1 file now uncovered.

6985 of 8692 relevant lines covered (80.36%)

87.88 hits per line

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

78.1
/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 { ToolBase, 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 { UIRegistry } from "./ui/registry/index.js";
3✔
25

26
export interface ServerOptions {
27
    session: Session;
28
    userConfig: UserConfig;
29
    mcpServer: McpServer;
30
    telemetry: Telemetry;
31
    elicitation: Elicitation;
32
    connectionErrorHandler: ConnectionErrorHandler;
33
    /**
34
     * Custom tool constructors to register with the server.
35
     * This will override any default tools. You can use both existing and custom tools by using the `mongodb-mcp-server/tools` export.
36
     *
37
     * ```ts
38
     * import { AllTools, ToolBase, type ToolCategory, type OperationType } from "mongodb-mcp-server/tools";
39
     * class CustomTool extends ToolBase {
40
     *     override name = "custom_tool";
41
     *     static category: ToolCategory = "mongodb";
42
     *     static operationType: OperationType = "read";
43
     *     public description = "Custom tool description";
44
     *     public argsShape = {};
45
     *     protected async execute() {
46
     *         return { content: [{ type: "text", text: "Result" }] };
47
     *     }
48
     *     protected resolveTelemetryMetadata() {
49
     *         return {};
50
     *     }
51
     * }
52
     * const server = new Server({
53
     *     session: mySession,
54
     *     userConfig: myUserConfig,
55
     *     mcpServer: myMcpServer,
56
     *     telemetry: myTelemetry,
57
     *     elicitation: myElicitation,
58
     *     connectionErrorHandler: myConnectionErrorHandler,
59
     *     tools: [...AllTools, CustomTool],
60
     * });
61
     * ```
62
     */
63
    tools?: ToolClass[];
64
    /**
65
     * Custom UIs for tools. Function that returns HTML strings for tool names.
66
     * Use this to add UIs to tools or replace the default bundled UIs.
67
     * The function is called lazily when a UI is requested, allowing you to
68
     * defer loading large HTML files until needed.
69
     *
70
     * ```ts
71
     * import { readFileSync } from 'fs';
72
     * const server = new Server({
73
     *     // ... other options
74
     *     customUIs: (toolName) => {
75
     *         if (toolName === 'list-databases') {
76
     *             return readFileSync('./my-custom-ui.html', 'utf-8');
77
     *         }
78
     *         return null;
79
     *     }
80
     * });
81
     * ```
82
     */
83
    customUIs?: (toolName: string) => string | null | Promise<string | null>;
84
}
85

86
export class Server {
3✔
87
    public readonly session: Session;
88
    public readonly mcpServer: McpServer;
89
    private readonly telemetry: Telemetry;
90
    public readonly userConfig: UserConfig;
91
    public readonly elicitation: Elicitation;
92
    private readonly toolConstructors: ToolClass[];
93
    public readonly tools: ToolBase[] = [];
3✔
94
    public readonly connectionErrorHandler: ConnectionErrorHandler;
95
    public readonly uiRegistry: UIRegistry;
96

97
    private _mcpLogLevel: LogLevel = "debug";
3✔
98

99
    public get mcpLogLevel(): LogLevel {
3✔
100
        return this._mcpLogLevel;
3✔
101
    }
3✔
102

103
    private readonly startTime: number;
104
    private readonly subscriptions = new Set<string>();
3✔
105

106
    constructor({
3✔
107
        session,
133✔
108
        mcpServer,
133✔
109
        userConfig,
133✔
110
        telemetry,
133✔
111
        connectionErrorHandler,
133✔
112
        elicitation,
133✔
113
        tools,
133✔
114
        customUIs,
133✔
115
    }: ServerOptions) {
133✔
116
        this.startTime = Date.now();
133✔
117
        this.session = session;
133✔
118
        this.telemetry = telemetry;
133✔
119
        this.mcpServer = mcpServer;
133✔
120
        this.userConfig = userConfig;
133✔
121
        this.elicitation = elicitation;
133✔
122
        this.connectionErrorHandler = connectionErrorHandler;
133✔
123
        this.toolConstructors = tools ?? AllTools;
133✔
124
        this.uiRegistry = new UIRegistry({ customUIs });
133✔
125
    }
133✔
126

127
    async connect(transport: Transport): Promise<void> {
3✔
128
        await this.validateConfig();
129✔
129
        // Register resources after the server is initialized so they can listen to events like
130
        // connection events.
131
        this.registerResources();
129✔
132
        this.mcpServer.server.registerCapabilities({
129✔
133
            logging: {},
129✔
134
            resources: { listChanged: true, subscribe: true },
129✔
135
        });
129✔
136

137
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
138
        this.registerTools();
129✔
139

140
        // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
141
        // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
142
        // the tool accepts any arguments, even if they're all optional.
143
        //
144
        // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/server/mcp.ts#L705
145
        // Since paramsSchema here is not undefined, the server will create a non-optional z.object from it.
146
        const existingHandler = (
129✔
147
            this.mcpServer.server["_requestHandlers"] as Map<
129✔
148
                string,
149
                (request: unknown, extra: unknown) => Promise<CallToolResult>
150
            >
151
        ).get(CallToolRequestSchema.shape.method.value);
129✔
152

153
        if (!existingHandler) {
129!
154
            throw new Error("No existing handler found for CallToolRequestSchema");
×
155
        }
✔
156

157
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
128✔
158
            if (!request.params.arguments) {
751!
159
                request.params.arguments = {};
1✔
160
            }
1✔
161

162
            return existingHandler(request, extra);
751✔
163
        });
128✔
164

165
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
128✔
166
            this.subscriptions.add(params.uri);
20✔
167
            this.session.logger.debug({
20✔
168
                id: LogId.serverInitialized,
20✔
169
                context: "resources",
20✔
170
                message: `Client subscribed to resource: ${params.uri}`,
20✔
171
            });
20✔
172
            return {};
20✔
173
        });
128✔
174

175
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
128✔
176
            this.subscriptions.delete(params.uri);
×
177
            this.session.logger.debug({
×
178
                id: LogId.serverInitialized,
×
179
                context: "resources",
×
180
                message: `Client unsubscribed from resource: ${params.uri}`,
×
181
            });
×
182
            return {};
×
183
        });
128✔
184

185
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
128✔
186
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
187
                throw new Error(`Invalid log level: ${params.level}`);
×
188
            }
×
189

190
            this._mcpLogLevel = params.level;
×
191
            return {};
×
192
        });
128✔
193

194
        this.mcpServer.server.oninitialized = (): void => {
128✔
195
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
122✔
196
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
197
            void this.connectToConfigConnectionString();
122✔
198
            this.session.logger.info({
122✔
199
                id: LogId.serverInitialized,
122✔
200
                context: "server",
122✔
201
                message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
122✔
202
            });
122✔
203

204
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
122✔
205
        };
122✔
206

207
        this.mcpServer.server.onclose = (): void => {
128✔
208
            const closeTime = Date.now();
124✔
209
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
124✔
210
        };
124✔
211

212
        this.mcpServer.server.onerror = (error: Error): void => {
128✔
213
            const closeTime = Date.now();
×
214
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
215
        };
×
216

217
        await this.mcpServer.connect(transport);
128✔
218
    }
129✔
219

220
    async close(): Promise<void> {
3✔
221
        await this.telemetry.close();
123✔
222
        await this.session.close();
121✔
223
        await this.mcpServer.close();
121✔
224
    }
123✔
225

226
    public sendResourceListChanged(): void {
3✔
227
        this.mcpServer.sendResourceListChanged();
806✔
228
    }
806✔
229

230
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
231
        return !!this.tools.filter((t) => t.category === name).length;
2✔
232
    }
2✔
233

234
    public sendResourceUpdated(uri: string): void {
3✔
235
        this.session.logger.info({
806✔
236
            id: LogId.resourceUpdateFailure,
806✔
237
            context: "resources",
806✔
238
            message: `Resource updated: ${uri}`,
806✔
239
        });
806✔
240

241
        if (this.subscriptions.has(uri)) {
806!
242
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
243
        }
20✔
244
    }
806✔
245

246
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
247
        const event: ServerEvent = {
246✔
248
            timestamp: new Date().toISOString(),
246✔
249
            source: "mdbmcp",
246✔
250
            properties: {
246✔
251
                result: "success",
246✔
252
                duration_ms: commandDuration,
246✔
253
                component: "server",
246✔
254
                category: "other",
246✔
255
                command: command,
246✔
256
            },
246✔
257
        };
246✔
258

259
        if (command === "start") {
246✔
260
            event.properties.startup_time_ms = commandDuration;
122✔
261
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
122!
262
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
122!
263
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
122!
264
            event.properties.previewFeatures = this.userConfig.previewFeatures;
122✔
265
            event.properties.embeddingProviderConfigured = !!this.userConfig.voyageApiKey;
122✔
266
        }
122✔
267
        if (command === "stop") {
246✔
268
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
124✔
269
            if (error) {
124!
270
                event.properties.result = "failure";
×
271
                event.properties.reason = error.message;
×
272
            }
×
273
        }
124✔
274

275
        this.telemetry.emitEvents([event]);
246✔
276
    }
246✔
277

278
    public registerTools(): void {
3✔
279
        for (const toolConstructor of this.toolConstructors) {
129✔
280
            const tool = new toolConstructor({
4,719✔
281
                category: toolConstructor.category,
4,719✔
282
                operationType: toolConstructor.operationType,
4,719✔
283
                session: this.session,
4,719✔
284
                config: this.userConfig,
4,719✔
285
                telemetry: this.telemetry,
4,719✔
286
                elicitation: this.elicitation,
4,719✔
287
                uiRegistry: this.uiRegistry,
4,719✔
288
            });
4,719✔
289
            if (tool.register(this)) {
4,719✔
290
                this.tools.push(tool);
3,406✔
291
            }
3,406✔
292
        }
4,719✔
293
    }
129✔
294

295
    public registerResources(): void {
3✔
296
        for (const resourceConstructor of Resources) {
129✔
297
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
387✔
298
            resource.register(this);
387✔
299
        }
387✔
300
    }
129✔
301

302
    private async validateConfig(): Promise<void> {
3✔
303
        // Validate connection string
304
        if (this.userConfig.connectionString) {
129!
305
            try {
8✔
306
                validateConnectionString(this.userConfig.connectionString, false);
8✔
307
            } catch (error) {
8!
308
                console.error("Connection string validation failed with error: ", error);
×
309
                throw new Error(
×
310
                    "Connection string validation failed with error: " +
×
311
                        (error instanceof Error ? error.message : String(error))
×
312
                );
×
313
            }
×
314
        }
8✔
315

316
        // Validate API client credentials
317
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
129!
318
            try {
13✔
319
                if (!this.session.apiClient) {
13!
320
                    throw new Error("API client is not available.");
×
321
                }
×
322
                if (!this.userConfig.apiBaseUrl.startsWith("https://")) {
13!
323
                    const message =
×
324
                        "Failed to validate MongoDB Atlas the credentials from config: apiBaseUrl must start with https://";
×
325
                    console.error(message);
×
326
                    throw new Error(message);
×
327
                }
×
328

329
                await this.session.apiClient.validateAuthConfig();
13✔
330
            } catch (error) {
13!
331
                if (this.userConfig.connectionString === undefined) {
×
332
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
333

334
                    throw new Error(
×
335
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
336
                    );
×
337
                }
×
338
                console.error(
×
NEW
339
                    "Failed to validate MongoDB Atlas credentials from the config, but validated the connection string."
×
340
                );
×
341
            }
×
342
        }
13✔
343
    }
129✔
344

345
    private async connectToConfigConnectionString(): Promise<void> {
3✔
346
        if (this.userConfig.connectionString) {
122!
347
            try {
8✔
348
                this.session.logger.info({
8✔
349
                    id: LogId.mongodbConnectTry,
8✔
350
                    context: "server",
8✔
351
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
8✔
352
                });
8✔
353
                await this.session.connectToConfiguredConnection();
8✔
354
            } catch (error) {
8✔
355
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
356
                this.session.logger.error({
2✔
357
                    id: LogId.mongodbConnectFailure,
2✔
358
                    context: "server",
2✔
359
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
2!
360
                });
2✔
361
            }
2✔
362
        }
8✔
363
    }
122✔
364
}
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