mongodb-mcp-server 0.0.8 → 0.1.1
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/CODEOWNERS +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
- package/.github/workflows/{lint.yml → check.yml} +22 -1
- package/.github/workflows/code_health.yaml +0 -22
- package/.github/workflows/code_health_fork.yaml +7 -63
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +11 -0
- package/README.md +41 -22
- package/dist/common/atlas/apiClient.js +141 -34
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/apiClientError.js +38 -5
- package/dist/common/atlas/apiClientError.js.map +1 -1
- package/dist/common/atlas/cluster.js +66 -0
- package/dist/common/atlas/cluster.js.map +1 -0
- package/dist/common/atlas/generatePassword.js +9 -0
- package/dist/common/atlas/generatePassword.js.map +1 -0
- package/dist/helpers/EJsonTransport.js +38 -0
- package/dist/helpers/EJsonTransport.js.map +1 -0
- package/dist/helpers/connectionOptions.js +10 -0
- package/dist/helpers/connectionOptions.js.map +1 -0
- package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
- package/dist/helpers/packageInfo.js.map +1 -0
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +2 -0
- package/dist/logger.js.map +1 -1
- package/dist/server.js +15 -11
- package/dist/server.js.map +1 -1
- package/dist/session.js +8 -3
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +1 -3
- package/dist/telemetry/constants.js.map +1 -1
- package/dist/telemetry/eventCache.js.map +1 -1
- package/dist/telemetry/telemetry.js +46 -4
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/atlasTool.js +38 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/create/createDBUser.js +19 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -1
- package/dist/tools/atlas/metadata/connectCluster.js +5 -22
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
- package/dist/tools/atlas/read/inspectCluster.js +4 -24
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
- package/dist/tools/atlas/read/listClusters.js +9 -18
- package/dist/tools/atlas/read/listClusters.js.map +1 -1
- package/dist/tools/mongodb/tools.js +2 -4
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/eslint.config.js +2 -1
- package/{jest.config.ts → jest.config.cjs} +1 -1
- package/package.json +4 -2
- package/scripts/apply.ts +4 -1
- package/scripts/filter.ts +4 -0
- package/src/common/atlas/apiClient.ts +179 -37
- package/src/common/atlas/apiClientError.ts +58 -7
- package/src/common/atlas/cluster.ts +95 -0
- package/src/common/atlas/generatePassword.ts +10 -0
- package/src/common/atlas/openapi.d.ts +438 -15
- package/src/helpers/EJsonTransport.ts +47 -0
- package/src/helpers/connectionOptions.ts +20 -0
- package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
- package/src/index.ts +7 -3
- package/src/logger.ts +2 -0
- package/src/server.ts +22 -14
- package/src/session.ts +8 -4
- package/src/telemetry/constants.ts +2 -3
- package/src/telemetry/eventCache.ts +1 -1
- package/src/telemetry/telemetry.ts +72 -6
- package/src/telemetry/types.ts +0 -1
- package/src/tools/atlas/atlasTool.ts +47 -1
- package/src/tools/atlas/create/createDBUser.ts +22 -2
- package/src/tools/atlas/metadata/connectCluster.ts +5 -27
- package/src/tools/atlas/read/inspectCluster.ts +4 -40
- package/src/tools/atlas/read/listClusters.ts +19 -36
- package/src/tools/mongodb/tools.ts +2 -4
- package/src/types/mongodb-connection-string-url.d.ts +69 -0
- package/tests/integration/helpers.ts +18 -2
- package/tests/integration/telemetry.test.ts +28 -0
- package/tests/integration/tools/atlas/dbUsers.test.ts +57 -32
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
- package/tests/integration/tools/mongodb/read/find.test.ts +28 -0
- package/tests/unit/EJsonTransport.test.ts +71 -0
- package/tests/unit/apiClient.test.ts +193 -0
- package/tests/unit/session.test.ts +65 -0
- package/tests/unit/telemetry.test.ts +165 -71
- package/tsconfig.build.json +1 -1
- package/dist/packageInfo.js.map +0 -1
- package/dist/telemetry/device-id.js +0 -20
- package/dist/telemetry/device-id.js.map +0 -1
- package/src/telemetry/device-id.ts +0 -21
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { EJSON } from "bson";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
// This is almost a copy of ReadBuffer from @modelcontextprotocol/sdk
|
|
6
|
+
// but it uses EJSON.parse instead of JSON.parse to handle BSON types
|
|
7
|
+
export class EJsonReadBuffer {
|
|
8
|
+
private _buffer?: Buffer;
|
|
9
|
+
|
|
10
|
+
append(chunk: Buffer): void {
|
|
11
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
readMessage(): JSONRPCMessage | null {
|
|
15
|
+
if (!this._buffer) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const index = this._buffer.indexOf("\n");
|
|
20
|
+
if (index === -1) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
25
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
26
|
+
|
|
27
|
+
// This is using EJSON.parse instead of JSON.parse to handle BSON types
|
|
28
|
+
return JSONRPCMessageSchema.parse(EJSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
clear(): void {
|
|
32
|
+
this._buffer = undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This is a hacky workaround for https://github.com/mongodb-js/mongodb-mcp-server/issues/211
|
|
37
|
+
// The underlying issue is that StdioServerTransport uses JSON.parse to deserialize
|
|
38
|
+
// messages, but that doesn't handle bson types, such as ObjectId when serialized as EJSON.
|
|
39
|
+
//
|
|
40
|
+
// This function creates a StdioServerTransport and replaces the internal readBuffer with EJsonReadBuffer
|
|
41
|
+
// that uses EJson.parse instead.
|
|
42
|
+
export function createEJsonTransport(): StdioServerTransport {
|
|
43
|
+
const server = new StdioServerTransport();
|
|
44
|
+
server["_readBuffer"] = new EJsonReadBuffer();
|
|
45
|
+
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MongoClientOptions } from "mongodb";
|
|
2
|
+
import ConnectionString from "mongodb-connection-string-url";
|
|
3
|
+
|
|
4
|
+
export function setAppNameParamIfMissing({
|
|
5
|
+
connectionString,
|
|
6
|
+
defaultAppName,
|
|
7
|
+
}: {
|
|
8
|
+
connectionString: string;
|
|
9
|
+
defaultAppName?: string;
|
|
10
|
+
}): string {
|
|
11
|
+
const connectionStringUrl = new ConnectionString(connectionString);
|
|
12
|
+
|
|
13
|
+
const searchParams = connectionStringUrl.typedSearchParams<MongoClientOptions>();
|
|
14
|
+
|
|
15
|
+
if (!searchParams.has("appName") && defaultAppName !== undefined) {
|
|
16
|
+
searchParams.set("appName", defaultAppName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return connectionStringUrl.toString();
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
3
|
import logger, { LogId } from "./logger.js";
|
|
5
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
5
|
import { config } from "./config.js";
|
|
7
6
|
import { Session } from "./session.js";
|
|
8
7
|
import { Server } from "./server.js";
|
|
9
|
-
import { packageInfo } from "./packageInfo.js";
|
|
8
|
+
import { packageInfo } from "./helpers/packageInfo.js";
|
|
9
|
+
import { Telemetry } from "./telemetry/telemetry.js";
|
|
10
|
+
import { createEJsonTransport } from "./helpers/EJsonTransport.js";
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
const session = new Session({
|
|
@@ -19,13 +20,16 @@ try {
|
|
|
19
20
|
version: packageInfo.version,
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
const telemetry = Telemetry.create(session, config);
|
|
24
|
+
|
|
22
25
|
const server = new Server({
|
|
23
26
|
mcpServer,
|
|
24
27
|
session,
|
|
28
|
+
telemetry,
|
|
25
29
|
userConfig: config,
|
|
26
30
|
});
|
|
27
31
|
|
|
28
|
-
const transport =
|
|
32
|
+
const transport = createEJsonTransport();
|
|
29
33
|
|
|
30
34
|
await server.connect(transport);
|
|
31
35
|
} catch (error: unknown) {
|
package/src/logger.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const LogId = {
|
|
|
13
13
|
atlasCheckCredentials: mongoLogId(1_001_001),
|
|
14
14
|
atlasDeleteDatabaseUserFailure: mongoLogId(1_001_002),
|
|
15
15
|
atlasConnectFailure: mongoLogId(1_001_003),
|
|
16
|
+
atlasInspectFailure: mongoLogId(1_001_004),
|
|
16
17
|
|
|
17
18
|
telemetryDisabled: mongoLogId(1_002_001),
|
|
18
19
|
telemetryEmitFailure: mongoLogId(1_002_002),
|
|
@@ -20,6 +21,7 @@ export const LogId = {
|
|
|
20
21
|
telemetryEmitSuccess: mongoLogId(1_002_004),
|
|
21
22
|
telemetryMetadataError: mongoLogId(1_002_005),
|
|
22
23
|
telemetryDeviceIdFailure: mongoLogId(1_002_006),
|
|
24
|
+
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
|
|
23
25
|
|
|
24
26
|
toolExecute: mongoLogId(1_003_001),
|
|
25
27
|
toolExecuteFailure: mongoLogId(1_003_002),
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ServerOptions {
|
|
|
16
16
|
session: Session;
|
|
17
17
|
userConfig: UserConfig;
|
|
18
18
|
mcpServer: McpServer;
|
|
19
|
+
telemetry: Telemetry;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export class Server {
|
|
@@ -25,10 +26,10 @@ export class Server {
|
|
|
25
26
|
public readonly userConfig: UserConfig;
|
|
26
27
|
private readonly startTime: number;
|
|
27
28
|
|
|
28
|
-
constructor({ session, mcpServer, userConfig }: ServerOptions) {
|
|
29
|
+
constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
|
|
29
30
|
this.startTime = Date.now();
|
|
30
31
|
this.session = session;
|
|
31
|
-
this.telemetry =
|
|
32
|
+
this.telemetry = telemetry;
|
|
32
33
|
this.mcpServer = mcpServer;
|
|
33
34
|
this.userConfig = userConfig;
|
|
34
35
|
}
|
|
@@ -93,6 +94,7 @@ export class Server {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
async close(): Promise<void> {
|
|
97
|
+
await this.telemetry.close();
|
|
96
98
|
await this.session.close();
|
|
97
99
|
await this.mcpServer.close();
|
|
98
100
|
}
|
|
@@ -102,7 +104,7 @@ export class Server {
|
|
|
102
104
|
* @param command - The server command (e.g., "start", "stop", "register", "deregister")
|
|
103
105
|
* @param additionalProperties - Additional properties specific to the event
|
|
104
106
|
*/
|
|
105
|
-
emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error) {
|
|
107
|
+
private emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error) {
|
|
106
108
|
const event: ServerEvent = {
|
|
107
109
|
timestamp: new Date().toISOString(),
|
|
108
110
|
source: "mdbmcp",
|
|
@@ -172,17 +174,6 @@ export class Server {
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
private async validateConfig(): Promise<void> {
|
|
175
|
-
const isAtlasConfigured = this.userConfig.apiClientId && this.userConfig.apiClientSecret;
|
|
176
|
-
const isMongoDbConfigured = this.userConfig.connectionString;
|
|
177
|
-
if (!isAtlasConfigured && !isMongoDbConfigured) {
|
|
178
|
-
console.error(
|
|
179
|
-
"Either Atlas Client Id or a MongoDB connection string must be configured - you can provide them as environment variables or as startup arguments. \n" +
|
|
180
|
-
"Provide the Atlas credentials as `MDB_MCP_API_CLIENT_ID` and `MDB_MCP_API_CLIENT_SECRET` environment variables or as `--apiClientId` and `--apiClientSecret` startup arguments. \n" +
|
|
181
|
-
"Provide the MongoDB connection string as `MDB_MCP_CONNECTION_STRING` environment variable or as `--connectionString` startup argument."
|
|
182
|
-
);
|
|
183
|
-
throw new Error("Either Atlas Client Id or a MongoDB connection string must be configured");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
177
|
if (this.userConfig.connectionString) {
|
|
187
178
|
try {
|
|
188
179
|
await this.session.connectToMongoDB(this.userConfig.connectionString, this.userConfig.connectOptions);
|
|
@@ -194,5 +185,22 @@ export class Server {
|
|
|
194
185
|
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
|
|
195
186
|
}
|
|
196
187
|
}
|
|
188
|
+
|
|
189
|
+
if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
|
|
190
|
+
try {
|
|
191
|
+
await this.session.apiClient.validateAccessToken();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (this.userConfig.connectionString === undefined) {
|
|
194
|
+
console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error);
|
|
195
|
+
|
|
196
|
+
throw new Error(
|
|
197
|
+
"Failed to connect to MongoDB Atlas instance using the credentials from the config"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
console.error(
|
|
201
|
+
"Failed to validate MongoDB Atlas the credentials from the config, but validated the connection string."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
197
205
|
}
|
|
198
206
|
}
|
package/src/session.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js";
|
|
|
4
4
|
import logger, { LogId } from "./logger.js";
|
|
5
5
|
import EventEmitter from "events";
|
|
6
6
|
import { ConnectOptions } from "./config.js";
|
|
7
|
+
import { setAppNameParamIfMissing } from "./helpers/connectionOptions.js";
|
|
8
|
+
import { packageInfo } from "./helpers/packageInfo.js";
|
|
7
9
|
|
|
8
10
|
export interface SessionOptions {
|
|
9
11
|
apiBaseUrl: string;
|
|
@@ -98,8 +100,12 @@ export class Session extends EventEmitter<{
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
async connectToMongoDB(connectionString: string, connectOptions: ConnectOptions): Promise<void> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
connectionString = setAppNameParamIfMissing({
|
|
104
|
+
connectionString,
|
|
105
|
+
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
|
|
106
|
+
});
|
|
107
|
+
this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, {
|
|
108
|
+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
|
|
103
109
|
productName: "MongoDB MCP",
|
|
104
110
|
readConcern: {
|
|
105
111
|
level: connectOptions.readConcern,
|
|
@@ -110,7 +116,5 @@ export class Session extends EventEmitter<{
|
|
|
110
116
|
},
|
|
111
117
|
timeoutMS: connectOptions.timeoutMS,
|
|
112
118
|
});
|
|
113
|
-
|
|
114
|
-
this.serviceProvider = provider;
|
|
115
119
|
}
|
|
116
120
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { packageInfo } from "../packageInfo.js";
|
|
1
|
+
import { packageInfo } from "../helpers/packageInfo.js";
|
|
2
2
|
import { type CommonStaticProperties } from "./types.js";
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
/**
|
|
5
5
|
* Machine-specific metadata formatted for telemetry
|
|
6
6
|
*/
|
|
7
7
|
export const MACHINE_METADATA: CommonStaticProperties = {
|
|
8
|
-
device_id: getDeviceId(),
|
|
9
8
|
mcp_server_version: packageInfo.version,
|
|
10
9
|
mcp_server_name: packageInfo.mcpServerName,
|
|
11
10
|
platform: process.platform,
|
|
@@ -5,23 +5,84 @@ import logger, { LogId } from "../logger.js";
|
|
|
5
5
|
import { ApiClient } from "../common/atlas/apiClient.js";
|
|
6
6
|
import { MACHINE_METADATA } from "./constants.js";
|
|
7
7
|
import { EventCache } from "./eventCache.js";
|
|
8
|
+
import nodeMachineId from "node-machine-id";
|
|
9
|
+
import { getDeviceId } from "@mongodb-js/device-id";
|
|
8
10
|
|
|
9
11
|
type EventResult = {
|
|
10
12
|
success: boolean;
|
|
11
13
|
error?: Error;
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
export const DEVICE_ID_TIMEOUT = 3000;
|
|
17
|
+
|
|
14
18
|
export class Telemetry {
|
|
15
|
-
private
|
|
19
|
+
private isBufferingEvents: boolean = true;
|
|
20
|
+
/** Resolves when the device ID is retrieved or timeout occurs */
|
|
21
|
+
public deviceIdPromise: Promise<string> | undefined;
|
|
22
|
+
private deviceIdAbortController = new AbortController();
|
|
23
|
+
private eventCache: EventCache;
|
|
24
|
+
private getRawMachineId: () => Promise<string>;
|
|
16
25
|
|
|
17
|
-
constructor(
|
|
26
|
+
private constructor(
|
|
18
27
|
private readonly session: Session,
|
|
19
28
|
private readonly userConfig: UserConfig,
|
|
20
|
-
private readonly
|
|
29
|
+
private readonly commonProperties: CommonProperties,
|
|
30
|
+
{ eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
|
|
21
31
|
) {
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
this.eventCache = eventCache;
|
|
33
|
+
this.getRawMachineId = getRawMachineId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static create(
|
|
37
|
+
session: Session,
|
|
38
|
+
userConfig: UserConfig,
|
|
39
|
+
{
|
|
40
|
+
commonProperties = { ...MACHINE_METADATA },
|
|
41
|
+
eventCache = EventCache.getInstance(),
|
|
42
|
+
getRawMachineId = () => nodeMachineId.machineId(true),
|
|
43
|
+
}: {
|
|
44
|
+
eventCache?: EventCache;
|
|
45
|
+
getRawMachineId?: () => Promise<string>;
|
|
46
|
+
commonProperties?: CommonProperties;
|
|
47
|
+
} = {}
|
|
48
|
+
): Telemetry {
|
|
49
|
+
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
|
|
50
|
+
|
|
51
|
+
void instance.start();
|
|
52
|
+
return instance;
|
|
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,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.commonProperties.device_id = await this.deviceIdPromise;
|
|
78
|
+
|
|
79
|
+
this.isBufferingEvents = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async close(): Promise<void> {
|
|
83
|
+
this.deviceIdAbortController.abort();
|
|
84
|
+
this.isBufferingEvents = false;
|
|
85
|
+
await this.emitEvents(this.eventCache.getEvents());
|
|
25
86
|
}
|
|
26
87
|
|
|
27
88
|
/**
|
|
@@ -78,6 +139,11 @@ export class Telemetry {
|
|
|
78
139
|
* Falls back to caching if both attempts fail
|
|
79
140
|
*/
|
|
80
141
|
private async emit(events: BaseEvent[]): Promise<void> {
|
|
142
|
+
if (this.isBufferingEvents) {
|
|
143
|
+
this.eventCache.appendEvents(events);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
81
147
|
const cachedEvents = this.eventCache.getEvents();
|
|
82
148
|
const allEvents = [...cachedEvents, ...events];
|
|
83
149
|
|
package/src/telemetry/types.ts
CHANGED
|
@@ -53,7 +53,6 @@ export type ServerEvent = TelemetryEvent<ServerEventProperties>;
|
|
|
53
53
|
* Interface for static properties, they can be fetched once and reused.
|
|
54
54
|
*/
|
|
55
55
|
export type CommonStaticProperties = {
|
|
56
|
-
device_id?: string;
|
|
57
56
|
mcp_server_version: string;
|
|
58
57
|
mcp_server_name: string;
|
|
59
58
|
platform: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ToolBase, ToolCategory, TelemetryToolMetadata } from "../tool.js";
|
|
1
|
+
import { ToolBase, ToolCategory, TelemetryToolMetadata, ToolArgs } from "../tool.js";
|
|
2
2
|
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import logger, { LogId } from "../../logger.js";
|
|
4
5
|
import { z } from "zod";
|
|
6
|
+
import { ApiClientError } from "../../common/atlas/apiClientError.js";
|
|
5
7
|
|
|
6
8
|
export abstract class AtlasToolBase extends ToolBase {
|
|
7
9
|
protected category: ToolCategory = "atlas";
|
|
@@ -13,6 +15,50 @@ export abstract class AtlasToolBase extends ToolBase {
|
|
|
13
15
|
return super.verifyAllowed();
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
protected handleError(
|
|
19
|
+
error: unknown,
|
|
20
|
+
args: ToolArgs<typeof this.argsShape>
|
|
21
|
+
): Promise<CallToolResult> | CallToolResult {
|
|
22
|
+
if (error instanceof ApiClientError) {
|
|
23
|
+
const statusCode = error.response.status;
|
|
24
|
+
|
|
25
|
+
if (statusCode === 401) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `Unable to authenticate with MongoDB Atlas, API error: ${error.message}
|
|
31
|
+
|
|
32
|
+
Hint: Your API credentials may be invalid, expired or lack permissions.
|
|
33
|
+
Please check your Atlas API credentials and ensure they have the appropriate permissions.
|
|
34
|
+
For more information on setting up API keys, visit: https://www.mongodb.com/docs/atlas/configure-api-access/`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (statusCode === 403) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `Received a Forbidden API Error: ${error.message}
|
|
47
|
+
|
|
48
|
+
You don't have sufficient permissions to perform this action in MongoDB Atlas
|
|
49
|
+
Please ensure your API key has the necessary roles assigned.
|
|
50
|
+
For more information on Atlas API access roles, visit: https://www.mongodb.com/docs/atlas/api/service-accounts-overview/`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For other types of errors, use the default error handling from the base class
|
|
59
|
+
return super.handleError(error, args);
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
/**
|
|
17
63
|
*
|
|
18
64
|
* Resolves the tool metadata from the arguments passed to the tool
|
|
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
import { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
|
|
6
|
+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
|
|
6
7
|
|
|
7
8
|
export class CreateDBUserTool extends AtlasToolBase {
|
|
8
9
|
protected name = "atlas-create-db-user";
|
|
@@ -11,7 +12,16 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
11
12
|
protected argsShape = {
|
|
12
13
|
projectId: z.string().describe("Atlas project ID"),
|
|
13
14
|
username: z.string().describe("Username for the new user"),
|
|
14
|
-
|
|
15
|
+
// Models will generate overly simplistic passwords like SecurePassword123 or
|
|
16
|
+
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
|
|
17
|
+
// the model not to try and generate anything and instead leave the field unset.
|
|
18
|
+
password: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.nullable()
|
|
22
|
+
.describe(
|
|
23
|
+
"Password for the new user. If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
|
|
24
|
+
),
|
|
15
25
|
roles: z
|
|
16
26
|
.array(
|
|
17
27
|
z.object({
|
|
@@ -34,6 +44,11 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
34
44
|
roles,
|
|
35
45
|
clusters,
|
|
36
46
|
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
47
|
+
const shouldGeneratePassword = !password;
|
|
48
|
+
if (shouldGeneratePassword) {
|
|
49
|
+
password = await generateSecurePassword();
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
const input = {
|
|
38
53
|
groupId: projectId,
|
|
39
54
|
awsIAMType: "NONE",
|
|
@@ -62,7 +77,12 @@ export class CreateDBUserTool extends AtlasToolBase {
|
|
|
62
77
|
});
|
|
63
78
|
|
|
64
79
|
return {
|
|
65
|
-
content: [
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `User "${username}" created successfully${shouldGeneratePassword ? ` with password: \`${password}\`` : ""}.`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
66
86
|
};
|
|
67
87
|
}
|
|
68
88
|
}
|
|
@@ -2,24 +2,15 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from "util";
|
|
5
|
+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
|
|
7
6
|
import logger, { LogId } from "../../../logger.js";
|
|
7
|
+
import { inspectCluster } from "../../../common/atlas/cluster.js";
|
|
8
8
|
|
|
9
9
|
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
|
|
10
10
|
|
|
11
|
-
const randomBytesAsync = promisify(randomBytes);
|
|
12
|
-
|
|
13
|
-
async function generateSecurePassword(): Promise<string> {
|
|
14
|
-
const buf = await randomBytesAsync(16);
|
|
15
|
-
const pass = buf.toString("base64url");
|
|
16
|
-
return pass;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
11
|
function sleep(ms: number): Promise<void> {
|
|
20
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
13
|
}
|
|
22
|
-
|
|
23
14
|
export class ConnectClusterTool extends AtlasToolBase {
|
|
24
15
|
protected name = "atlas-connect-cluster";
|
|
25
16
|
protected description = "Connect to MongoDB Atlas cluster";
|
|
@@ -32,22 +23,9 @@ export class ConnectClusterTool extends AtlasToolBase {
|
|
|
32
23
|
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
33
24
|
await this.session.disconnect();
|
|
34
25
|
|
|
35
|
-
const cluster = await this.session.apiClient
|
|
36
|
-
params: {
|
|
37
|
-
path: {
|
|
38
|
-
groupId: projectId,
|
|
39
|
-
clusterName,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!cluster) {
|
|
45
|
-
throw new Error("Cluster not found");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const baseConnectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard;
|
|
26
|
+
const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName);
|
|
49
27
|
|
|
50
|
-
if (!
|
|
28
|
+
if (!cluster.connectionString) {
|
|
51
29
|
throw new Error("Connection string not available");
|
|
52
30
|
}
|
|
53
31
|
|
|
@@ -99,7 +77,7 @@ export class ConnectClusterTool extends AtlasToolBase {
|
|
|
99
77
|
expiryDate,
|
|
100
78
|
};
|
|
101
79
|
|
|
102
|
-
const cn = new URL(
|
|
80
|
+
const cn = new URL(cluster.connectionString);
|
|
103
81
|
cn.username = username;
|
|
104
82
|
cn.password = password;
|
|
105
83
|
cn.searchParams.set("authSource", "admin");
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Cluster, inspectCluster } from "../../../common/atlas/cluster.js";
|
|
6
6
|
|
|
7
7
|
export class InspectClusterTool extends AtlasToolBase {
|
|
8
8
|
protected name = "atlas-inspect-cluster";
|
|
@@ -14,55 +14,19 @@ export class InspectClusterTool extends AtlasToolBase {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
|
|
17
|
-
const cluster = await this.session.apiClient
|
|
18
|
-
params: {
|
|
19
|
-
path: {
|
|
20
|
-
groupId: projectId,
|
|
21
|
-
clusterName,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
});
|
|
17
|
+
const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName);
|
|
25
18
|
|
|
26
19
|
return this.formatOutput(cluster);
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
private formatOutput(
|
|
30
|
-
if (!cluster) {
|
|
31
|
-
throw new Error("Cluster not found");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const regionConfigs = (cluster.replicationSpecs || [])
|
|
35
|
-
.map(
|
|
36
|
-
(replicationSpec) =>
|
|
37
|
-
(replicationSpec.regionConfigs || []) as {
|
|
38
|
-
providerName: string;
|
|
39
|
-
electableSpecs?: {
|
|
40
|
-
instanceSize: string;
|
|
41
|
-
};
|
|
42
|
-
readOnlySpecs?: {
|
|
43
|
-
instanceSize: string;
|
|
44
|
-
};
|
|
45
|
-
}[]
|
|
46
|
-
)
|
|
47
|
-
.flat()
|
|
48
|
-
.map((regionConfig) => {
|
|
49
|
-
return {
|
|
50
|
-
providerName: regionConfig.providerName,
|
|
51
|
-
instanceSize: regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const instanceSize = (regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
|
|
56
|
-
|
|
57
|
-
const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
|
|
58
|
-
|
|
22
|
+
private formatOutput(formattedCluster: Cluster): CallToolResult {
|
|
59
23
|
return {
|
|
60
24
|
content: [
|
|
61
25
|
{
|
|
62
26
|
type: "text",
|
|
63
27
|
text: `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
|
|
64
28
|
----------------|----------------|----------------|----------------|----------------|----------------
|
|
65
|
-
${
|
|
29
|
+
${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`,
|
|
66
30
|
},
|
|
67
31
|
],
|
|
68
32
|
};
|