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

mongodb-js / mongodb-mcp-server / 14735750222

29 Apr 2025 03:54PM UTC coverage: 78.141%. First build
14735750222

Pull #167

github

fmenezes
fix: issue template with title twice
Pull Request #167: fix: issue template with title twice

134 of 244 branches covered (54.92%)

Branch coverage included in aggregate %.

774 of 918 relevant lines covered (84.31%)

39.05 hits per line

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

53.85
/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
export type TelemetryToolMetadata = {
15
    projectId?: string;
16
    orgId?: string;
17
};
18

19
export abstract class ToolBase {
31✔
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 abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
31

32
    constructor(
33
        protected readonly session: Session,
810✔
34
        protected readonly config: UserConfig,
810✔
35
        protected readonly telemetry: Telemetry
810✔
36
    ) {}
37

38
    public register(server: McpServer): void {
39
        if (!this.verifyAllowed()) {
810✔
40
            return;
232✔
41
        }
42

43
        const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
578✔
44
            const startTime = Date.now();
99✔
45
            try {
99✔
46
                logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
99✔
47

48
                const result = await this.execute(...args);
99✔
49
                await this.emitToolEvent(startTime, result, ...args).catch(() => {});
93✔
50
                return result;
93✔
51
            } catch (error: unknown) {
52
                logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
6✔
53
                const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
6✔
54
                await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
6✔
55
                return toolResult;
6✔
56
            }
57
        };
58

59
        server.tool(this.name, this.description, this.argsShape, callback);
578✔
60

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

69
            if (updates.name && updates.name !== this.name) {
×
70
                delete tools[this.name];
×
71
                this.name = updates.name;
×
72
                tools[this.name] = existingTool;
×
73
            }
74

75
            if (updates.description) {
×
76
                existingTool.description = updates.description;
×
77
                this.description = updates.description;
×
78
            }
79

80
            if (updates.inputSchema) {
×
81
                existingTool.inputSchema = updates.inputSchema;
×
82
            }
83

84
            server.sendToolListChanged();
×
85
        };
86
    }
87

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

90
    // Checks if a tool is allowed to run based on the config
91
    protected verifyAllowed(): boolean {
92
        let errorClarification: string | undefined;
93

94
        // Check read-only mode first
95
        if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
590✔
96
            errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
12✔
97
        } else if (this.config.disabledTools.includes(this.category)) {
578!
98
            errorClarification = `its category, \`${this.category}\`,`;
×
99
        } else if (this.config.disabledTools.includes(this.operationType)) {
578!
100
            errorClarification = `its operation type, \`${this.operationType}\`,`;
×
101
        } else if (this.config.disabledTools.includes(this.name)) {
578!
102
            errorClarification = `it`;
×
103
        }
104

105
        if (errorClarification) {
590✔
106
            logger.debug(
12✔
107
                LogId.toolDisabled,
108
                "tool",
109
                `Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
110
            );
111

112
            return false;
12✔
113
        }
114

115
        return true;
578✔
116
    }
117

118
    // This method is intended to be overridden by subclasses to handle errors
119
    protected handleError(
120
        error: unknown,
121
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
122
        args: ToolArgs<typeof this.argsShape>
123
    ): Promise<CallToolResult> | CallToolResult {
124
        return {
1✔
125
            content: [
126
                {
127
                    type: "text",
128
                    text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
1!
129
                    isError: true,
130
                },
131
            ],
132
        };
133
    }
134

135
    protected abstract resolveTelemetryMetadata(
136
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
137
    ): TelemetryToolMetadata;
138

139
    /**
140
     * Creates and emits a tool telemetry event
141
     * @param startTime - Start time in milliseconds
142
     * @param result - Whether the command succeeded or failed
143
     * @param args - The arguments passed to the tool
144
     */
145
    private async emitToolEvent(
146
        startTime: number,
147
        result: CallToolResult,
148
        ...args: Parameters<ToolCallback<typeof this.argsShape>>
149
    ): Promise<void> {
150
        if (!this.telemetry.isTelemetryEnabled()) {
99✔
151
            return;
99✔
152
        }
153
        const duration = Date.now() - startTime;
×
154
        const metadata = this.resolveTelemetryMetadata(...args);
×
155
        const event: ToolEvent = {
×
156
            timestamp: new Date().toISOString(),
157
            source: "mdbmcp",
158
            properties: {
159
                command: this.name,
160
                category: this.category,
161
                component: "tool",
162
                duration_ms: duration,
163
                result: result.isError ? "failure" : "success",
×
164
            },
165
        };
166

167
        if (metadata?.orgId) {
×
168
            event.properties.org_id = metadata.orgId;
×
169
        }
170

171
        if (metadata?.projectId) {
×
172
            event.properties.project_id = metadata.projectId;
×
173
        }
174

175
        await this.telemetry.emitEvents([event]);
×
176
    }
177
}
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