mongodb-mcp-server 0.1.1 → 0.1.2
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/.dockerignore +11 -0
- package/.github/workflows/check-pr-title.yml +29 -0
- package/.github/workflows/docker.yaml +57 -0
- package/.github/workflows/stale.yml +32 -0
- package/.smithery/Dockerfile +30 -0
- package/.smithery/smithery.yaml +63 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +10 -0
- package/README.md +135 -14
- package/dist/common/atlas/apiClient.js +10 -1
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/cluster.js +1 -1
- package/dist/common/atlas/cluster.js.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.js +5 -0
- 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 +115 -78
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/create/createProject.js +5 -1
- package/dist/tools/atlas/create/createProject.js.map +1 -1
- package/dist/tools/atlas/read/listAlerts.js +41 -0
- package/dist/tools/atlas/read/listAlerts.js.map +1 -0
- package/dist/tools/atlas/read/listProjects.js +3 -1
- package/dist/tools/atlas/read/listProjects.js.map +1 -1
- package/dist/tools/atlas/tools.js +2 -0
- package/dist/tools/atlas/tools.js.map +1 -1
- package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +2 -2
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/tool.js +38 -6
- package/dist/tools/tool.js.map +1 -1
- package/package.json +8 -8
- package/scripts/apply.ts +4 -4
- package/scripts/filter.ts +1 -0
- package/src/common/atlas/apiClient.ts +11 -1
- package/src/common/atlas/cluster.ts +1 -2
- package/src/common/atlas/openapi.d.ts +1242 -28
- package/src/index.ts +20 -0
- package/src/logger.ts +6 -0
- package/src/server.ts +1 -1
- package/src/telemetry/telemetry.ts +150 -98
- package/src/telemetry/types.ts +1 -0
- package/src/tools/atlas/create/createProject.ts +7 -1
- package/src/tools/atlas/read/listAlerts.ts +45 -0
- package/src/tools/atlas/read/listProjects.ts +4 -2
- package/src/tools/atlas/tools.ts +2 -0
- package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
- package/src/tools/mongodb/read/count.ts +3 -2
- package/src/tools/tool.ts +45 -8
- package/tests/integration/helpers.ts +23 -0
- package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
- package/tests/integration/tools/atlas/alerts.test.ts +42 -0
- package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
- package/tests/integration/tools/atlas/clusters.test.ts +4 -4
- package/tests/integration/tools/atlas/dbUsers.test.ts +7 -7
- package/tests/integration/tools/atlas/orgs.test.ts +2 -2
- package/tests/integration/tools/atlas/projects.test.ts +3 -3
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
- package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
- package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
- package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
- package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
- package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
- package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
- package/tests/integration/tools/mongodb/read/find.test.ts +6 -6
- package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
- package/tests/unit/EJsonTransport.test.ts +1 -1
- package/tests/unit/session.test.ts +1 -1
- package/tests/unit/telemetry.test.ts +106 -58
- package/tsconfig.build.json +1 -0
- package/tests/integration/telemetry.test.ts +0 -28
package/src/index.ts
CHANGED
|
@@ -31,6 +31,26 @@ try {
|
|
|
31
31
|
|
|
32
32
|
const transport = createEJsonTransport();
|
|
33
33
|
|
|
34
|
+
const shutdown = () => {
|
|
35
|
+
logger.info(LogId.serverCloseRequested, "server", `Server close requested`);
|
|
36
|
+
|
|
37
|
+
server
|
|
38
|
+
.close()
|
|
39
|
+
.then(() => {
|
|
40
|
+
logger.info(LogId.serverClosed, "server", `Server closed successfully`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
})
|
|
43
|
+
.catch((err: unknown) => {
|
|
44
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
process.once("SIGINT", shutdown);
|
|
51
|
+
process.once("SIGTERM", shutdown);
|
|
52
|
+
process.once("SIGQUIT", shutdown);
|
|
53
|
+
|
|
34
54
|
await server.connect(transport);
|
|
35
55
|
} catch (error: unknown) {
|
|
36
56
|
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
|
package/src/logger.ts
CHANGED
|
@@ -9,6 +9,9 @@ export type LogLevel = LoggingMessageNotification["params"]["level"];
|
|
|
9
9
|
export const LogId = {
|
|
10
10
|
serverStartFailure: mongoLogId(1_000_001),
|
|
11
11
|
serverInitialized: mongoLogId(1_000_002),
|
|
12
|
+
serverCloseRequested: mongoLogId(1_000_003),
|
|
13
|
+
serverClosed: mongoLogId(1_000_004),
|
|
14
|
+
serverCloseFailure: mongoLogId(1_000_005),
|
|
12
15
|
|
|
13
16
|
atlasCheckCredentials: mongoLogId(1_001_001),
|
|
14
17
|
atlasDeleteDatabaseUserFailure: mongoLogId(1_001_002),
|
|
@@ -22,6 +25,7 @@ export const LogId = {
|
|
|
22
25
|
telemetryMetadataError: mongoLogId(1_002_005),
|
|
23
26
|
telemetryDeviceIdFailure: mongoLogId(1_002_006),
|
|
24
27
|
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
|
|
28
|
+
telemetryContainerEnvFailure: mongoLogId(1_002_008),
|
|
25
29
|
|
|
26
30
|
toolExecute: mongoLogId(1_003_001),
|
|
27
31
|
toolExecuteFailure: mongoLogId(1_003_002),
|
|
@@ -29,6 +33,8 @@ export const LogId = {
|
|
|
29
33
|
|
|
30
34
|
mongodbConnectFailure: mongoLogId(1_004_001),
|
|
31
35
|
mongodbDisconnectFailure: mongoLogId(1_004_002),
|
|
36
|
+
|
|
37
|
+
toolUpdateFailure: mongoLogId(1_005_001),
|
|
32
38
|
} as const;
|
|
33
39
|
|
|
34
40
|
abstract class LoggerBase {
|
package/src/server.ts
CHANGED
|
@@ -7,114 +7,152 @@ 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
|
+
}
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
31
|
+
async function isContainerized(): Promise<boolean> {
|
|
32
|
+
if (process.env.container) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(fileExists));
|
|
15
37
|
|
|
16
|
-
|
|
38
|
+
return exists.includes(true);
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
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;
|
|
22
42
|
private deviceIdAbortController = new AbortController();
|
|
23
43
|
private eventCache: EventCache;
|
|
24
44
|
private getRawMachineId: () => Promise<string>;
|
|
45
|
+
private getContainerEnv: () => Promise<boolean>;
|
|
46
|
+
private cachedCommonProperties?: CommonProperties;
|
|
47
|
+
private flushing: boolean = false;
|
|
25
48
|
|
|
26
49
|
private constructor(
|
|
27
50
|
private readonly session: Session,
|
|
28
51
|
private readonly userConfig: UserConfig,
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
{
|
|
53
|
+
eventCache,
|
|
54
|
+
getRawMachineId,
|
|
55
|
+
getContainerEnv,
|
|
56
|
+
}: {
|
|
57
|
+
eventCache: EventCache;
|
|
58
|
+
getRawMachineId: () => Promise<string>;
|
|
59
|
+
getContainerEnv: () => Promise<boolean>;
|
|
60
|
+
}
|
|
31
61
|
) {
|
|
32
62
|
this.eventCache = eventCache;
|
|
33
63
|
this.getRawMachineId = getRawMachineId;
|
|
64
|
+
this.getContainerEnv = getContainerEnv;
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
static create(
|
|
37
68
|
session: Session,
|
|
38
69
|
userConfig: UserConfig,
|
|
39
70
|
{
|
|
40
|
-
commonProperties = { ...MACHINE_METADATA },
|
|
41
71
|
eventCache = EventCache.getInstance(),
|
|
42
72
|
getRawMachineId = () => nodeMachineId.machineId(true),
|
|
73
|
+
getContainerEnv = isContainerized,
|
|
43
74
|
}: {
|
|
44
75
|
eventCache?: EventCache;
|
|
45
76
|
getRawMachineId?: () => Promise<string>;
|
|
46
|
-
|
|
77
|
+
getContainerEnv?: () => Promise<boolean>;
|
|
47
78
|
} = {}
|
|
48
79
|
): Telemetry {
|
|
49
|
-
const instance = new Telemetry(session, userConfig,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
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,
|
|
80
|
+
const instance = new Telemetry(session, userConfig, {
|
|
81
|
+
eventCache,
|
|
82
|
+
getRawMachineId,
|
|
83
|
+
getContainerEnv,
|
|
75
84
|
});
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.isBufferingEvents = false;
|
|
86
|
+
return instance;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
public async close(): Promise<void> {
|
|
83
90
|
this.deviceIdAbortController.abort();
|
|
84
|
-
this.
|
|
85
|
-
await this.emitEvents(this.eventCache.getEvents());
|
|
91
|
+
await this.flush();
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/**
|
|
89
95
|
* Emits events through the telemetry pipeline
|
|
90
96
|
* @param events - The events to emit
|
|
91
97
|
*/
|
|
92
|
-
public
|
|
93
|
-
|
|
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
|
-
}
|
|
98
|
+
public emitEvents(events: BaseEvent[]): void {
|
|
99
|
+
void this.flush(events);
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
/**
|
|
106
103
|
* Gets the common properties for events
|
|
107
104
|
* @returns Object containing common properties for all events
|
|
108
105
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
private async getCommonProperties(): Promise<CommonProperties> {
|
|
107
|
+
if (!this.cachedCommonProperties) {
|
|
108
|
+
let deviceId: string | undefined;
|
|
109
|
+
let containerEnv: boolean | undefined;
|
|
110
|
+
try {
|
|
111
|
+
await Promise.all([
|
|
112
|
+
getDeviceId({
|
|
113
|
+
getMachineId: () => this.getRawMachineId(),
|
|
114
|
+
onError: (reason, error) => {
|
|
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;
|
|
118
156
|
}
|
|
119
157
|
|
|
120
158
|
/**
|
|
@@ -135,60 +173,74 @@ export class Telemetry {
|
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
/**
|
|
138
|
-
* Attempts to
|
|
176
|
+
* Attempts to flush events through authenticated and unauthenticated clients
|
|
139
177
|
* Falls back to caching if both attempts fail
|
|
140
178
|
*/
|
|
141
|
-
|
|
142
|
-
if (this.
|
|
143
|
-
|
|
179
|
+
public async flush(events?: BaseEvent[]): Promise<void> {
|
|
180
|
+
if (!this.isTelemetryEnabled()) {
|
|
181
|
+
logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
|
|
144
182
|
return;
|
|
145
183
|
}
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
LogId.telemetryEmitStart,
|
|
152
|
-
"telemetry",
|
|
153
|
-
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
|
|
154
|
-
);
|
|
194
|
+
this.flushing = true;
|
|
155
195
|
|
|
156
|
-
|
|
157
|
-
|
|
196
|
+
try {
|
|
197
|
+
const cachedEvents = this.eventCache.getEvents();
|
|
198
|
+
const allEvents = [...cachedEvents, ...(events ?? [])];
|
|
199
|
+
if (allEvents.length <= 0) {
|
|
200
|
+
this.flushing = false;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
LogId.telemetryEmitStart,
|
|
206
|
+
"telemetry",
|
|
207
|
+
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await this.sendEvents(this.session.apiClient, allEvents);
|
|
158
211
|
this.eventCache.clearEvents();
|
|
159
212
|
logger.debug(
|
|
160
213
|
LogId.telemetryEmitSuccess,
|
|
161
214
|
"telemetry",
|
|
162
215
|
`Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
|
|
163
216
|
);
|
|
164
|
-
|
|
217
|
+
} catch (error: unknown) {
|
|
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
|
+
});
|
|
165
228
|
}
|
|
166
229
|
|
|
167
|
-
|
|
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);
|
|
230
|
+
this.flushing = false;
|
|
173
231
|
}
|
|
174
232
|
|
|
175
233
|
/**
|
|
176
234
|
* Attempts to send events through the provided API client
|
|
177
235
|
*/
|
|
178
|
-
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
} catch (error) {
|
|
188
|
-
return {
|
|
189
|
-
success: false,
|
|
190
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
236
|
+
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<void> {
|
|
237
|
+
const commonProperties = await this.getCommonProperties();
|
|
238
|
+
|
|
239
|
+
await client.sendEvents(
|
|
240
|
+
events.map((event) => ({
|
|
241
|
+
...event,
|
|
242
|
+
properties: { ...commonProperties, ...event.properties },
|
|
243
|
+
}))
|
|
244
|
+
);
|
|
193
245
|
}
|
|
194
246
|
}
|
package/src/telemetry/types.ts
CHANGED
|
@@ -28,7 +28,13 @@ export class CreateProjectTool extends AtlasToolBase {
|
|
|
28
28
|
"No organizations were found in your MongoDB Atlas account. Please create an organization first."
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const firstOrg = organizations.results[0];
|
|
32
|
+
if (!firstOrg?.id) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"The first organization found does not have an ID. Please check your Atlas account."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
organizationId = firstOrg.id;
|
|
32
38
|
assumedOrg = true;
|
|
33
39
|
} catch {
|
|
34
40
|
throw new Error(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
|
+
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
+
|
|
6
|
+
export class ListAlertsTool extends AtlasToolBase {
|
|
7
|
+
protected name = "atlas-list-alerts";
|
|
8
|
+
protected description = "List MongoDB Atlas alerts";
|
|
9
|
+
protected operationType: OperationType = "read";
|
|
10
|
+
protected argsShape = {
|
|
11
|
+
projectId: z.string().describe("Atlas project ID to list alerts for"),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
15
|
+
const data = await this.session.apiClient.listAlerts({
|
|
16
|
+
params: {
|
|
17
|
+
path: {
|
|
18
|
+
groupId: projectId,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!data?.results?.length) {
|
|
24
|
+
return { content: [{ type: "text", text: "No alerts found in your MongoDB Atlas project." }] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Format alerts as a table
|
|
28
|
+
const output =
|
|
29
|
+
`Alert ID | Status | Created | Updated | Type | Comment
|
|
30
|
+
----------|---------|----------|----------|------|--------
|
|
31
|
+
` +
|
|
32
|
+
data.results
|
|
33
|
+
.map((alert) => {
|
|
34
|
+
const created = alert.created ? new Date(alert.created).toLocaleString() : "N/A";
|
|
35
|
+
const updated = alert.updated ? new Date(alert.updated).toLocaleString() : "N/A";
|
|
36
|
+
const comment = alert.acknowledgementComment ?? "N/A";
|
|
37
|
+
return `${alert.id} | ${alert.status} | ${created} | ${updated} | ${alert.eventTypeName} | ${comment}`;
|
|
38
|
+
})
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: output }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -21,7 +21,8 @@ export class ListProjectsTool extends AtlasToolBase {
|
|
|
21
21
|
|
|
22
22
|
const orgs: Record<string, string> = orgData.results
|
|
23
23
|
.map((org) => [org.id || "", org.name])
|
|
24
|
-
.
|
|
24
|
+
.filter(([id]) => id)
|
|
25
|
+
.reduce((acc, [id, name]) => ({ ...acc, [id as string]: name }), {});
|
|
25
26
|
|
|
26
27
|
const data = orgId
|
|
27
28
|
? await this.session.apiClient.listOrganizationProjects({
|
|
@@ -41,7 +42,8 @@ export class ListProjectsTool extends AtlasToolBase {
|
|
|
41
42
|
const rows = data.results
|
|
42
43
|
.map((project) => {
|
|
43
44
|
const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A";
|
|
44
|
-
|
|
45
|
+
const orgName = orgs[project.orgId] ?? "N/A";
|
|
46
|
+
return `${project.name} | ${project.id} | ${orgName} | ${project.orgId} | ${createdAt}`;
|
|
45
47
|
})
|
|
46
48
|
.join("\n");
|
|
47
49
|
const formattedProjects = `Project Name | Project ID | Organization Name | Organization ID | Created At
|
package/src/tools/atlas/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CreateDBUserTool } from "./create/createDBUser.js";
|
|
|
9
9
|
import { CreateProjectTool } from "./create/createProject.js";
|
|
10
10
|
import { ListOrganizationsTool } from "./read/listOrgs.js";
|
|
11
11
|
import { ConnectClusterTool } from "./metadata/connectCluster.js";
|
|
12
|
+
import { ListAlertsTool } from "./read/listAlerts.js";
|
|
12
13
|
|
|
13
14
|
export const AtlasTools = [
|
|
14
15
|
ListClustersTool,
|
|
@@ -22,4 +23,5 @@ export const AtlasTools = [
|
|
|
22
23
|
CreateProjectTool,
|
|
23
24
|
ListOrganizationsTool,
|
|
24
25
|
ConnectClusterTool,
|
|
26
|
+
ListAlertsTool,
|
|
25
27
|
];
|
|
@@ -7,7 +7,6 @@ export class ListDatabasesTool extends MongoDBToolBase {
|
|
|
7
7
|
protected name = "list-databases";
|
|
8
8
|
protected description = "List all databases for a MongoDB connection";
|
|
9
9
|
protected argsShape = {};
|
|
10
|
-
|
|
11
10
|
protected operationType: OperationType = "metadata";
|
|
12
11
|
|
|
13
12
|
protected async execute(): Promise<CallToolResult> {
|
|
@@ -8,13 +8,14 @@ export const CountArgs = {
|
|
|
8
8
|
.record(z.string(), z.unknown())
|
|
9
9
|
.optional()
|
|
10
10
|
.describe(
|
|
11
|
-
"
|
|
11
|
+
"A filter/query parameter. Allows users to filter the documents to count. Matches the syntax of the filter argument of db.collection.count()."
|
|
12
12
|
),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export class CountTool extends MongoDBToolBase {
|
|
16
16
|
protected name = "count";
|
|
17
|
-
protected description =
|
|
17
|
+
protected description =
|
|
18
|
+
"Gets the number of documents in a MongoDB collection using db.collection.count() and query as an optional filter parameter";
|
|
18
19
|
protected argsShape = {
|
|
19
20
|
...DbOperationArgs,
|
|
20
21
|
...CountArgs,
|
package/src/tools/tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z, type ZodRawShape, type ZodNever, AnyZodObject } from "zod";
|
|
2
2
|
import type { McpServer, RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { Session } from "../session.js";
|
|
5
5
|
import logger, { LogId } from "../logger.js";
|
|
6
6
|
import { Telemetry } from "../telemetry/telemetry.js";
|
|
@@ -27,6 +27,34 @@ export abstract class ToolBase {
|
|
|
27
27
|
|
|
28
28
|
protected abstract argsShape: ZodRawShape;
|
|
29
29
|
|
|
30
|
+
protected get annotations(): ToolAnnotations {
|
|
31
|
+
const annotations: ToolAnnotations = {
|
|
32
|
+
title: this.name,
|
|
33
|
+
description: this.description,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
switch (this.operationType) {
|
|
37
|
+
case "read":
|
|
38
|
+
case "metadata":
|
|
39
|
+
annotations.readOnlyHint = true;
|
|
40
|
+
annotations.destructiveHint = false;
|
|
41
|
+
break;
|
|
42
|
+
case "delete":
|
|
43
|
+
annotations.readOnlyHint = false;
|
|
44
|
+
annotations.destructiveHint = true;
|
|
45
|
+
break;
|
|
46
|
+
case "create":
|
|
47
|
+
case "update":
|
|
48
|
+
annotations.destructiveHint = false;
|
|
49
|
+
annotations.readOnlyHint = false;
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return annotations;
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
|
|
31
59
|
|
|
32
60
|
constructor(
|
|
@@ -43,20 +71,20 @@ export abstract class ToolBase {
|
|
|
43
71
|
const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
|
|
44
72
|
const startTime = Date.now();
|
|
45
73
|
try {
|
|
46
|
-
logger.debug(LogId.toolExecute, "tool", `Executing ${this.name}
|
|
74
|
+
logger.debug(LogId.toolExecute, "tool", `Executing tool ${this.name}`);
|
|
47
75
|
|
|
48
76
|
const result = await this.execute(...args);
|
|
49
|
-
|
|
77
|
+
this.emitToolEvent(startTime, result, ...args);
|
|
50
78
|
return result;
|
|
51
79
|
} catch (error: unknown) {
|
|
52
80
|
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
|
|
53
81
|
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
|
|
54
|
-
|
|
82
|
+
this.emitToolEvent(startTime, toolResult, ...args);
|
|
55
83
|
return toolResult;
|
|
56
84
|
}
|
|
57
85
|
};
|
|
58
86
|
|
|
59
|
-
server.tool(this.name, this.description, this.argsShape, callback);
|
|
87
|
+
server.tool(this.name, this.description, this.argsShape, this.annotations, callback);
|
|
60
88
|
|
|
61
89
|
// This is very similar to RegisteredTool.update, but without the bugs around the name.
|
|
62
90
|
// In the upstream update method, the name is captured in the closure and not updated when
|
|
@@ -66,13 +94,22 @@ export abstract class ToolBase {
|
|
|
66
94
|
const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
|
|
67
95
|
const existingTool = tools[this.name];
|
|
68
96
|
|
|
97
|
+
if (!existingTool) {
|
|
98
|
+
logger.warning(LogId.toolUpdateFailure, "tool", `Tool ${this.name} not found in update`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
existingTool.annotations = this.annotations;
|
|
103
|
+
|
|
69
104
|
if (updates.name && updates.name !== this.name) {
|
|
105
|
+
existingTool.annotations.title = updates.name;
|
|
70
106
|
delete tools[this.name];
|
|
71
107
|
this.name = updates.name;
|
|
72
108
|
tools[this.name] = existingTool;
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
if (updates.description) {
|
|
112
|
+
existingTool.annotations.description = updates.description;
|
|
76
113
|
existingTool.description = updates.description;
|
|
77
114
|
this.description = updates.description;
|
|
78
115
|
}
|
|
@@ -142,11 +179,11 @@ export abstract class ToolBase {
|
|
|
142
179
|
* @param result - Whether the command succeeded or failed
|
|
143
180
|
* @param args - The arguments passed to the tool
|
|
144
181
|
*/
|
|
145
|
-
private
|
|
182
|
+
private emitToolEvent(
|
|
146
183
|
startTime: number,
|
|
147
184
|
result: CallToolResult,
|
|
148
185
|
...args: Parameters<ToolCallback<typeof this.argsShape>>
|
|
149
|
-
):
|
|
186
|
+
): void {
|
|
150
187
|
if (!this.telemetry.isTelemetryEnabled()) {
|
|
151
188
|
return;
|
|
152
189
|
}
|
|
@@ -172,6 +209,6 @@ export abstract class ToolBase {
|
|
|
172
209
|
event.properties.project_id = metadata.projectId;
|
|
173
210
|
}
|
|
174
211
|
|
|
175
|
-
|
|
212
|
+
this.telemetry.emitEvents([event]);
|
|
176
213
|
}
|
|
177
214
|
}
|
|
@@ -206,6 +206,7 @@ export function validateToolMetadata(
|
|
|
206
206
|
expectDefined(tool);
|
|
207
207
|
expect(tool.description).toBe(description);
|
|
208
208
|
|
|
209
|
+
validateToolAnnotations(tool, name, description);
|
|
209
210
|
const toolParameters = getParameters(tool);
|
|
210
211
|
expect(toolParameters).toHaveLength(parameters.length);
|
|
211
212
|
expect(toolParameters).toIncludeAllMembers(parameters);
|
|
@@ -240,3 +241,25 @@ export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined |
|
|
|
240
241
|
expect(arg).toBeDefined();
|
|
241
242
|
expect(arg).not.toBeNull();
|
|
242
243
|
}
|
|
244
|
+
|
|
245
|
+
function validateToolAnnotations(tool: ToolInfo, name: string, description: string): void {
|
|
246
|
+
expectDefined(tool.annotations);
|
|
247
|
+
expect(tool.annotations.title).toBe(name);
|
|
248
|
+
expect(tool.annotations.description).toBe(description);
|
|
249
|
+
|
|
250
|
+
switch (tool.operationType) {
|
|
251
|
+
case "read":
|
|
252
|
+
case "metadata":
|
|
253
|
+
expect(tool.annotations.readOnlyHint).toBe(true);
|
|
254
|
+
expect(tool.annotations.destructiveHint).toBe(false);
|
|
255
|
+
break;
|
|
256
|
+
case "delete":
|
|
257
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
258
|
+
expect(tool.annotations.destructiveHint).toBe(true);
|
|
259
|
+
break;
|
|
260
|
+
case "create":
|
|
261
|
+
case "update":
|
|
262
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
263
|
+
expect(tool.annotations.destructiveHint).toBe(false);
|
|
264
|
+
}
|
|
265
|
+
}
|