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

mongodb-js / mongodb-mcp-server / 16779406063

06 Aug 2025 02:11PM UTC coverage: 81.424% (-0.4%) from 81.781%
16779406063

Pull #420

github

web-flow
Merge cc01ac54e into a35d18dd6
Pull Request #420: chore: allow logging unredacted messages MCP-103

679 of 874 branches covered (77.69%)

Branch coverage included in aggregate %.

161 of 291 new or added lines in 16 files covered. (55.33%)

7 existing lines in 4 files now uncovered.

3485 of 4240 relevant lines covered (82.19%)

63.42 hits per line

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

80.83
/src/tools/tool.ts
1
import { z, type ZodRawShape, type ZodNever, AnyZodObject } from "zod";
2
import type { RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
3
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
4
import { Session } from "../common/session.js";
5
import logger, { LogId } from "../common/logger.js";
2✔
6
import { Telemetry } from "../telemetry/telemetry.js";
7
import { type ToolEvent } from "../telemetry/types.js";
8
import { UserConfig } from "../common/config.js";
9
import { Server } from "../server.js";
10

11
export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
12

13
export type OperationType = "metadata" | "read" | "create" | "delete" | "update" | "connect";
14
export type ToolCategory = "mongodb" | "atlas";
15
export type TelemetryToolMetadata = {
16
    projectId?: string;
17
    orgId?: string;
18
};
19

20
export abstract class ToolBase {
2✔
21
    public abstract name: string;
22

23
    public abstract category: ToolCategory;
24

25
    public abstract operationType: OperationType;
26

27
    protected abstract description: string;
28

29
    protected abstract argsShape: ZodRawShape;
30

31
    protected get annotations(): ToolAnnotations {
2✔
32
        const annotations: ToolAnnotations = {
2✔
33
            title: this.name,
2✔
34
            description: this.description,
2✔
35
        };
2✔
36

37
        switch (this.operationType) {
2✔
38
            case "read":
2✔
39
            case "metadata":
2✔
40
            case "connect":
2✔
41
                annotations.readOnlyHint = true;
2,009✔
42
                annotations.destructiveHint = false;
2,009✔
43
                break;
2,009✔
44
            case "delete":
2✔
45
                annotations.readOnlyHint = false;
198✔
46
                annotations.destructiveHint = true;
198✔
47
                break;
198✔
48
            case "create":
2✔
49
            case "update":
2✔
50
                annotations.destructiveHint = false;
474✔
51
                annotations.readOnlyHint = false;
474✔
52
                break;
474✔
53
            default:
2!
54
                break;
×
55
        }
2✔
56

57
        return annotations;
2✔
58
    }
2✔
59

60
    protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
61

62
    constructor(
2✔
63
        protected readonly session: Session,
2,176✔
64
        protected readonly config: UserConfig,
2,176✔
65
        protected readonly telemetry: Telemetry
2,176✔
66
    ) {}
2,176✔
67

68
    public register(server: Server): boolean {
2✔
69
        if (!this.verifyAllowed()) {
2,176✔
70
            return false;
391✔
71
        }
391✔
72

73
        const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
1,785✔
74
            const startTime = Date.now();
522✔
75
            try {
522✔
76
                logger.debug({
522✔
77
                    id: LogId.toolExecute,
522✔
78
                    context: "tool",
522✔
79
                    message: `Executing tool ${this.name}`,
522✔
80
                    noRedaction: true,
522✔
81
                });
522✔
82

83
                const result = await this.execute(...args);
522✔
84
                await this.emitToolEvent(startTime, result, ...args).catch(() => {});
449✔
85
                return result;
449✔
86
            } catch (error: unknown) {
522✔
87
                logger.error({
73✔
88
                    id: LogId.toolExecuteFailure,
73✔
89
                    context: "tool",
73✔
90
                    message: `Error executing ${this.name}: ${error as string}`,
73✔
91
                });
73✔
92
                const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
73✔
93
                await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
73✔
94
                return toolResult;
73✔
95
            }
73✔
96
        };
522✔
97

98
        server.mcpServer.tool(this.name, this.description, this.argsShape, this.annotations, callback);
1,785✔
99

100
        // This is very similar to RegisteredTool.update, but without the bugs around the name.
101
        // In the upstream update method, the name is captured in the closure and not updated when
102
        // the tool name changes. This means that you only get one name update before things end up
103
        // in a broken state.
104
        // See https://github.com/modelcontextprotocol/typescript-sdk/issues/414 for more details.
105
        this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => {
1,785✔
106
            const tools = server.mcpServer["_registeredTools"] as { [toolName: string]: RegisteredTool };
896✔
107
            const existingTool = tools[this.name];
896✔
108

109
            if (!existingTool) {
896!
NEW
110
                logger.warning({
×
NEW
111
                    id: LogId.toolUpdateFailure,
×
NEW
112
                    context: "tool",
×
NEW
113
                    message: `Tool ${this.name} not found in update`,
×
NEW
114
                    noRedaction: true,
×
NEW
115
                });
×
116
                return;
×
117
            }
×
118

119
            existingTool.annotations = this.annotations;
896✔
120

121
            if (updates.name && updates.name !== this.name) {
896✔
122
                existingTool.annotations.title = updates.name;
596✔
123
                delete tools[this.name];
596✔
124
                this.name = updates.name;
596✔
125
                tools[this.name] = existingTool;
596✔
126
            }
596✔
127

128
            if (updates.description) {
896✔
129
                existingTool.annotations.description = updates.description;
896✔
130
                existingTool.description = updates.description;
896✔
131
                this.description = updates.description;
896✔
132
            }
896✔
133

134
            if (updates.inputSchema) {
896✔
135
                existingTool.inputSchema = updates.inputSchema;
896✔
136
            }
896✔
137

138
            server.mcpServer.sendToolListChanged();
896✔
139
        };
896✔
140

141
        return true;
1,785✔
142
    }
2,176✔
143

144
    protected update?: (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => void;
145

146
    // Checks if a tool is allowed to run based on the config
147
    protected verifyAllowed(): boolean {
2✔
148
        let errorClarification: string | undefined;
1,816✔
149

150
        // Check read-only mode first
151
        if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
1,816✔
152
            errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
28✔
153
        } else if (this.config.disabledTools.includes(this.category)) {
1,816!
154
            errorClarification = `its category, \`${this.category}\`,`;
×
155
        } else if (this.config.disabledTools.includes(this.operationType)) {
1,788✔
156
            errorClarification = `its operation type, \`${this.operationType}\`,`;
3✔
157
        } else if (this.config.disabledTools.includes(this.name)) {
1,788!
158
            errorClarification = `it`;
×
159
        }
×
160

161
        if (errorClarification) {
1,816✔
162
            logger.debug({
31✔
163
                id: LogId.toolDisabled,
31✔
164
                context: "tool",
31✔
165
                message: `Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`,
31✔
166
                noRedaction: true,
31✔
167
            });
31✔
168

169
            return false;
31✔
170
        }
31✔
171

172
        return true;
1,785✔
173
    }
1,816✔
174

175
    // This method is intended to be overridden by subclasses to handle errors
176
    protected handleError(
2✔
177
        error: unknown,
4✔
178
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
179
        args: ToolArgs<typeof this.argsShape>
4✔
180
    ): Promise<CallToolResult> | CallToolResult {
4✔
181
        return {
4✔
182
            content: [
4✔
183
                {
4✔
184
                    type: "text",
4✔
185
                    text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
4!
186
                },
4✔
187
            ],
4✔
188
            isError: true,
4✔
189
        };
4✔
190
    }
4✔
191

192
    protected abstract resolveTelemetryMetadata(
193
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
194
    ): TelemetryToolMetadata;
195

196
    /**
197
     * Creates and emits a tool telemetry event
198
     * @param startTime - Start time in milliseconds
199
     * @param result - Whether the command succeeded or failed
200
     * @param args - The arguments passed to the tool
201
     */
202
    private async emitToolEvent(
2✔
203
        startTime: number,
522✔
204
        result: CallToolResult,
522✔
205
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
522✔
206
    ): Promise<void> {
522✔
207
        if (!this.telemetry.isTelemetryEnabled()) {
522✔
208
            return;
522✔
209
        }
522!
210
        const duration = Date.now() - startTime;
×
211
        const metadata = this.resolveTelemetryMetadata(...args);
×
212
        const event: ToolEvent = {
×
213
            timestamp: new Date().toISOString(),
×
214
            source: "mdbmcp",
×
215
            properties: {
×
216
                command: this.name,
×
217
                category: this.category,
×
218
                component: "tool",
×
219
                duration_ms: duration,
×
220
                result: result.isError ? "failure" : "success",
522!
221
            },
522✔
222
        };
522✔
223

224
        if (metadata?.orgId) {
522!
225
            event.properties.org_id = metadata.orgId;
×
226
        }
×
227

228
        if (metadata?.projectId) {
522!
229
            event.properties.project_id = metadata.projectId;
×
230
        }
×
231

232
        await this.telemetry.emitEvents([event]);
×
233
    }
522✔
234
}
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