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

mongodb-js / mongodb-mcp-server / 17402691381

02 Sep 2025 11:55AM UTC coverage: 80.962% (+0.02%) from 80.945%
17402691381

Pull #503

github

web-flow
Merge 369ed330e into d471cdd0d
Pull Request #503: fix: start mcp even if connection fails - [MCP-140]

892 of 1187 branches covered (75.15%)

Branch coverage included in aggregate %.

8 of 13 new or added lines in 2 files covered. (61.54%)

1 existing line in 1 file now uncovered.

4543 of 5526 relevant lines covered (82.21%)

39.98 hits per line

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

78.3
/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 { AtlasTools } from "./tools/atlas/tools.js";
2✔
5
import { MongoDbTools } from "./tools/mongodb/tools.js";
2✔
6
import { Resources } from "./resources/resources.js";
2✔
7
import type { LogLevel } from "./common/logger.js";
8
import { LogId, McpLogger } from "./common/logger.js";
2✔
9
import type { Telemetry } from "./telemetry/telemetry.js";
10
import type { UserConfig } from "./common/config.js";
11
import { type ServerEvent } from "./telemetry/types.js";
12
import { type ServerCommand } from "./telemetry/types.js";
13
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
14
import {
2✔
15
    CallToolRequestSchema,
16
    SetLevelRequestSchema,
17
    SubscribeRequestSchema,
18
    UnsubscribeRequestSchema,
19
} from "@modelcontextprotocol/sdk/types.js";
20
import assert from "assert";
2✔
21
import type { ToolBase } from "./tools/tool.js";
22
import { validateConnectionString } from "./helpers/connectionOptions.js";
2✔
23
import { packageInfo } from "./common/packageInfo.js";
2✔
24

25
export interface ServerOptions {
26
    session: Session;
27
    userConfig: UserConfig;
28
    mcpServer: McpServer;
29
    telemetry: Telemetry;
30
}
31

32
export class Server {
2✔
33
    public readonly session: Session;
34
    public readonly mcpServer: McpServer;
35
    private readonly telemetry: Telemetry;
36
    public readonly userConfig: UserConfig;
37
    public readonly tools: ToolBase[] = [];
2✔
38

39
    private _mcpLogLevel: LogLevel = "debug";
2✔
40

41
    public get mcpLogLevel(): LogLevel {
2✔
42
        return this._mcpLogLevel;
2✔
43
    }
2✔
44

45
    private readonly startTime: number;
46
    private readonly subscriptions = new Set<string>();
2✔
47

48
    constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
2✔
49
        this.startTime = Date.now();
48✔
50
        this.session = session;
48✔
51
        this.telemetry = telemetry;
48✔
52
        this.mcpServer = mcpServer;
48✔
53
        this.userConfig = userConfig;
48✔
54
    }
48✔
55

56
    async connect(transport: Transport): Promise<void> {
2✔
57
        // Resources are now reactive, so we register them ASAP so they can listen to events like
58
        // connection events.
59
        this.registerResources();
48✔
60
        await this.validateConfig();
48✔
61

62
        this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } });
48✔
63

64
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
65
        this.registerTools();
48✔
66

67
        // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
68
        // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
69
        // the tool accepts any arguments, even if they're all optional.
70
        //
71
        // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/131776764536b5fdca642df51230a3746fb4ade0/src/server/mcp.ts#L705
72
        // Since paramsSchema here is not undefined, the server will create a non-optional z.object from it.
73
        const existingHandler = (
48✔
74
            this.mcpServer.server["_requestHandlers"] as Map<
48✔
75
                string,
76
                (request: unknown, extra: unknown) => Promise<CallToolResult>
77
            >
78
        ).get(CallToolRequestSchema.shape.method.value);
48✔
79

80
        assert(existingHandler, "No existing handler found for CallToolRequestSchema");
48✔
81

82
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
48✔
83
            if (!request.params.arguments) {
456✔
84
                request.params.arguments = {};
1✔
85
            }
1✔
86

87
            return existingHandler(request, extra);
456✔
88
        });
48✔
89

90
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
48✔
91
            this.subscriptions.add(params.uri);
20✔
92
            this.session.logger.debug({
20✔
93
                id: LogId.serverInitialized,
20✔
94
                context: "resources",
20✔
95
                message: `Client subscribed to resource: ${params.uri}`,
20✔
96
            });
20✔
97
            return {};
20✔
98
        });
48✔
99

100
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
48✔
101
            this.subscriptions.delete(params.uri);
×
102
            this.session.logger.debug({
×
103
                id: LogId.serverInitialized,
×
104
                context: "resources",
×
105
                message: `Client unsubscribed from resource: ${params.uri}`,
×
106
            });
×
107
            return {};
×
108
        });
48✔
109

110
        this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
48✔
111
            if (!McpLogger.LOG_LEVELS.includes(params.level)) {
×
112
                throw new Error(`Invalid log level: ${params.level}`);
×
113
            }
×
114

115
            this._mcpLogLevel = params.level;
×
116
            return {};
×
117
        });
48✔
118

119
        this.mcpServer.server.oninitialized = (): void => {
48✔
120
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
48✔
121
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
122
            void this.connectToConfigConnectionString();
48✔
123
            this.session.logger.info({
48✔
124
                id: LogId.serverInitialized,
48✔
125
                context: "server",
48✔
126
                message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
48✔
127
            });
48✔
128

129
            this.emitServerEvent("start", Date.now() - this.startTime);
48✔
130
        };
48✔
131

132
        this.mcpServer.server.onclose = (): void => {
48✔
133
            const closeTime = Date.now();
48✔
134
            this.emitServerEvent("stop", Date.now() - closeTime);
48✔
135
        };
48✔
136

137
        this.mcpServer.server.onerror = (error: Error): void => {
48✔
138
            const closeTime = Date.now();
×
139
            this.emitServerEvent("stop", Date.now() - closeTime, error);
×
140
        };
×
141

142
        await this.mcpServer.connect(transport);
48✔
143
    }
48✔
144

145
    async close(): Promise<void> {
2✔
146
        await this.telemetry.close();
48✔
147
        await this.session.close();
48✔
148
        await this.mcpServer.close();
48✔
149
    }
48✔
150

151
    public sendResourceListChanged(): void {
2✔
152
        this.mcpServer.sendResourceListChanged();
513✔
153
    }
513✔
154

155
    public sendResourceUpdated(uri: string): void {
2✔
156
        if (this.subscriptions.has(uri)) {
513!
157
            void this.mcpServer.server.sendResourceUpdated({ uri });
20✔
158
        }
20✔
159
    }
513✔
160

161
    /**
162
     * Emits a server event
163
     * @param command - The server command (e.g., "start", "stop", "register", "deregister")
164
     * @param additionalProperties - Additional properties specific to the event
165
     */
166
    private emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
2✔
167
        const event: ServerEvent = {
96✔
168
            timestamp: new Date().toISOString(),
96✔
169
            source: "mdbmcp",
96✔
170
            properties: {
96✔
171
                result: "success",
96✔
172
                duration_ms: commandDuration,
96✔
173
                component: "server",
96✔
174
                category: "other",
96✔
175
                command: command,
96✔
176
            },
96✔
177
        };
96✔
178

179
        if (command === "start") {
96✔
180
            event.properties.startup_time_ms = commandDuration;
48✔
181
            event.properties.read_only_mode = this.userConfig.readOnly || false;
48✔
182
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
48!
183
        }
48✔
184
        if (command === "stop") {
96✔
185
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
48✔
186
            if (error) {
48!
187
                event.properties.result = "failure";
×
188
                event.properties.reason = error.message;
×
189
            }
×
190
        }
48✔
191

192
        this.telemetry.emitEvents([event]).catch(() => {});
96✔
193
    }
96✔
194

195
    private registerTools(): void {
2✔
196
        for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
48✔
197
            const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
1,584✔
198
            if (tool.register(this)) {
1,584✔
199
                this.tools.push(tool);
1,091✔
200
            }
1,091✔
201
        }
1,584✔
202
    }
48✔
203

204
    private registerResources(): void {
2✔
205
        for (const resourceConstructor of Resources) {
48✔
206
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
144✔
207
            resource.register(this);
144✔
208
        }
144✔
209
    }
48✔
210

211
    private async validateConfig(): Promise<void> {
2✔
212
        // Validate connection string
213
        if (this.userConfig.connectionString) {
48!
214
            try {
1✔
215
                validateConnectionString(this.userConfig.connectionString, false);
1✔
216
            } catch (error) {
1!
217
                console.error("Connection string validation failed with error: ", error);
×
218
                throw new Error(
×
219
                    "Connection string validation failed with error: " +
×
220
                        (error instanceof Error ? error.message : String(error))
×
221
                );
×
222
            }
×
223
        }
1✔
224

225
        // Validate API client credentials
226
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
48✔
227
            try {
8✔
228
                await this.session.apiClient.validateAccessToken();
8✔
229
            } catch (error) {
8!
230
                if (this.userConfig.connectionString === undefined) {
×
231
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
232

233
                    throw new Error(
×
234
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
235
                    );
×
236
                }
×
237
                console.error(
×
238
                    "Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
×
239
                );
×
240
            }
×
241
        }
8✔
242
    }
48✔
243

244
    private async connectToConfigConnectionString(): Promise<void> {
2✔
245
        if (this.userConfig.connectionString) {
48!
246
            try {
1✔
247
                this.session.logger.info({
1✔
248
                    id: LogId.mongodbConnectTry,
1✔
249
                    context: "server",
1✔
250
                    message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
1✔
251
                });
1✔
252
                await this.session.connectToMongoDB({
1✔
253
                    connectionString: this.userConfig.connectionString,
1✔
254
                });
1✔
255
            } catch (error) {
1!
256
                // We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
NEW
257
                this.session.logger.error({
×
NEW
258
                    id: LogId.mongodbConnectFailure,
×
NEW
259
                    context: "server",
×
NEW
260
                    message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
×
NEW
261
                });
×
UNCOV
262
            }
×
263
        }
1✔
264
    }
48✔
265
}
2✔
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

© 2025 Coveralls, Inc