mongodb-mcp-server 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/pull_request_template.md +5 -0
- package/.github/workflows/code_health.yaml +3 -3
- package/.github/workflows/docker.yaml +1 -1
- package/.smithery/smithery.yaml +10 -0
- package/README.md +15 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/helpers/indexCheck.js +63 -0
- package/dist/helpers/indexCheck.js.map +1 -0
- package/dist/logger.js +0 -1
- package/dist/logger.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/telemetry/telemetry.js +78 -115
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/mongodb/delete/deleteMany.js +18 -0
- package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
- package/dist/tools/mongodb/metadata/explain.js +1 -1
- package/dist/tools/mongodb/metadata/explain.js.map +1 -1
- package/dist/tools/mongodb/mongodbTool.js +10 -0
- package/dist/tools/mongodb/mongodbTool.js.map +1 -1
- package/dist/tools/mongodb/read/aggregate.js +9 -0
- package/dist/tools/mongodb/read/aggregate.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +13 -0
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/read/find.js +7 -0
- package/dist/tools/mongodb/read/find.js.map +1 -1
- package/dist/tools/mongodb/update/updateMany.js +20 -0
- package/dist/tools/mongodb/update/updateMany.js.map +1 -1
- package/dist/tools/tool.js +4 -4
- package/dist/tools/tool.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +2 -0
- package/src/errors.ts +1 -0
- package/src/helpers/indexCheck.ts +83 -0
- package/src/logger.ts +0 -1
- package/src/server.ts +1 -1
- package/src/telemetry/telemetry.ts +98 -150
- package/src/telemetry/types.ts +0 -1
- package/src/tools/mongodb/delete/deleteMany.ts +20 -0
- package/src/tools/mongodb/metadata/explain.ts +1 -1
- package/src/tools/mongodb/mongodbTool.ts +10 -0
- package/src/tools/mongodb/read/aggregate.ts +11 -0
- package/src/tools/mongodb/read/count.ts +15 -0
- package/src/tools/mongodb/read/find.ts +9 -0
- package/src/tools/mongodb/update/updateMany.ts +22 -0
- package/src/tools/tool.ts +5 -5
- package/tests/integration/indexCheck.test.ts +463 -0
- package/tests/integration/server.test.ts +5 -4
- package/tests/integration/telemetry.test.ts +28 -0
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +1 -0
- package/tests/unit/indexCheck.test.ts +149 -0
- package/tests/unit/telemetry.test.ts +58 -106
|
@@ -7,152 +7,114 @@ import { MACHINE_METADATA } from "./constants.js";
|
|
|
7
7
|
import { EventCache } from "./eventCache.js";
|
|
8
8
|
import nodeMachineId from "node-machine-id";
|
|
9
9
|
import { getDeviceId } from "@mongodb-js/device-id";
|
|
10
|
-
import fs from "fs/promises";
|
|
11
|
-
|
|
12
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
13
|
-
try {
|
|
14
|
-
await fs.access(filePath, fs.constants.F_OK);
|
|
15
|
-
return true; // File exists
|
|
16
|
-
} catch (e: unknown) {
|
|
17
|
-
if (
|
|
18
|
-
e instanceof Error &&
|
|
19
|
-
(
|
|
20
|
-
e as Error & {
|
|
21
|
-
code: string;
|
|
22
|
-
}
|
|
23
|
-
).code === "ENOENT"
|
|
24
|
-
) {
|
|
25
|
-
return false; // File does not exist
|
|
26
|
-
}
|
|
27
|
-
throw e; // Re-throw unexpected errors
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
10
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(fileExists));
|
|
11
|
+
type EventResult = {
|
|
12
|
+
success: boolean;
|
|
13
|
+
error?: Error;
|
|
14
|
+
};
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
}
|
|
16
|
+
export const DEVICE_ID_TIMEOUT = 3000;
|
|
40
17
|
|
|
41
18
|
export class Telemetry {
|
|
19
|
+
private isBufferingEvents: boolean = true;
|
|
20
|
+
/** Resolves when the device ID is retrieved or timeout occurs */
|
|
21
|
+
public deviceIdPromise: Promise<string> | undefined;
|
|
42
22
|
private deviceIdAbortController = new AbortController();
|
|
43
23
|
private eventCache: EventCache;
|
|
44
24
|
private getRawMachineId: () => Promise<string>;
|
|
45
|
-
private getContainerEnv: () => Promise<boolean>;
|
|
46
|
-
private cachedCommonProperties?: CommonProperties;
|
|
47
|
-
private flushing: boolean = false;
|
|
48
25
|
|
|
49
26
|
private constructor(
|
|
50
27
|
private readonly session: Session,
|
|
51
28
|
private readonly userConfig: UserConfig,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
getRawMachineId,
|
|
55
|
-
getContainerEnv,
|
|
56
|
-
}: {
|
|
57
|
-
eventCache: EventCache;
|
|
58
|
-
getRawMachineId: () => Promise<string>;
|
|
59
|
-
getContainerEnv: () => Promise<boolean>;
|
|
60
|
-
}
|
|
29
|
+
private readonly commonProperties: CommonProperties,
|
|
30
|
+
{ eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
|
|
61
31
|
) {
|
|
62
32
|
this.eventCache = eventCache;
|
|
63
33
|
this.getRawMachineId = getRawMachineId;
|
|
64
|
-
this.getContainerEnv = getContainerEnv;
|
|
65
34
|
}
|
|
66
35
|
|
|
67
36
|
static create(
|
|
68
37
|
session: Session,
|
|
69
38
|
userConfig: UserConfig,
|
|
70
39
|
{
|
|
40
|
+
commonProperties = { ...MACHINE_METADATA },
|
|
71
41
|
eventCache = EventCache.getInstance(),
|
|
72
42
|
getRawMachineId = () => nodeMachineId.machineId(true),
|
|
73
|
-
getContainerEnv = isContainerized,
|
|
74
43
|
}: {
|
|
75
44
|
eventCache?: EventCache;
|
|
76
45
|
getRawMachineId?: () => Promise<string>;
|
|
77
|
-
|
|
46
|
+
commonProperties?: CommonProperties;
|
|
78
47
|
} = {}
|
|
79
48
|
): Telemetry {
|
|
80
|
-
const instance = new Telemetry(session, userConfig, {
|
|
81
|
-
eventCache,
|
|
82
|
-
getRawMachineId,
|
|
83
|
-
getContainerEnv,
|
|
84
|
-
});
|
|
49
|
+
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
|
|
85
50
|
|
|
51
|
+
void instance.start();
|
|
86
52
|
return instance;
|
|
87
53
|
}
|
|
88
54
|
|
|
55
|
+
private async start(): Promise<void> {
|
|
56
|
+
if (!this.isTelemetryEnabled()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.deviceIdPromise = getDeviceId({
|
|
60
|
+
getMachineId: () => this.getRawMachineId(),
|
|
61
|
+
onError: (reason, error) => {
|
|
62
|
+
switch (reason) {
|
|
63
|
+
case "resolutionError":
|
|
64
|
+
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
|
|
65
|
+
break;
|
|
66
|
+
case "timeout":
|
|
67
|
+
logger.debug(LogId.telemetryDeviceIdTimeout, "telemetry", "Device ID retrieval timed out");
|
|
68
|
+
break;
|
|
69
|
+
case "abort":
|
|
70
|
+
// No need to log in the case of aborts
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
abortSignal: this.deviceIdAbortController.signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.commonProperties.device_id = await this.deviceIdPromise;
|
|
78
|
+
|
|
79
|
+
this.isBufferingEvents = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
89
82
|
public async close(): Promise<void> {
|
|
90
83
|
this.deviceIdAbortController.abort();
|
|
91
|
-
|
|
84
|
+
this.isBufferingEvents = false;
|
|
85
|
+
await this.emitEvents(this.eventCache.getEvents());
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
/**
|
|
95
89
|
* Emits events through the telemetry pipeline
|
|
96
90
|
* @param events - The events to emit
|
|
97
91
|
*/
|
|
98
|
-
public emitEvents(events: BaseEvent[]): void {
|
|
99
|
-
|
|
92
|
+
public async emitEvents(events: BaseEvent[]): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
if (!this.isTelemetryEnabled()) {
|
|
95
|
+
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await this.emit(events);
|
|
100
|
+
} catch {
|
|
101
|
+
logger.debug(LogId.telemetryEmitFailure, "telemetry", `Error emitting telemetry events.`);
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
106
|
* Gets the common properties for events
|
|
104
107
|
* @returns Object containing common properties for all events
|
|
105
108
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
switch (reason) {
|
|
116
|
-
case "resolutionError":
|
|
117
|
-
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
|
|
118
|
-
break;
|
|
119
|
-
case "timeout":
|
|
120
|
-
logger.debug(
|
|
121
|
-
LogId.telemetryDeviceIdTimeout,
|
|
122
|
-
"telemetry",
|
|
123
|
-
"Device ID retrieval timed out"
|
|
124
|
-
);
|
|
125
|
-
break;
|
|
126
|
-
case "abort":
|
|
127
|
-
// No need to log in the case of aborts
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
abortSignal: this.deviceIdAbortController.signal,
|
|
132
|
-
}).then((id) => {
|
|
133
|
-
deviceId = id;
|
|
134
|
-
}),
|
|
135
|
-
this.getContainerEnv().then((env) => {
|
|
136
|
-
containerEnv = env;
|
|
137
|
-
}),
|
|
138
|
-
]);
|
|
139
|
-
} catch (error: unknown) {
|
|
140
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
141
|
-
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", err.message);
|
|
142
|
-
}
|
|
143
|
-
this.cachedCommonProperties = {
|
|
144
|
-
...MACHINE_METADATA,
|
|
145
|
-
mcp_client_version: this.session.agentRunner?.version,
|
|
146
|
-
mcp_client_name: this.session.agentRunner?.name,
|
|
147
|
-
session_id: this.session.sessionId,
|
|
148
|
-
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
|
|
149
|
-
config_connection_string: this.userConfig.connectionString ? "true" : "false",
|
|
150
|
-
is_container_env: containerEnv ? "true" : "false",
|
|
151
|
-
device_id: deviceId,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return this.cachedCommonProperties;
|
|
109
|
+
public getCommonProperties(): CommonProperties {
|
|
110
|
+
return {
|
|
111
|
+
...this.commonProperties,
|
|
112
|
+
mcp_client_version: this.session.agentRunner?.version,
|
|
113
|
+
mcp_client_name: this.session.agentRunner?.name,
|
|
114
|
+
session_id: this.session.sessionId,
|
|
115
|
+
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
|
|
116
|
+
config_connection_string: this.userConfig.connectionString ? "true" : "false",
|
|
117
|
+
};
|
|
156
118
|
}
|
|
157
119
|
|
|
158
120
|
/**
|
|
@@ -173,74 +135,60 @@ export class Telemetry {
|
|
|
173
135
|
}
|
|
174
136
|
|
|
175
137
|
/**
|
|
176
|
-
* Attempts to
|
|
138
|
+
* Attempts to emit events through authenticated and unauthenticated clients
|
|
177
139
|
* Falls back to caching if both attempts fail
|
|
178
140
|
*/
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (this.flushing) {
|
|
186
|
-
this.eventCache.appendEvents(events ?? []);
|
|
187
|
-
process.nextTick(async () => {
|
|
188
|
-
// try again if in the middle of a flush
|
|
189
|
-
await this.flush();
|
|
190
|
-
});
|
|
141
|
+
private async emit(events: BaseEvent[]): Promise<void> {
|
|
142
|
+
if (this.isBufferingEvents) {
|
|
143
|
+
this.eventCache.appendEvents(events);
|
|
191
144
|
return;
|
|
192
145
|
}
|
|
193
146
|
|
|
194
|
-
|
|
147
|
+
const cachedEvents = this.eventCache.getEvents();
|
|
148
|
+
const allEvents = [...cachedEvents, ...events];
|
|
195
149
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
logger.debug(
|
|
205
|
-
LogId.telemetryEmitStart,
|
|
206
|
-
"telemetry",
|
|
207
|
-
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
|
|
208
|
-
);
|
|
150
|
+
logger.debug(
|
|
151
|
+
LogId.telemetryEmitStart,
|
|
152
|
+
"telemetry",
|
|
153
|
+
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
|
|
154
|
+
);
|
|
209
155
|
|
|
210
|
-
|
|
156
|
+
const result = await this.sendEvents(this.session.apiClient, allEvents);
|
|
157
|
+
if (result.success) {
|
|
211
158
|
this.eventCache.clearEvents();
|
|
212
159
|
logger.debug(
|
|
213
160
|
LogId.telemetryEmitSuccess,
|
|
214
161
|
"telemetry",
|
|
215
162
|
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
|
|
216
163
|
);
|
|
217
|
-
|
|
218
|
-
logger.debug(
|
|
219
|
-
LogId.telemetryEmitFailure,
|
|
220
|
-
"telemetry",
|
|
221
|
-
`Error sending event to client: ${error instanceof Error ? error.message : String(error)}`
|
|
222
|
-
);
|
|
223
|
-
this.eventCache.appendEvents(events ?? []);
|
|
224
|
-
process.nextTick(async () => {
|
|
225
|
-
// try again
|
|
226
|
-
await this.flush();
|
|
227
|
-
});
|
|
164
|
+
return;
|
|
228
165
|
}
|
|
229
166
|
|
|
230
|
-
|
|
167
|
+
logger.debug(
|
|
168
|
+
LogId.telemetryEmitFailure,
|
|
169
|
+
"telemetry",
|
|
170
|
+
`Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}`
|
|
171
|
+
);
|
|
172
|
+
this.eventCache.appendEvents(events);
|
|
231
173
|
}
|
|
232
174
|
|
|
233
175
|
/**
|
|
234
176
|
* Attempts to send events through the provided API client
|
|
235
177
|
*/
|
|
236
|
-
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
178
|
+
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
|
|
179
|
+
try {
|
|
180
|
+
await client.sendEvents(
|
|
181
|
+
events.map((event) => ({
|
|
182
|
+
...event,
|
|
183
|
+
properties: { ...this.getCommonProperties(), ...event.properties },
|
|
184
|
+
}))
|
|
185
|
+
);
|
|
186
|
+
return { success: true };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
245
193
|
}
|
|
246
194
|
}
|
package/src/telemetry/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
+
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
|
|
5
6
|
|
|
6
7
|
export class DeleteManyTool extends MongoDBToolBase {
|
|
7
8
|
protected name = "delete-many";
|
|
@@ -23,6 +24,25 @@ export class DeleteManyTool extends MongoDBToolBase {
|
|
|
23
24
|
filter,
|
|
24
25
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
25
26
|
const provider = await this.ensureConnected();
|
|
27
|
+
|
|
28
|
+
// Check if delete operation uses an index if enabled
|
|
29
|
+
if (this.config.indexCheck) {
|
|
30
|
+
await checkIndexUsage(provider, database, collection, "deleteMany", async () => {
|
|
31
|
+
return provider.runCommandWithCheck(database, {
|
|
32
|
+
explain: {
|
|
33
|
+
delete: collection,
|
|
34
|
+
deletes: [
|
|
35
|
+
{
|
|
36
|
+
q: filter || {},
|
|
37
|
+
limit: 0, // 0 means delete all matching documents
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
verbosity: "queryPlanner",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
const result = await provider.deleteMany(database, collection, filter);
|
|
27
47
|
|
|
28
48
|
return {
|
|
@@ -76,7 +76,7 @@ export class ExplainTool extends MongoDBToolBase {
|
|
|
76
76
|
}
|
|
77
77
|
case "count": {
|
|
78
78
|
const { query } = method.arguments;
|
|
79
|
-
result = await provider.
|
|
79
|
+
result = await provider.runCommandWithCheck(database, {
|
|
80
80
|
explain: {
|
|
81
81
|
count: collection,
|
|
82
82
|
query,
|
|
@@ -64,6 +64,16 @@ export abstract class MongoDBToolBase extends ToolBase {
|
|
|
64
64
|
],
|
|
65
65
|
isError: true,
|
|
66
66
|
};
|
|
67
|
+
case ErrorCodes.ForbiddenCollscan:
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: error.message,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
|
|
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
3
3
|
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
import { EJSON } from "bson";
|
|
6
|
+
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
|
|
6
7
|
|
|
7
8
|
export const AggregateArgs = {
|
|
8
9
|
pipeline: z.array(z.record(z.string(), z.unknown())).describe("An array of aggregation stages to execute"),
|
|
@@ -23,6 +24,16 @@ export class AggregateTool extends MongoDBToolBase {
|
|
|
23
24
|
pipeline,
|
|
24
25
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
25
26
|
const provider = await this.ensureConnected();
|
|
27
|
+
|
|
28
|
+
// Check if aggregate operation uses an index if enabled
|
|
29
|
+
if (this.config.indexCheck) {
|
|
30
|
+
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
|
|
31
|
+
return provider
|
|
32
|
+
.aggregate(database, collection, pipeline, {}, { writeConcern: undefined })
|
|
33
|
+
.explain("queryPlanner");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
const documents = await provider.aggregate(database, collection, pipeline).toArray();
|
|
27
38
|
|
|
28
39
|
const content: Array<{ text: string; type: "text" }> = [
|
|
@@ -2,6 +2,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
2
2
|
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
3
3
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
|
|
5
6
|
|
|
6
7
|
export const CountArgs = {
|
|
7
8
|
query: z
|
|
@@ -25,6 +26,20 @@ export class CountTool extends MongoDBToolBase {
|
|
|
25
26
|
|
|
26
27
|
protected async execute({ database, collection, query }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
27
28
|
const provider = await this.ensureConnected();
|
|
29
|
+
|
|
30
|
+
// Check if count operation uses an index if enabled
|
|
31
|
+
if (this.config.indexCheck) {
|
|
32
|
+
await checkIndexUsage(provider, database, collection, "count", async () => {
|
|
33
|
+
return provider.runCommandWithCheck(database, {
|
|
34
|
+
explain: {
|
|
35
|
+
count: collection,
|
|
36
|
+
query,
|
|
37
|
+
},
|
|
38
|
+
verbosity: "queryPlanner",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
const count = await provider.count(database, collection, query);
|
|
29
44
|
|
|
30
45
|
return {
|
|
@@ -4,6 +4,7 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
import { SortDirection } from "mongodb";
|
|
6
6
|
import { EJSON } from "bson";
|
|
7
|
+
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
|
|
7
8
|
|
|
8
9
|
export const FindArgs = {
|
|
9
10
|
filter: z
|
|
@@ -39,6 +40,14 @@ export class FindTool extends MongoDBToolBase {
|
|
|
39
40
|
sort,
|
|
40
41
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
41
42
|
const provider = await this.ensureConnected();
|
|
43
|
+
|
|
44
|
+
// Check if find operation uses an index if enabled
|
|
45
|
+
if (this.config.indexCheck) {
|
|
46
|
+
await checkIndexUsage(provider, database, collection, "find", async () => {
|
|
47
|
+
return provider.find(database, collection, filter, { projection, limit, sort }).explain("queryPlanner");
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
const documents = await provider.find(database, collection, filter, { projection, limit, sort }).toArray();
|
|
43
52
|
|
|
44
53
|
const content: Array<{ text: string; type: "text" }> = [
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
+
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
|
|
5
6
|
|
|
6
7
|
export class UpdateManyTool extends MongoDBToolBase {
|
|
7
8
|
protected name = "update-many";
|
|
@@ -32,6 +33,27 @@ export class UpdateManyTool extends MongoDBToolBase {
|
|
|
32
33
|
upsert,
|
|
33
34
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
34
35
|
const provider = await this.ensureConnected();
|
|
36
|
+
|
|
37
|
+
// Check if update operation uses an index if enabled
|
|
38
|
+
if (this.config.indexCheck) {
|
|
39
|
+
await checkIndexUsage(provider, database, collection, "updateMany", async () => {
|
|
40
|
+
return provider.runCommandWithCheck(database, {
|
|
41
|
+
explain: {
|
|
42
|
+
update: collection,
|
|
43
|
+
updates: [
|
|
44
|
+
{
|
|
45
|
+
q: filter || {},
|
|
46
|
+
u: update,
|
|
47
|
+
upsert: upsert || false,
|
|
48
|
+
multi: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
verbosity: "queryPlanner",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
const result = await provider.updateMany(database, collection, filter, update, {
|
|
36
58
|
upsert,
|
|
37
59
|
});
|
package/src/tools/tool.ts
CHANGED
|
@@ -74,12 +74,12 @@ export abstract class ToolBase {
|
|
|
74
74
|
logger.debug(LogId.toolExecute, "tool", `Executing tool ${this.name}`);
|
|
75
75
|
|
|
76
76
|
const result = await this.execute(...args);
|
|
77
|
-
this.emitToolEvent(startTime, result, ...args);
|
|
77
|
+
await this.emitToolEvent(startTime, result, ...args).catch(() => {});
|
|
78
78
|
return result;
|
|
79
79
|
} catch (error: unknown) {
|
|
80
80
|
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
|
|
81
81
|
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
|
|
82
|
-
this.emitToolEvent(startTime, toolResult, ...args);
|
|
82
|
+
await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
|
|
83
83
|
return toolResult;
|
|
84
84
|
}
|
|
85
85
|
};
|
|
@@ -179,11 +179,11 @@ export abstract class ToolBase {
|
|
|
179
179
|
* @param result - Whether the command succeeded or failed
|
|
180
180
|
* @param args - The arguments passed to the tool
|
|
181
181
|
*/
|
|
182
|
-
private emitToolEvent(
|
|
182
|
+
private async emitToolEvent(
|
|
183
183
|
startTime: number,
|
|
184
184
|
result: CallToolResult,
|
|
185
185
|
...args: Parameters<ToolCallback<typeof this.argsShape>>
|
|
186
|
-
): void {
|
|
186
|
+
): Promise<void> {
|
|
187
187
|
if (!this.telemetry.isTelemetryEnabled()) {
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
@@ -209,6 +209,6 @@ export abstract class ToolBase {
|
|
|
209
209
|
event.properties.project_id = metadata.projectId;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
this.telemetry.emitEvents([event]);
|
|
212
|
+
await this.telemetry.emitEvents([event]);
|
|
213
213
|
}
|
|
214
214
|
}
|