mongodb-mcp-server 0.1.0 → 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.
Files changed (137) hide show
  1. package/.dockerignore +11 -0
  2. package/.github/CODEOWNERS +0 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
  4. package/.github/workflows/check-pr-title.yml +29 -0
  5. package/.github/workflows/{lint.yml → check.yml} +22 -1
  6. package/.github/workflows/code_health.yaml +0 -22
  7. package/.github/workflows/code_health_fork.yaml +7 -63
  8. package/.github/workflows/docker.yaml +57 -0
  9. package/.github/workflows/stale.yml +32 -0
  10. package/.smithery/Dockerfile +30 -0
  11. package/.smithery/smithery.yaml +63 -0
  12. package/.vscode/extensions.json +9 -0
  13. package/.vscode/settings.json +11 -0
  14. package/CONTRIBUTING.md +1 -1
  15. package/Dockerfile +10 -0
  16. package/README.md +173 -35
  17. package/dist/common/atlas/apiClient.js +151 -35
  18. package/dist/common/atlas/apiClient.js.map +1 -1
  19. package/dist/common/atlas/apiClientError.js +38 -5
  20. package/dist/common/atlas/apiClientError.js.map +1 -1
  21. package/dist/common/atlas/cluster.js +66 -0
  22. package/dist/common/atlas/cluster.js.map +1 -0
  23. package/dist/common/atlas/generatePassword.js +9 -0
  24. package/dist/common/atlas/generatePassword.js.map +1 -0
  25. package/dist/helpers/EJsonTransport.js +38 -0
  26. package/dist/helpers/EJsonTransport.js.map +1 -0
  27. package/dist/helpers/connectionOptions.js +10 -0
  28. package/dist/helpers/connectionOptions.js.map +1 -0
  29. package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
  30. package/dist/helpers/packageInfo.js.map +1 -0
  31. package/dist/index.js +23 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/logger.js +7 -0
  34. package/dist/logger.js.map +1 -1
  35. package/dist/server.js +16 -12
  36. package/dist/server.js.map +1 -1
  37. package/dist/session.js +8 -3
  38. package/dist/session.js.map +1 -1
  39. package/dist/telemetry/constants.js +1 -3
  40. package/dist/telemetry/constants.js.map +1 -1
  41. package/dist/telemetry/eventCache.js.map +1 -1
  42. package/dist/telemetry/telemetry.js +126 -47
  43. package/dist/telemetry/telemetry.js.map +1 -1
  44. package/dist/tools/atlas/atlasTool.js +38 -0
  45. package/dist/tools/atlas/atlasTool.js.map +1 -1
  46. package/dist/tools/atlas/create/createDBUser.js +19 -2
  47. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  48. package/dist/tools/atlas/create/createProject.js +5 -1
  49. package/dist/tools/atlas/create/createProject.js.map +1 -1
  50. package/dist/tools/atlas/metadata/connectCluster.js +5 -22
  51. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  52. package/dist/tools/atlas/read/inspectCluster.js +4 -24
  53. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  54. package/dist/tools/atlas/read/listAlerts.js +41 -0
  55. package/dist/tools/atlas/read/listAlerts.js.map +1 -0
  56. package/dist/tools/atlas/read/listClusters.js +9 -18
  57. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  58. package/dist/tools/atlas/read/listProjects.js +3 -1
  59. package/dist/tools/atlas/read/listProjects.js.map +1 -1
  60. package/dist/tools/atlas/tools.js +2 -0
  61. package/dist/tools/atlas/tools.js.map +1 -1
  62. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
  63. package/dist/tools/mongodb/read/count.js +2 -2
  64. package/dist/tools/mongodb/read/count.js.map +1 -1
  65. package/dist/tools/mongodb/tools.js +2 -4
  66. package/dist/tools/mongodb/tools.js.map +1 -1
  67. package/dist/tools/tool.js +38 -6
  68. package/dist/tools/tool.js.map +1 -1
  69. package/eslint.config.js +2 -1
  70. package/{jest.config.ts → jest.config.cjs} +1 -1
  71. package/package.json +11 -9
  72. package/scripts/apply.ts +8 -5
  73. package/scripts/filter.ts +5 -0
  74. package/src/common/atlas/apiClient.ts +190 -38
  75. package/src/common/atlas/apiClientError.ts +58 -7
  76. package/src/common/atlas/cluster.ts +94 -0
  77. package/src/common/atlas/generatePassword.ts +10 -0
  78. package/src/common/atlas/openapi.d.ts +1876 -239
  79. package/src/helpers/EJsonTransport.ts +47 -0
  80. package/src/helpers/connectionOptions.ts +20 -0
  81. package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
  82. package/src/index.ts +27 -3
  83. package/src/logger.ts +8 -0
  84. package/src/server.ts +23 -15
  85. package/src/session.ts +8 -4
  86. package/src/telemetry/constants.ts +2 -3
  87. package/src/telemetry/eventCache.ts +1 -1
  88. package/src/telemetry/telemetry.ts +182 -64
  89. package/src/telemetry/types.ts +1 -1
  90. package/src/tools/atlas/atlasTool.ts +47 -1
  91. package/src/tools/atlas/create/createDBUser.ts +22 -2
  92. package/src/tools/atlas/create/createProject.ts +7 -1
  93. package/src/tools/atlas/metadata/connectCluster.ts +5 -27
  94. package/src/tools/atlas/read/inspectCluster.ts +4 -40
  95. package/src/tools/atlas/read/listAlerts.ts +45 -0
  96. package/src/tools/atlas/read/listClusters.ts +19 -36
  97. package/src/tools/atlas/read/listProjects.ts +4 -2
  98. package/src/tools/atlas/tools.ts +2 -0
  99. package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
  100. package/src/tools/mongodb/read/count.ts +3 -2
  101. package/src/tools/mongodb/tools.ts +2 -4
  102. package/src/tools/tool.ts +45 -8
  103. package/src/types/mongodb-connection-string-url.d.ts +69 -0
  104. package/tests/integration/helpers.ts +41 -2
  105. package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
  106. package/tests/integration/tools/atlas/alerts.test.ts +42 -0
  107. package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
  108. package/tests/integration/tools/atlas/clusters.test.ts +4 -4
  109. package/tests/integration/tools/atlas/dbUsers.test.ts +58 -33
  110. package/tests/integration/tools/atlas/orgs.test.ts +2 -2
  111. package/tests/integration/tools/atlas/projects.test.ts +3 -3
  112. package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
  113. package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
  114. package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
  115. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
  116. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
  117. package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
  118. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
  119. package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
  120. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
  121. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
  122. package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
  123. package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
  124. package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
  125. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
  126. package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
  127. package/tests/integration/tools/mongodb/read/find.test.ts +32 -4
  128. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
  129. package/tests/unit/EJsonTransport.test.ts +71 -0
  130. package/tests/unit/apiClient.test.ts +193 -0
  131. package/tests/unit/session.test.ts +65 -0
  132. package/tests/unit/telemetry.test.ts +222 -80
  133. package/tsconfig.build.json +2 -1
  134. package/dist/packageInfo.js.map +0 -1
  135. package/dist/telemetry/device-id.js +0 -20
  136. package/dist/telemetry/device-id.js.map +0 -1
  137. package/src/telemetry/device-id.ts +0 -21
@@ -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
  }
@@ -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
- organizationId = organizations.results[0].id;
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(
@@ -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
  };
@@ -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
+ }
@@ -2,7 +2,13 @@ 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 { PaginatedClusterDescription20240805, PaginatedOrgGroupView, Group } from "../../../common/atlas/openapi.js";
5
+ import {
6
+ PaginatedClusterDescription20240805,
7
+ PaginatedOrgGroupView,
8
+ Group,
9
+ PaginatedFlexClusters20241113,
10
+ } from "../../../common/atlas/openapi.js";
11
+ import { formatCluster, formatFlexCluster } from "../../../common/atlas/cluster.js";
6
12
 
7
13
  export class ListClustersTool extends AtlasToolBase {
8
14
  protected name = "atlas-list-clusters";
@@ -73,43 +79,20 @@ ${rows}`,
73
79
  };
74
80
  }
75
81
 
76
- private formatClustersTable(project: Group, clusters?: PaginatedClusterDescription20240805): CallToolResult {
77
- if (!clusters?.results?.length) {
82
+ private formatClustersTable(
83
+ project: Group,
84
+ clusters?: PaginatedClusterDescription20240805,
85
+ flexClusters?: PaginatedFlexClusters20241113
86
+ ): CallToolResult {
87
+ // Check if both traditional clusters and flex clusters are absent
88
+ if (!clusters?.results?.length && !flexClusters?.results?.length) {
78
89
  throw new Error("No clusters found.");
79
90
  }
80
- const rows = clusters.results
81
- .map((cluster) => {
82
- const connectionString =
83
- cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard || "N/A";
84
- const mongoDBVersion = cluster.mongoDBVersion || "N/A";
85
- const regionConfigs = (cluster.replicationSpecs || [])
86
- .map(
87
- (replicationSpec) =>
88
- (replicationSpec.regionConfigs || []) as {
89
- providerName: string;
90
- electableSpecs?: {
91
- instanceSize: string;
92
- };
93
- readOnlySpecs?: {
94
- instanceSize: string;
95
- };
96
- }[]
97
- )
98
- .flat()
99
- .map((regionConfig) => {
100
- return {
101
- providerName: regionConfig.providerName,
102
- instanceSize:
103
- regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
104
- };
105
- });
106
-
107
- const instanceSize =
108
- (regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
109
-
110
- const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
111
-
112
- return `${cluster.name} | ${clusterInstanceType} | ${clusterInstanceType == "DEDICATED" ? instanceSize : "N/A"} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`;
91
+ const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
92
+ const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
93
+ const rows = [...formattedClusters, ...formattedFlexClusters]
94
+ .map((formattedCluster) => {
95
+ return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`;
113
96
  })
114
97
  .join("\n");
115
98
  return {
@@ -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
- .reduce((acc, [id, name]) => ({ ...acc, [id]: name }), {});
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
- return `${project.name} | ${project.id} | ${orgs[project.orgId]} | ${project.orgId} | ${createdAt}`;
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
@@ -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
- "The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()"
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 = "Gets the number of documents in a MongoDB collection";
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,
@@ -1,5 +1,4 @@
1
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
2
- // import { ConnectTool } from "./metadata/connect.js";
1
+ import { ConnectTool } from "./metadata/connect.js";
3
2
  import { ListCollectionsTool } from "./metadata/listCollections.js";
4
3
  import { CollectionIndexesTool } from "./read/collectionIndexes.js";
5
4
  import { ListDatabasesTool } from "./metadata/listDatabases.js";
@@ -21,8 +20,7 @@ import { CreateCollectionTool } from "./create/createCollection.js";
21
20
  import { LogsTool } from "./metadata/logs.js";
22
21
 
23
22
  export const MongoDbTools = [
24
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
25
- // ConnectTool,
23
+ ConnectTool,
26
24
  ListCollectionsTool,
27
25
  ListDatabasesTool,
28
26
  CollectionIndexesTool,
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} with args: ${JSON.stringify(args)}`);
74
+ logger.debug(LogId.toolExecute, "tool", `Executing tool ${this.name}`);
47
75
 
48
76
  const result = await this.execute(...args);
49
- await this.emitToolEvent(startTime, result, ...args).catch(() => {});
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
- await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
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 async emitToolEvent(
182
+ private emitToolEvent(
146
183
  startTime: number,
147
184
  result: CallToolResult,
148
185
  ...args: Parameters<ToolCallback<typeof this.argsShape>>
149
- ): Promise<void> {
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
- await this.telemetry.emitEvents([event]);
212
+ this.telemetry.emitEvents([event]);
176
213
  }
177
214
  }
@@ -0,0 +1,69 @@
1
+ declare module "mongodb-connection-string-url" {
2
+ import { URL } from "whatwg-url";
3
+ import { redactConnectionString, ConnectionStringRedactionOptions } from "./redact";
4
+ export { redactConnectionString, ConnectionStringRedactionOptions };
5
+ declare class CaseInsensitiveMap<K extends string = string> extends Map<K, string> {
6
+ delete(name: K): boolean;
7
+ get(name: K): string | undefined;
8
+ has(name: K): boolean;
9
+ set(name: K, value: any): this;
10
+ _normalizeKey(name: any): K;
11
+ }
12
+ declare abstract class URLWithoutHost extends URL {
13
+ abstract get host(): never;
14
+ abstract set host(value: never);
15
+ abstract get hostname(): never;
16
+ abstract set hostname(value: never);
17
+ abstract get port(): never;
18
+ abstract set port(value: never);
19
+ abstract get href(): string;
20
+ abstract set href(value: string);
21
+ }
22
+ export interface ConnectionStringParsingOptions {
23
+ looseValidation?: boolean;
24
+ }
25
+ export declare class ConnectionString extends URLWithoutHost {
26
+ _hosts: string[];
27
+ constructor(uri: string, options?: ConnectionStringParsingOptions);
28
+ get host(): never;
29
+ set host(_ignored: never);
30
+ get hostname(): never;
31
+ set hostname(_ignored: never);
32
+ get port(): never;
33
+ set port(_ignored: never);
34
+ get href(): string;
35
+ set href(_ignored: string);
36
+ get isSRV(): boolean;
37
+ get hosts(): string[];
38
+ set hosts(list: string[]);
39
+ toString(): string;
40
+ clone(): ConnectionString;
41
+ redact(options?: ConnectionStringRedactionOptions): ConnectionString;
42
+ typedSearchParams<T extends {}>(): {
43
+ append(name: keyof T & string, value: any): void;
44
+ delete(name: keyof T & string): void;
45
+ get(name: keyof T & string): string | null;
46
+ getAll(name: keyof T & string): string[];
47
+ has(name: keyof T & string): boolean;
48
+ set(name: keyof T & string, value: any): void;
49
+ keys(): IterableIterator<keyof T & string>;
50
+ values(): IterableIterator<string>;
51
+ entries(): IterableIterator<[keyof T & string, string]>;
52
+ _normalizeKey(name: keyof T & string): string;
53
+ [Symbol.iterator](): IterableIterator<[keyof T & string, string]>;
54
+ sort(): void;
55
+ forEach<THIS_ARG = void>(
56
+ callback: (this: THIS_ARG, value: string, name: string, searchParams: any) => void,
57
+ thisArg?: THIS_ARG | undefined
58
+ ): void;
59
+ readonly [Symbol.toStringTag]: "URLSearchParams";
60
+ };
61
+ }
62
+ export declare class CommaAndColonSeparatedRecord<
63
+ K extends {} = Record<string, unknown>,
64
+ > extends CaseInsensitiveMap<keyof K & string> {
65
+ constructor(from?: string | null);
66
+ toString(): string;
67
+ }
68
+ export default ConnectionString;
69
+ }
@@ -5,7 +5,9 @@ import { UserConfig } from "../../src/config.js";
5
5
  import { McpError } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { Session } from "../../src/session.js";
8
+ import { Telemetry } from "../../src/telemetry/telemetry.js";
8
9
  import { config } from "../../src/config.js";
10
+ import { jest } from "@jest/globals";
9
11
 
10
12
  interface ParameterInfo {
11
13
  name: string;
@@ -56,14 +58,27 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
56
58
  apiClientSecret: userConfig.apiClientSecret,
57
59
  });
58
60
 
61
+ // Mock hasValidAccessToken for tests
62
+ if (userConfig.apiClientId && userConfig.apiClientSecret) {
63
+ const mockFn = jest.fn<() => Promise<boolean>>().mockResolvedValue(true);
64
+ // @ts-expect-error accessing private property for testing
65
+ session.apiClient.validateAccessToken = mockFn;
66
+ }
67
+
68
+ userConfig.telemetry = "disabled";
69
+
70
+ const telemetry = Telemetry.create(session, userConfig);
71
+
59
72
  mcpServer = new Server({
60
73
  session,
61
74
  userConfig,
75
+ telemetry,
62
76
  mcpServer: new McpServer({
63
77
  name: "test-server",
64
78
  version: "5.2.3",
65
79
  }),
66
80
  });
81
+
67
82
  await mcpServer.connect(serverTransport);
68
83
  await mcpClient.connect(clientTransport);
69
84
  });
@@ -117,7 +132,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
117
132
  content = (content as { content: unknown }).content;
118
133
  }
119
134
 
120
- expect(Array.isArray(content)).toBe(true);
135
+ expect(content).toBeArray();
121
136
 
122
137
  const response = content as { type: string; text: string }[];
123
138
  for (const item of response) {
@@ -191,6 +206,7 @@ export function validateToolMetadata(
191
206
  expectDefined(tool);
192
207
  expect(tool.description).toBe(description);
193
208
 
209
+ validateToolAnnotations(tool, name, description);
194
210
  const toolParameters = getParameters(tool);
195
211
  expect(toolParameters).toHaveLength(parameters.length);
196
212
  expect(toolParameters).toIncludeAllMembers(parameters);
@@ -221,6 +237,29 @@ export function validateThrowsForInvalidArguments(
221
237
  }
222
238
 
223
239
  /** Expects the argument being defined and asserts it */
224
- export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
240
+ export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined | null> {
225
241
  expect(arg).toBeDefined();
242
+ expect(arg).not.toBeNull();
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
+ }
226
265
  }