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

mongodb-js / mongodb-mcp-server / 14714557796

28 Apr 2025 05:59PM UTC coverage: 82.713%. First build
14714557796

Pull #151

github

blva
chore: default telemetry
Pull Request #151: chore: default telemetry

142 of 235 branches covered (60.43%)

Branch coverage included in aggregate %.

791 of 893 relevant lines covered (88.58%)

48.19 hits per line

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

63.24
/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 } from "@modelcontextprotocol/sdk/types.js";
4
import { Session } from "../session.js";
5
import logger, { LogId } from "../logger.js";
31✔
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

15
export abstract class ToolBase {
31✔
16
    protected abstract name: string;
17

18
    protected abstract category: ToolCategory;
19

20
    protected abstract operationType: OperationType;
21

22
    protected abstract description: string;
23

24
    protected abstract argsShape: ZodRawShape;
25

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

28
    constructor(
29
        protected readonly session: Session,
810✔
30
        protected readonly config: UserConfig,
810✔
31
        protected readonly telemetry: Telemetry
810✔
32
    ) {}
33

34
    /**
35
     * Creates and emits a tool telemetry event
36
     * @param startTime - Start time in milliseconds
37
     * @param result - Whether the command succeeded or failed
38
     * @param error - Optional error if the command failed
39
     */
40
    private async emitToolEvent(startTime: number, result: CallToolResult): Promise<void> {
41
        const duration = Date.now() - startTime;
99✔
42
        const event: ToolEvent = {
99✔
43
            timestamp: new Date().toISOString(),
44
            source: "mdbmcp",
45
            properties: {
46
                ...this.telemetry.getCommonProperties(),
47
                command: this.name,
48
                category: this.category,
49
                component: "tool",
50
                duration_ms: duration,
51
                result: result.isError ? "failure" : "success",
99!
52
            },
53
        };
54
        await this.telemetry.emitEvents([event]);
99✔
55
    }
56

57
    public register(server: McpServer): void {
58
        if (!this.verifyAllowed()) {
810✔
59
            return;
232✔
60
        }
61

62
        const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
578✔
63
            const startTime = Date.now();
99✔
64
            try {
99✔
65
                logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
99✔
66

67
                const result = await this.execute(...args);
99✔
68
                await this.emitToolEvent(startTime, result);
93✔
69
                return result;
93✔
70
            } catch (error: unknown) {
71
                logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
6✔
72
                const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
6✔
73
                await this.emitToolEvent(startTime, toolResult).catch(() => {});
6✔
74
                return toolResult;
6✔
75
            }
76
        };
77

78
        server.tool(this.name, this.description, this.argsShape, callback);
578✔
79

80
        // This is very similar to RegisteredTool.update, but without the bugs around the name.
81
        // In the upstream update method, the name is captured in the closure and not updated when
82
        // the tool name changes. This means that you only get one name update before things end up
83
        // in a broken state.
84
        this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => {
578✔
85
            const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
×
86
            const existingTool = tools[this.name];
×
87

88
            if (updates.name && updates.name !== this.name) {
×
89
                delete tools[this.name];
×
90
                this.name = updates.name;
×
91
                tools[this.name] = existingTool;
×
92
            }
93

94
            if (updates.description) {
×
95
                existingTool.description = updates.description;
×
96
                this.description = updates.description;
×
97
            }
98

99
            if (updates.inputSchema) {
×
100
                existingTool.inputSchema = updates.inputSchema;
×
101
            }
102

103
            server.sendToolListChanged();
×
104
        };
105
    }
106

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

109
    // Checks if a tool is allowed to run based on the config
110
    protected verifyAllowed(): boolean {
111
        let errorClarification: string | undefined;
112

113
        // Check read-only mode first
114
        if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
590✔
115
            errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
12✔
116
        } else if (this.config.disabledTools.includes(this.category)) {
578!
117
            errorClarification = `its category, \`${this.category}\`,`;
×
118
        } else if (this.config.disabledTools.includes(this.operationType)) {
578!
119
            errorClarification = `its operation type, \`${this.operationType}\`,`;
×
120
        } else if (this.config.disabledTools.includes(this.name)) {
578!
121
            errorClarification = `it`;
×
122
        }
123

124
        if (errorClarification) {
590✔
125
            logger.debug(
12✔
126
                LogId.toolDisabled,
127
                "tool",
128
                `Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
129
            );
130

131
            return false;
12✔
132
        }
133

134
        return true;
578✔
135
    }
136

137
    // This method is intended to be overridden by subclasses to handle errors
138
    protected handleError(
139
        error: unknown,
140
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
        args: ToolArgs<typeof this.argsShape>
142
    ): Promise<CallToolResult> | CallToolResult {
143
        return {
1✔
144
            content: [
145
                {
146
                    type: "text",
147
                    text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
1!
148
                    isError: true,
149
                },
150
            ],
151
        };
152
    }
153
}
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

© 2026 Coveralls, Inc