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

mongodb-js / mongodb-mcp-server / 17155288437

22 Aug 2025 12:29PM UTC coverage: 82.014% (-0.03%) from 82.04%
17155288437

Pull #468

github

web-flow
Merge 3bb127c20 into ca68195c4
Pull Request #468: chore: update readme with the new tool and resources

855 of 1075 branches covered (79.53%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 2 files covered. (100.0%)

97 existing lines in 8 files now uncovered.

4357 of 5280 relevant lines covered (82.52%)

70.4 hits per line

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

79.64
/src/server.ts
1
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
import { Session } from "./common/session.js";
3
import { 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 { LogId } from "./common/logger.js";
2✔
8
import { Telemetry } from "./telemetry/telemetry.js";
9
import { UserConfig } from "./common/config.js";
10
import { type ServerEvent } from "./telemetry/types.js";
11
import { type ServerCommand } from "./telemetry/types.js";
12
import {
2✔
13
    CallToolRequestSchema,
14
    CallToolResult,
15
    SubscribeRequestSchema,
16
    UnsubscribeRequestSchema,
17
} from "@modelcontextprotocol/sdk/types.js";
18
import assert from "assert";
2✔
19
import { ToolBase } from "./tools/tool.js";
20
import { validateConnectionString } from "./helpers/connectionOptions.js";
2✔
21

22
export interface ServerOptions {
23
    session: Session;
24
    userConfig: UserConfig;
25
    mcpServer: McpServer;
26
    telemetry: Telemetry;
27
}
28

29
export class Server {
2✔
30
    public readonly session: Session;
31
    public readonly mcpServer: McpServer;
32
    private readonly telemetry: Telemetry;
33
    public readonly userConfig: UserConfig;
34
    public readonly tools: ToolBase[] = [];
2✔
35
    private readonly startTime: number;
36
    private readonly subscriptions = new Set<string>();
2✔
37

38
    constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
2✔
39
        this.startTime = Date.now();
80✔
40
        this.session = session;
80✔
41
        this.telemetry = telemetry;
80✔
42
        this.mcpServer = mcpServer;
80✔
43
        this.userConfig = userConfig;
80✔
44
    }
80✔
45

46
    async connect(transport: Transport): Promise<void> {
2✔
47
        // Resources are now reactive, so we register them ASAP so they can listen to events like
48
        // connection events.
49
        this.registerResources();
80✔
50
        await this.validateConfig();
80✔
51

52
        this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } });
80✔
53

54
        // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
55
        this.registerTools();
80✔
56

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

70
        assert(existingHandler, "No existing handler found for CallToolRequestSchema");
80✔
71

72
        this.mcpServer.server.setRequestHandler(CallToolRequestSchema, (request, extra): Promise<CallToolResult> => {
80✔
73
            if (!request.params.arguments) {
896✔
74
                request.params.arguments = {};
2✔
75
            }
2✔
76

77
            return existingHandler(request, extra);
896✔
78
        });
80✔
79

80
        this.mcpServer.server.setRequestHandler(SubscribeRequestSchema, ({ params }) => {
80✔
81
            this.subscriptions.add(params.uri);
40✔
82
            this.session.logger.debug({
40✔
83
                id: LogId.serverInitialized,
40✔
84
                context: "resources",
40✔
85
                message: `Client subscribed to resource: ${params.uri}`,
40✔
86
            });
40✔
87
            return {};
40✔
88
        });
80✔
89

90
        this.mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
80✔
91
            this.subscriptions.delete(params.uri);
×
92
            this.session.logger.debug({
×
93
                id: LogId.serverInitialized,
×
94
                context: "resources",
×
95
                message: `Client unsubscribed from resource: ${params.uri}`,
×
96
            });
×
UNCOV
97
            return {};
×
98
        });
80✔
99

100
        this.mcpServer.server.oninitialized = (): void => {
80✔
101
            this.session.setMcpClient(this.mcpServer.server.getClientVersion());
80✔
102
            // Placed here to start the connection to the config connection string as soon as the server is initialized.
103
            void this.connectToConfigConnectionString();
80✔
104

105
            this.session.logger.info({
80✔
106
                id: LogId.serverInitialized,
80✔
107
                context: "server",
80✔
108
                message: `Server started with transport ${transport.constructor.name} and agent runner ${this.session.mcpClient?.name}`,
80✔
109
            });
80✔
110

111
            this.emitServerEvent("start", Date.now() - this.startTime);
80✔
112
        };
80✔
113

114
        this.mcpServer.server.onclose = (): void => {
80✔
115
            const closeTime = Date.now();
80✔
116
            this.emitServerEvent("stop", Date.now() - closeTime);
80✔
117
        };
80✔
118

119
        this.mcpServer.server.onerror = (error: Error): void => {
80✔
UNCOV
120
            const closeTime = Date.now();
×
UNCOV
121
            this.emitServerEvent("stop", Date.now() - closeTime, error);
×
UNCOV
122
        };
×
123

124
        await this.mcpServer.connect(transport);
80✔
125
    }
80✔
126

127
    async close(): Promise<void> {
2✔
128
        await this.telemetry.close();
80✔
129
        await this.session.close();
80✔
130
        await this.mcpServer.close();
79✔
131
    }
80✔
132

133
    public sendResourceListChanged(): void {
2✔
134
        this.mcpServer.sendResourceListChanged();
998✔
135
    }
998✔
136

137
    public sendResourceUpdated(uri: string): void {
2✔
138
        if (this.subscriptions.has(uri)) {
998✔
139
            void this.mcpServer.server.sendResourceUpdated({ uri });
40✔
140
        }
40✔
141
    }
998✔
142

143
    /**
144
     * Emits a server event
145
     * @param command - The server command (e.g., "start", "stop", "register", "deregister")
146
     * @param additionalProperties - Additional properties specific to the event
147
     */
148
    private emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
2✔
149
        const event: ServerEvent = {
160✔
150
            timestamp: new Date().toISOString(),
160✔
151
            source: "mdbmcp",
160✔
152
            properties: {
160✔
153
                result: "success",
160✔
154
                duration_ms: commandDuration,
160✔
155
                component: "server",
160✔
156
                category: "other",
160✔
157
                command: command,
160✔
158
            },
160✔
159
        };
160✔
160

161
        if (command === "start") {
160✔
162
            event.properties.startup_time_ms = commandDuration;
80✔
163
            event.properties.read_only_mode = this.userConfig.readOnly || false;
80✔
164
            event.properties.disabled_tools = this.userConfig.disabledTools || [];
80!
165
        }
80✔
166
        if (command === "stop") {
160✔
167
            event.properties.runtime_duration_ms = Date.now() - this.startTime;
80✔
168
            if (error) {
80!
UNCOV
169
                event.properties.result = "failure";
×
UNCOV
170
                event.properties.reason = error.message;
×
UNCOV
171
            }
×
172
        }
80✔
173

174
        this.telemetry.emitEvents([event]).catch(() => {});
160✔
175
    }
160✔
176

177
    private registerTools(): void {
2✔
178
        for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
80✔
179
            const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
2,640✔
180
            if (tool.register(this)) {
2,640✔
181
                this.tools.push(tool);
2,177✔
182
            }
2,177✔
183
        }
2,640✔
184
    }
80✔
185

186
    private registerResources(): void {
2✔
187
        for (const resourceConstructor of Resources) {
80✔
188
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
240✔
189
            resource.register(this);
240✔
190
        }
240✔
191
    }
80✔
192

193
    private async validateConfig(): Promise<void> {
2✔
194
        // Validate connection string
195
        if (this.userConfig.connectionString) {
80✔
196
            try {
2✔
197
                validateConnectionString(this.userConfig.connectionString, false);
2✔
198
            } catch (error) {
2!
199
                console.error("Connection string validation failed with error: ", error);
×
200
                throw new Error(
×
201
                    "Connection string validation failed with error: " +
×
202
                        (error instanceof Error ? error.message : String(error))
×
UNCOV
203
                );
×
UNCOV
204
            }
×
205
        }
2✔
206

207
        // Validate API client credentials
208
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
80✔
209
            try {
44✔
210
                await this.session.apiClient.validateAccessToken();
44✔
211
            } catch (error) {
44!
212
                if (this.userConfig.connectionString === undefined) {
×
213
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
214

215
                    throw new Error(
×
216
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
217
                    );
×
218
                }
×
219
                console.error(
×
UNCOV
220
                    "Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
×
UNCOV
221
                );
×
UNCOV
222
            }
×
223
        }
44✔
224
    }
80✔
225

226
    private async connectToConfigConnectionString(): Promise<void> {
2✔
227
        if (this.userConfig.connectionString) {
80✔
228
            try {
2✔
229
                await this.session.connectToMongoDB({
2✔
230
                    connectionString: this.userConfig.connectionString,
2✔
231
                });
2✔
232
            } catch (error) {
2!
UNCOV
233
                console.error(
×
UNCOV
234
                    "Failed to connect to MongoDB instance using the connection string from the config: ",
×
UNCOV
235
                    error
×
UNCOV
236
                );
×
UNCOV
237
                throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
×
UNCOV
238
            }
×
239
        }
2✔
240
    }
80✔
241
}
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