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

mongodb-js / mongodb-mcp-server / 19827658831

01 Dec 2025 03:18PM UTC coverage: 80.423% (-0.05%) from 80.472%
19827658831

Pull #767

github

web-flow
Merge 2ea82d529 into a585a82e0
Pull Request #767: chore: add developer guide and code examples to extend MCP server via library exports MCP-299

1470 of 1905 branches covered (77.17%)

Branch coverage included in aggregate %.

118 of 122 new or added lines in 47 files covered. (96.72%)

23 existing lines in 4 files now uncovered.

6635 of 8173 relevant lines covered (81.18%)

75.07 hits per line

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

79.38
/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

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
     *     override category: ToolCategory = "mongodb";
42
     *     static operationType: OperationType = "read";
43
     *     protected description = "Custom tool description";
44
     *     protected 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: [...Object.values(AllTools), CustomTool],
60
     * });
61
     * ```
62
     */
63
    tools?: ToolClass[];
64
}
65

66
export class Server {
3✔
67
    public readonly session: Session;
68
    public readonly mcpServer: McpServer;
69
    private readonly telemetry: Telemetry;
70
    public readonly userConfig: UserConfig;
71
    public readonly elicitation: Elicitation;
72
    private readonly toolConstructors: ToolClass[];
73
    public readonly tools: ToolBase[] = [];
3✔
74
    public readonly connectionErrorHandler: ConnectionErrorHandler;
75

76
    private _mcpLogLevel: LogLevel = "debug";
3✔
77

78
    public get mcpLogLevel(): LogLevel {
3✔
79
        return this._mcpLogLevel;
3✔
80
    }
3✔
81

82
    private readonly startTime: number;
83
    private readonly subscriptions = new Set<string>();
3✔
84

85
    constructor({
3✔
86
        session,
123✔
87
        mcpServer,
123✔
88
        userConfig,
123✔
89
        telemetry,
123✔
90
        connectionErrorHandler,
123✔
91
        elicitation,
123✔
92
        tools,
123✔
93
    }: ServerOptions) {
123✔
94
        this.startTime = Date.now();
123✔
95
        this.session = session;
123✔
96
        this.telemetry = telemetry;
123✔
97
        this.mcpServer = mcpServer;
123✔
98
        this.userConfig = userConfig;
123✔
99
        this.elicitation = elicitation;
123✔
100
        this.connectionErrorHandler = connectionErrorHandler;
123✔
101
        this.toolConstructors = tools ?? Object.values(AllTools);
123✔
102
    }
123✔
103

104
    async connect(transport: Transport): Promise<void> {
3✔
105
        await this.validateConfig();
119✔
106
        // Register resources after the server is initialized so they can listen to events like
107
        // connection events.
108
        this.registerResources();
119✔
109
        this.mcpServer.server.registerCapabilities({
119✔
110
            logging: {},
119✔
111
            resources: { listChanged: true, subscribe: true },
119✔
112
        });
119✔
113

114
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
115
        this.registerTools();
119✔
116

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

130
        assert(existingHandler, "No existing handler found for CallToolRequestSchema");
119✔
131

132
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
119✔
133
            if (!request.params.arguments) {
710!
134
                request.params.arguments = {};
1✔
135
            }
1✔
136

137
            return existingHandler(request, extra);
710✔
138
        });
119✔
139

140
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
119✔
141
            this.subscriptions.add(params.uri);
20✔
142
            this.session.logger.debug({
20✔
143
                id: LogId.serverInitialized,
20✔
144
                context: "resources",
20✔
145
                message: `Client subscribed to resource: ${params.uri}`,
20✔
146
            });
20✔
147
            return {};
20✔
148
        });
119✔
149

150
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
119✔
151
            this.subscriptions.delete(params.uri);
×
152
            this.session.logger.debug({
×
153
                id: LogId.serverInitialized,
×
154
                context: "resources",
×
155
                message: `Client unsubscribed from resource: ${params.uri}`,
×
156
            });
×
157
            return {};
×
158
        });
119✔
159

160
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
119✔
161
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
162
                throw new Error(`Invalid log level: ${params.level}`);
×
163
            }
×
164

165
            this._mcpLogLevel = params.level;
×
166
            return {};
×
167
        });
119✔
168

169
        this.mcpServer.server.oninitialized = (): void => {
119✔
170
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
117✔
171
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
172
            void this.connectToConfigConnectionString();
117✔
173
            this.session.logger.info({
117✔
174
                id: LogId.serverInitialized,
117✔
175
                context: "server",
117✔
176
                message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
117✔
177
            });
117✔
178

179
            this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
117✔
180
        };
117✔
181

182
        this.mcpServer.server.onclose = (): void => {
119✔
183
            const closeTime = Date.now();
117✔
184
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
117✔
185
        };
117✔
186

187
        this.mcpServer.server.onerror = (error: Error): void => {
119✔
188
            const closeTime = Date.now();
×
189
            this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
×
190
        };
×
191

192
        await this.mcpServer.connect(transport);
119✔
193
    }
119✔
194

195
    async close(): Promise<void> {
3✔
196
        await this.telemetry.close();
117✔
197
        await this.session.close();
112✔
198
        await this.mcpServer.close();
112✔
199
    }
117✔
200

201
    public sendResourceListChanged(): void {
3✔
202
        this.mcpServer.sendResourceListChanged();
764✔
203
    }
764✔
204

205
    public isToolCategoryAvailable(name: ToolCategory): boolean {
3✔
206
        return !!this.tools.filter((t) => t.category === name).length;
2✔
207
    }
2✔
208

209
    public sendResourceUpdated(uri: string): void {
3✔
210
        this.session.logger.info({
764✔
211
            id: LogId.resourceUpdateFailure,
764✔
212
            context: "resources",
764✔
213
            message: `Resource updated: ${uri}`,
764✔
214
        });
764✔
215

216
        if (this.subscriptions.has(uri)) {
764!
217
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
218
        }
20✔
219
    }
764✔
220

221
    private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
3✔
222
        const event: ServerEvent = {
234✔
223
            timestamp: new Date().toISOString(),
234✔
224
            source: "mdbmcp",
234✔
225
            properties: {
234✔
226
                result: "success",
234✔
227
                duration_ms: commandDuration,
234✔
228
                component: "server",
234✔
229
                category: "other",
234✔
230
                command: command,
234✔
231
            },
234✔
232
        };
234✔
233

234
        if (command === "start") {
234✔
235
            event.properties.startup_time_ms = commandDuration;
117✔
236
            event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
117!
237
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
117!
238
            event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
117!
239
        }
117✔
240
        if (command === "stop") {
234✔
241
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
117✔
242
            if (error) {
117!
243
                event.properties.result = "failure";
×
244
                event.properties.reason = error.message;
×
245
            }
×
246
        }
117✔
247

248
        this.telemetry.emitEvents([event]);
234✔
249
    }
234✔
250

251
    private registerTools(): void {
3✔
252
        for (const toolConstructor of this.toolConstructors) {
119✔
253
            const tool = new toolConstructor({
4,396✔
254
                operationType: toolConstructor.operationType,
4,396✔
255
                session: this.session,
4,396✔
256
                config: this.userConfig,
4,396✔
257
                telemetry: this.telemetry,
4,396✔
258
                elicitation: this.elicitation,
4,396✔
259
            });
4,396✔
260
            if (tool.register(this)) {
4,396✔
261
                this.tools.push(tool);
3,188✔
262
            }
3,188✔
263
        }
4,396✔
264
    }
119✔
265

266
    private registerResources(): void {
3✔
267
        for (const resourceConstructor of Resources) {
119✔
268
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
357✔
269
            resource.register(this);
357✔
270
        }
357✔
271
    }
119✔
272

273
    private async validateConfig(): Promise<void> {
3✔
274
        // Validate connection string
275
        if (this.userConfig.connectionString) {
119!
276
            try {
8✔
277
                validateConnectionString(this.userConfig.connectionString, false);
8✔
278
            } catch (error) {
8!
UNCOV
279
                console.error("Connection string validation failed with error: ", error);
×
280
                throw new Error(
×
281
                    "Connection string validation failed with error: " +
×
282
                        (error instanceof Error ? error.message : String(error))
×
283
                );
×
284
            }
×
285
        }
8✔
286

287
        // Validate API client credentials
288
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
119!
289
            try {
13✔
290
                if (!this.userConfig.apiBaseUrl.startsWith("https://")) {
13!
UNCOV
291
                    const message =
×
292
                        "Failed to validate MongoDB Atlas the credentials from config: apiBaseUrl must start with https://";
×
293
                    console.error(message);
×
294
                    throw new Error(message);
×
295
                }
×
296

297
                await this.session.apiClient.validateAccessToken();
13✔
298
            } catch (error) {
13!
UNCOV
299
                if (this.userConfig.connectionString === undefined) {
×
300
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
301

UNCOV
302
                    throw new Error(
×
303
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
304
                    );
×
305
                }
×
306
                console.error(
×
307
                    "Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
×
308
                );
×
309
            }
×
310
        }
13✔
311
    }
119✔
312

313
    private async connectToConfigConnectionString(): Promise<void> {
3✔
314
        if (this.userConfig.connectionString) {
117!
315
            try {
8✔
316
                this.session.logger.info({
8✔
317
                    id: LogId.mongodbConnectTry,
8✔
318
                    context: "server",
8✔
319
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
8✔
320
                });
8✔
321
                await this.session.connectToConfiguredConnection();
8✔
322
            } catch (error) {
8✔
323
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
324
                this.session.logger.error({
2✔
325
                    id: LogId.mongodbConnectFailure,
2✔
326
                    context: "server",
2✔
327
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
2!
328
                });
2✔
329
            }
2✔
330
        }
8✔
331
    }
117✔
332
}
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