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

mongodb-js / mongodb-mcp-server / 20237133797

15 Dec 2025 03:10PM UTC coverage: 79.68% (-0.4%) from 80.089%
20237133797

Pull #783

github

web-flow
Merge 8aebebbbf into cced0c7fe
Pull Request #783: feat: implement MCP-UI support

1471 of 1927 branches covered (76.34%)

Branch coverage included in aggregate %.

57 of 238 new or added lines in 13 files covered. (23.95%)

26 existing lines in 1 file now uncovered.

6744 of 8383 relevant lines covered (80.45%)

84.67 hits per line

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

80.0
/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 assert from "assert";
3✔
19
import type { ToolBase, ToolCategory, ToolClass } from "./tools/tool.js";
20
import { validateConnectionString } from "./helpers/connectionOptions.js";
3✔
21
import { packageInfo } from "./common/packageInfo.js";
3✔
22
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
23
import type { Elicitation } from "./elicitation.js";
24
import { AllTools } from "./tools/index.js";
3✔
25
import { UIRegistry } from "./ui/registry/index.js";
3✔
26

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

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

93
    private _mcpLogLevel: LogLevel = "debug";
3✔
94

95
    public get mcpLogLevel(): LogLevel {
3✔
96
        return this._mcpLogLevel;
3✔
97
    }
3✔
98

99
    private readonly startTime: number;
100
    private readonly subscriptions = new Set<string>();
3✔
101

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

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

133
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
134
        this.registerTools();
122✔
135

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

149
        assert(existingHandler, "No existing handler found for CallToolRequestSchema");
122✔
150

151
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
122✔
152
            if (!request.params.arguments) {
736!
153
                request.params.arguments = {};
1✔
154
            }
1✔
155

156
            return existingHandler(request, extra);
736✔
157
        });
122✔
158

159
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
122✔
160
            this.subscriptions.add(params.uri);
20✔
161
            this.session.logger.debug({
20✔
162
                id: LogId.serverInitialized,
20✔
163
                context: "resources",
20✔
164
                message: `Client subscribed to resource: ${params.uri}`,
20✔
165
            });
20✔
166
            return {};
20✔
167
        });
122✔
168

169
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
122✔
UNCOV
170
            this.subscriptions.delete(params.uri);
×
UNCOV
171
            this.session.logger.debug({
×
UNCOV
172
                id: LogId.serverInitialized,
×
UNCOV
173
                context: "resources",
×
UNCOV
174
                message: `Client unsubscribed from resource: ${params.uri}`,
×
175
            });
×
176
            return {};
×
177
        });
122✔
178

179
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
122✔
180
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
181
                throw new Error(`Invalid log level: ${params.level}`);
×
UNCOV
182
            }
×
183

UNCOV
184
            this._mcpLogLevel = params.level;
×
185
            return {};
×
186
        });
122✔
187

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

198
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
118✔
199
        };
118✔
200

201
        this.mcpServer.server.onclose = (): void => {
122✔
202
            const closeTime = Date.now();
119✔
203
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
119✔
204
        };
119✔
205

206
        this.mcpServer.server.onerror = (error: Error): void => {
122✔
UNCOV
207
            const closeTime = Date.now();
×
UNCOV
208
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
UNCOV
209
        };
×
210

211
        await this.mcpServer.connect(transport);
122✔
212
    }
122✔
213

214
    async close(): Promise<void> {
3✔
215
        await this.telemetry.close();
118✔
216
        await this.session.close();
113✔
217
        await this.mcpServer.close();
113✔
218
    }
118✔
219

220
    public sendResourceListChanged(): void {
3✔
221
        this.mcpServer.sendResourceListChanged();
806✔
222
    }
806✔
223

224
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
225
        return !!this.tools.filter((t) => t.category === name).length;
2✔
226
    }
2✔
227

228
    public sendResourceUpdated(uri: string): void {
3✔
229
        this.session.logger.info({
806✔
230
            id: LogId.resourceUpdateFailure,
806✔
231
            context: "resources",
806✔
232
            message: `Resource updated: ${uri}`,
806✔
233
        });
806✔
234

235
        if (this.subscriptions.has(uri)) {
806!
236
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
237
        }
20✔
238
    }
806✔
239

240
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
241
        const event: ServerEvent = {
237✔
242
            timestamp: new Date().toISOString(),
237✔
243
            source: "mdbmcp",
237✔
244
            properties: {
237✔
245
                result: "success",
237✔
246
                duration_ms: commandDuration,
237✔
247
                component: "server",
237✔
248
                category: "other",
237✔
249
                command: command,
237✔
250
            },
237✔
251
        };
237✔
252

253
        if (command === "start") {
237✔
254
            event.properties.startup_time_ms = commandDuration;
118✔
255
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
118!
256
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
118!
257
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
118!
258
            event.properties.previewFeatures = this.userConfig.previewFeatures;
118✔
259
            event.properties.embeddingProviderConfigured = !!this.userConfig.voyageApiKey;
118✔
260
        }
118✔
261
        if (command === "stop") {
237✔
262
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
119✔
263
            if (error) {
119!
UNCOV
264
                event.properties.result = "failure";
×
UNCOV
265
                event.properties.reason = error.message;
×
UNCOV
266
            }
×
267
        }
119✔
268

269
        this.telemetry.emitEvents([event]);
237✔
270
    }
237✔
271

272
    private registerTools(): void {
3✔
273
        for (const toolConstructor of this.toolConstructors) {
122✔
274
            const tool = new toolConstructor({
4,439✔
275
                category: toolConstructor.category,
4,439✔
276
                operationType: toolConstructor.operationType,
4,439✔
277
                session: this.session,
4,439✔
278
                config: this.userConfig,
4,439✔
279
                telemetry: this.telemetry,
4,439✔
280
                elicitation: this.elicitation,
4,439✔
281
                uiRegistry: this.uiRegistry,
4,439✔
282
            });
4,439✔
283
            if (tool.register(this)) {
4,439✔
284
                this.tools.push(tool);
3,217✔
285
            }
3,217✔
286
        }
4,439✔
287
    }
122✔
288

289
    private registerResources(): void {
3✔
290
        for (const resourceConstructor of Resources) {
122✔
291
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
366✔
292
            resource.register(this);
366✔
293
        }
366✔
294
    }
122✔
295

296
    private async validateConfig(): Promise<void> {
3✔
297
        // Validate connection string
298
        if (this.userConfig.connectionString) {
122!
299
            try {
8✔
300
                validateConnectionString(this.userConfig.connectionString, false);
8✔
301
            } catch (error) {
8!
UNCOV
302
                console.error("Connection string validation failed with error: ", error);
×
UNCOV
303
                throw new Error(
×
UNCOV
304
                    "Connection string validation failed with error: " +
×
UNCOV
305
                        (error instanceof Error ? error.message : String(error))
×
UNCOV
306
                );
×
307
            }
×
308
        }
8✔
309

310
        // Validate API client credentials
311
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
122!
312
            try {
13✔
313
                if (!this.userConfig.apiBaseUrl.startsWith("https://")) {
13!
UNCOV
314
                    const message =
×
UNCOV
315
                        "Failed to validate MongoDB Atlas the credentials from config: apiBaseUrl must start with https://";
×
UNCOV
316
                    console.error(message);
×
UNCOV
317
                    throw new Error(message);
×
UNCOV
318
                }
×
319

320
                await this.session.apiClient.validateAccessToken();
13✔
321
            } catch (error) {
13!
322
                if (this.userConfig.connectionString === undefined) {
×
323
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
324

UNCOV
325
                    throw new Error(
×
UNCOV
326
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
327
                    );
×
328
                }
×
UNCOV
329
                console.error(
×
330
                    "Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
×
331
                );
×
332
            }
×
333
        }
13✔
334
    }
122✔
335

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