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

mongodb-js / mongodb-mcp-server / 15759142556

19 Jun 2025 01:32PM UTC coverage: 73.23%. First build
15759142556

Pull #298

github

web-flow
Merge 814ccb9b3 into 54effbbe4
Pull Request #298: chore: [MCP-2] add is_container_env to telemetry

210 of 378 branches covered (55.56%)

Branch coverage included in aggregate %.

38 of 55 new or added lines in 3 files covered. (69.09%)

783 of 978 relevant lines covered (80.06%)

56.26 hits per line

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

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

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

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

19
export abstract class ToolBase {
20
    protected abstract name: string;
21

22
    protected abstract category: ToolCategory;
23

24
    protected abstract operationType: OperationType;
25

26
    protected abstract description: string;
27

28
    protected abstract argsShape: ZodRawShape;
29

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

36
        switch (this.operationType) {
1,119!
37
            case "read":
38
            case "metadata":
39
                annotations.readOnlyHint = true;
859✔
40
                annotations.destructiveHint = false;
859✔
41
                break;
859✔
42
            case "delete":
43
                annotations.readOnlyHint = false;
87✔
44
                annotations.destructiveHint = true;
87✔
45
                break;
87✔
46
            case "create":
47
            case "update":
48
                annotations.destructiveHint = false;
173✔
49
                annotations.readOnlyHint = false;
173✔
50
                break;
173✔
51
            default:
52
                break;
×
53
        }
54

55
        return annotations;
1,119✔
56
    }
57

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

60
    constructor(
61
        protected readonly session: Session,
960✔
62
        protected readonly config: UserConfig,
960✔
63
        protected readonly telemetry: Telemetry
960✔
64
    ) {}
65

66
    public register(server: McpServer): void {
67
        if (!this.verifyAllowed()) {
960✔
68
            return;
276✔
69
        }
70

71
        const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
684✔
72
            const startTime = Date.now();
232✔
73
            try {
232✔
74
                logger.debug(LogId.toolExecute, "tool", `Executing tool ${this.name}`);
232✔
75

76
                const result = await this.execute(...args);
232✔
77
                this.emitToolEvent(startTime, result, ...args);
202✔
78
                return result;
202✔
79
            } catch (error: unknown) {
80
                logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
30✔
81
                const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
30✔
82
                this.emitToolEvent(startTime, toolResult, ...args);
30✔
83
                return toolResult;
30✔
84
            }
85
        };
86

87
        server.tool(this.name, this.description, this.argsShape, this.annotations, callback);
684✔
88

89
        // This is very similar to RegisteredTool.update, but without the bugs around the name.
90
        // In the upstream update method, the name is captured in the closure and not updated when
91
        // the tool name changes. This means that you only get one name update before things end up
92
        // in a broken state.
93
        this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => {
684✔
94
            const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
435✔
95
            const existingTool = tools[this.name];
435✔
96

97
            if (!existingTool) {
435!
98
                logger.warning(LogId.toolUpdateFailure, "tool", `Tool ${this.name} not found in update`);
×
99
                return;
×
100
            }
101

102
            existingTool.annotations = this.annotations;
435✔
103

104
            if (updates.name && updates.name !== this.name) {
435✔
105
                existingTool.annotations.title = updates.name;
173✔
106
                delete tools[this.name];
173✔
107
                this.name = updates.name;
173✔
108
                tools[this.name] = existingTool;
173✔
109
            }
110

111
            if (updates.description) {
435✔
112
                existingTool.annotations.description = updates.description;
435✔
113
                existingTool.description = updates.description;
435✔
114
                this.description = updates.description;
435✔
115
            }
116

117
            if (updates.inputSchema) {
435✔
118
                existingTool.inputSchema = updates.inputSchema;
435✔
119
            }
120

121
            server.sendToolListChanged();
435✔
122
        };
123
    }
124

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

127
    // Checks if a tool is allowed to run based on the config
128
    protected verifyAllowed(): boolean {
129
        let errorClarification: string | undefined;
130

131
        // Check read-only mode first
132
        if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
696✔
133
            errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
12✔
134
        } else if (this.config.disabledTools.includes(this.category)) {
684!
135
            errorClarification = `its category, \`${this.category}\`,`;
×
136
        } else if (this.config.disabledTools.includes(this.operationType)) {
684!
137
            errorClarification = `its operation type, \`${this.operationType}\`,`;
×
138
        } else if (this.config.disabledTools.includes(this.name)) {
684!
139
            errorClarification = `it`;
×
140
        }
141

142
        if (errorClarification) {
696✔
143
            logger.debug(
12✔
144
                LogId.toolDisabled,
145
                "tool",
146
                `Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
147
            );
148

149
            return false;
12✔
150
        }
151

152
        return true;
684✔
153
    }
154

155
    // This method is intended to be overridden by subclasses to handle errors
156
    protected handleError(
157
        error: unknown,
158
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
159
        args: ToolArgs<typeof this.argsShape>
160
    ): Promise<CallToolResult> | CallToolResult {
161
        return {
4✔
162
            content: [
163
                {
164
                    type: "text",
165
                    text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
4!
166
                },
167
            ],
168
            isError: true,
169
        };
170
    }
171

172
    protected abstract resolveTelemetryMetadata(
173
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
174
    ): TelemetryToolMetadata;
175

176
    /**
177
     * Creates and emits a tool telemetry event
178
     * @param startTime - Start time in milliseconds
179
     * @param result - Whether the command succeeded or failed
180
     * @param args - The arguments passed to the tool
181
     */
182
    private emitToolEvent(
183
        startTime: number,
184
        result: CallToolResult,
185
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
186
    ): void {
187
        if (!this.telemetry.isTelemetryEnabled()) {
232✔
188
            return;
232✔
189
        }
190
        const duration = Date.now() - startTime;
×
191
        const metadata = this.resolveTelemetryMetadata(...args);
×
192
        const event: ToolEvent = {
×
193
            timestamp: new Date().toISOString(),
194
            source: "mdbmcp",
195
            properties: {
196
                command: this.name,
197
                category: this.category,
198
                component: "tool",
199
                duration_ms: duration,
200
                result: result.isError ? "failure" : "success",
×
201
            },
202
        };
203

204
        if (metadata?.orgId) {
×
205
            event.properties.org_id = metadata.orgId;
×
206
        }
207

208
        if (metadata?.projectId) {
×
209
            event.properties.project_id = metadata.projectId;
×
210
        }
211

NEW
212
        this.telemetry.emitEvents([event]);
×
213
    }
214
}
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