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.
Files changed (90) hide show
  1. package/.github/CODEOWNERS +0 -2
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
  3. package/.github/workflows/{lint.yml → check.yml} +22 -1
  4. package/.github/workflows/code_health.yaml +0 -22
  5. package/.github/workflows/code_health_fork.yaml +7 -63
  6. package/.vscode/extensions.json +9 -0
  7. package/.vscode/settings.json +11 -0
  8. package/README.md +41 -22
  9. package/dist/common/atlas/apiClient.js +141 -34
  10. package/dist/common/atlas/apiClient.js.map +1 -1
  11. package/dist/common/atlas/apiClientError.js +38 -5
  12. package/dist/common/atlas/apiClientError.js.map +1 -1
  13. package/dist/common/atlas/cluster.js +66 -0
  14. package/dist/common/atlas/cluster.js.map +1 -0
  15. package/dist/common/atlas/generatePassword.js +9 -0
  16. package/dist/common/atlas/generatePassword.js.map +1 -0
  17. package/dist/helpers/EJsonTransport.js +38 -0
  18. package/dist/helpers/EJsonTransport.js.map +1 -0
  19. package/dist/helpers/connectionOptions.js +10 -0
  20. package/dist/helpers/connectionOptions.js.map +1 -0
  21. package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
  22. package/dist/helpers/packageInfo.js.map +1 -0
  23. package/dist/index.js +6 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/logger.js +2 -0
  26. package/dist/logger.js.map +1 -1
  27. package/dist/server.js +15 -11
  28. package/dist/server.js.map +1 -1
  29. package/dist/session.js +8 -3
  30. package/dist/session.js.map +1 -1
  31. package/dist/telemetry/constants.js +1 -3
  32. package/dist/telemetry/constants.js.map +1 -1
  33. package/dist/telemetry/eventCache.js.map +1 -1
  34. package/dist/telemetry/telemetry.js +46 -4
  35. package/dist/telemetry/telemetry.js.map +1 -1
  36. package/dist/tools/atlas/atlasTool.js +38 -0
  37. package/dist/tools/atlas/atlasTool.js.map +1 -1
  38. package/dist/tools/atlas/create/createDBUser.js +19 -2
  39. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  40. package/dist/tools/atlas/metadata/connectCluster.js +5 -22
  41. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  42. package/dist/tools/atlas/read/inspectCluster.js +4 -24
  43. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  44. package/dist/tools/atlas/read/listClusters.js +9 -18
  45. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  46. package/dist/tools/mongodb/tools.js +2 -4
  47. package/dist/tools/mongodb/tools.js.map +1 -1
  48. package/eslint.config.js +2 -1
  49. package/{jest.config.ts → jest.config.cjs} +1 -1
  50. package/package.json +4 -2
  51. package/scripts/apply.ts +4 -1
  52. package/scripts/filter.ts +4 -0
  53. package/src/common/atlas/apiClient.ts +179 -37
  54. package/src/common/atlas/apiClientError.ts +58 -7
  55. package/src/common/atlas/cluster.ts +95 -0
  56. package/src/common/atlas/generatePassword.ts +10 -0
  57. package/src/common/atlas/openapi.d.ts +438 -15
  58. package/src/helpers/EJsonTransport.ts +47 -0
  59. package/src/helpers/connectionOptions.ts +20 -0
  60. package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
  61. package/src/index.ts +7 -3
  62. package/src/logger.ts +2 -0
  63. package/src/server.ts +22 -14
  64. package/src/session.ts +8 -4
  65. package/src/telemetry/constants.ts +2 -3
  66. package/src/telemetry/eventCache.ts +1 -1
  67. package/src/telemetry/telemetry.ts +72 -6
  68. package/src/telemetry/types.ts +0 -1
  69. package/src/tools/atlas/atlasTool.ts +47 -1
  70. package/src/tools/atlas/create/createDBUser.ts +22 -2
  71. package/src/tools/atlas/metadata/connectCluster.ts +5 -27
  72. package/src/tools/atlas/read/inspectCluster.ts +4 -40
  73. package/src/tools/atlas/read/listClusters.ts +19 -36
  74. package/src/tools/mongodb/tools.ts +2 -4
  75. package/src/types/mongodb-connection-string-url.d.ts +69 -0
  76. package/tests/integration/helpers.ts +18 -2
  77. package/tests/integration/telemetry.test.ts +28 -0
  78. package/tests/integration/tools/atlas/dbUsers.test.ts +57 -32
  79. package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
  80. package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
  81. package/tests/integration/tools/mongodb/read/find.test.ts +28 -0
  82. package/tests/unit/EJsonTransport.test.ts +71 -0
  83. package/tests/unit/apiClient.test.ts +193 -0
  84. package/tests/unit/session.test.ts +65 -0
  85. package/tests/unit/telemetry.test.ts +165 -71
  86. package/tsconfig.build.json +1 -1
  87. package/dist/packageInfo.js.map +0 -1
  88. package/dist/telemetry/device-id.js +0 -20
  89. package/dist/telemetry/device-id.js.map +0 -1
  90. 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
+ }
@@ -1,4 +1,4 @@
1
- import packageJson from "../package.json" with { type: "json" };
1
+ import packageJson from "../../package.json" with { type: "json" };
2
2
 
3
3
  export const packageInfo = {
4
4
  version: packageJson.version,
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 = new StdioServerTransport();
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 = new Telemetry(session, userConfig);
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
- const provider = await NodeDriverServiceProvider.connect(connectionString, {
102
- productDocsLink: "https://docs.mongodb.com/todo-mcp",
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
- import { getDeviceId } from "./device-id.js";
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,
@@ -1,5 +1,5 @@
1
- import { BaseEvent } from "./types.js";
2
1
  import { LRUCache } from "lru-cache";
2
+ import { BaseEvent } from "./types.js";
3
3
 
4
4
  /**
5
5
  * Singleton class for in-memory telemetry event caching
@@ -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 readonly commonProperties: CommonProperties;
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 eventCache: EventCache = EventCache.getInstance()
29
+ private readonly commonProperties: CommonProperties,
30
+ { eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
21
31
  ) {
22
- this.commonProperties = {
23
- ...MACHINE_METADATA,
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
 
@@ -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
- password: z.string().describe("Password for the new user"),
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: [{ type: "text", text: `User "${username}" created sucessfully.` }],
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 { randomBytes } from "crypto";
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.getCluster({
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 (!baseConnectionString) {
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(baseConnectionString);
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 { ClusterDescription20240805 } from "../../../common/atlas/openapi.js";
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.getCluster({
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(cluster?: ClusterDescription20240805): CallToolResult {
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
- ${cluster.name} | ${clusterInstanceType} | ${clusterInstanceType == "DEDICATED" ? instanceSize : "N/A"} | ${cluster.stateName} | ${cluster.mongoDBVersion || "N/A"} | ${cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard || "N/A"}`,
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
  };