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

mongodb-js / mongodb-mcp-server / 16876636630

11 Aug 2025 09:51AM UTC coverage: 81.999% (+0.6%) from 81.362%
16876636630

Pull #424

github

web-flow
Merge 602f1c199 into 7572ec5d6
Pull Request #424: feat: adds an export tool and exported-data resource MCP-16

784 of 1003 branches covered (78.17%)

Branch coverage included in aggregate %.

592 of 659 new or added lines in 13 files covered. (89.83%)

18 existing lines in 3 files now uncovered.

4072 of 4919 relevant lines covered (82.78%)

66.27 hits per line

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

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

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

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

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

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

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

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

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

69
        assert(existingHandler, "No existing handler found for CallToolRequestSchema");
74✔
70

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

76
            return existingHandler(request, extra);
870✔
77
        });
74✔
78

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

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

99
        this.mcpServer.server.oninitialized = (): void => {
74✔
100
            this.session.setAgentRunner(this.mcpServer.server.getClientVersion());
74✔
101

102
            this.session.logger.info({
74✔
103
                id: LogId.serverInitialized,
74✔
104
                context: "server",
74✔
105
                message: `Server started with transport ${transport.constructor.name} and agent runner ${this.session.agentRunner?.name}`,
74✔
106
            });
74✔
107

108
            this.emitServerEvent("start", Date.now() - this.startTime);
74✔
109
        };
74✔
110

111
        this.mcpServer.server.onclose = (): void => {
74✔
112
            const closeTime = Date.now();
74✔
113
            this.emitServerEvent("stop", Date.now() - closeTime);
74✔
114
        };
74✔
115

116
        this.mcpServer.server.onerror = (error: Error): void => {
74✔
117
            const closeTime = Date.now();
×
118
            this.emitServerEvent("stop", Date.now() - closeTime, error);
×
119
        };
×
120

121
        await this.mcpServer.connect(transport);
74✔
122
    }
74✔
123

124
    async close(): Promise<void> {
2✔
125
        await this.telemetry.close();
74✔
126
        await this.session.close();
74✔
127
        await this.mcpServer.close();
73✔
128
    }
74✔
129

130
    public sendResourceListChanged(): void {
2✔
131
        this.mcpServer.sendResourceListChanged();
926✔
132
    }
926✔
133

134
    public sendResourceUpdated(uri: string): void {
2✔
135
        if (this.subscriptions.has(uri)) {
924✔
136
            void this.mcpServer.server.sendResourceUpdated({ uri });
36✔
137
        }
36✔
138
    }
924✔
139

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

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

171
        this.telemetry.emitEvents([event]).catch(() => {});
148✔
172
    }
148✔
173

174
    private registerTools(): void {
2✔
175
        for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) {
74✔
176
            const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
2,442✔
177
            if (tool.register(this)) {
2,442✔
178
                this.tools.push(tool);
2,015✔
179
            }
2,015✔
180
        }
2,442✔
181
    }
74✔
182

183
    private registerResources(): void {
2✔
184
        for (const resourceConstructor of Resources) {
74✔
185
            const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
222✔
186
            resource.register(this);
222✔
187
        }
222✔
188
    }
74✔
189

190
    private async validateConfig(): Promise<void> {
2✔
191
        const transport = this.userConfig.transport as string;
74✔
192
        if (transport !== "http" && transport !== "stdio") {
74!
193
            throw new Error(`Invalid transport: ${transport}`);
×
194
        }
×
195

196
        const telemetry = this.userConfig.telemetry as string;
74✔
197
        if (telemetry !== "enabled" && telemetry !== "disabled") {
74!
198
            throw new Error(`Invalid telemetry: ${telemetry}`);
×
199
        }
×
200

201
        if (this.userConfig.httpPort < 1 || this.userConfig.httpPort > 65535) {
74!
202
            throw new Error(`Invalid httpPort: ${this.userConfig.httpPort}`);
×
203
        }
×
204

205
        if (this.userConfig.loggers.length === 0) {
74!
206
            throw new Error("No loggers found in config");
×
207
        }
×
208

209
        const loggerTypes = new Set(this.userConfig.loggers);
74✔
210
        if (loggerTypes.size !== this.userConfig.loggers.length) {
74!
211
            throw new Error("Duplicate loggers found in config");
×
212
        }
×
213

214
        for (const loggerType of this.userConfig.loggers as string[]) {
74✔
215
            if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
78!
216
                throw new Error(`Invalid logger: ${loggerType}`);
×
217
            }
×
218
        }
78✔
219

220
        if (this.userConfig.connectionString) {
74✔
221
            try {
2✔
222
                await this.session.connectToMongoDB({
2✔
223
                    connectionString: this.userConfig.connectionString,
2✔
224
                    ...this.userConfig.connectOptions,
2✔
225
                });
2✔
226
            } catch (error) {
2!
227
                console.error(
×
228
                    "Failed to connect to MongoDB instance using the connection string from the config: ",
×
229
                    error
×
230
                );
×
231
                throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
×
232
            }
×
233
        }
2✔
234

235
        if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
74✔
236
            try {
41✔
237
                await this.session.apiClient.validateAccessToken();
41✔
238
            } catch (error) {
41!
239
                if (this.userConfig.connectionString === undefined) {
×
240
                    console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
×
241

242
                    throw new Error(
×
243
                        "Failed to connect to MongoDB Atlas instance using the credentials from the config"
×
244
                    );
×
245
                }
×
246
                console.error(
×
247
                    "Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
×
248
                );
×
249
            }
×
250
        }
41✔
251
    }
74✔
252
}
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