mongodb-mcp-server 0.1.2 → 0.2.0

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 (270) hide show
  1. package/.github/pull_request_template.md +5 -0
  2. package/.github/workflows/accuracy-tests.yml +55 -0
  3. package/.github/workflows/check.yml +1 -1
  4. package/.github/workflows/code_health.yaml +4 -4
  5. package/.github/workflows/code_health_fork.yaml +0 -14
  6. package/.github/workflows/dependabot_pr.yaml +26 -0
  7. package/.github/workflows/docker.yaml +1 -1
  8. package/.github/workflows/jira-issue.yml +72 -0
  9. package/.smithery/smithery.yaml +10 -0
  10. package/.vscode/extensions.json +1 -1
  11. package/.vscode/launch.json +11 -1
  12. package/.vscode/settings.json +1 -11
  13. package/Dockerfile +1 -0
  14. package/README.md +132 -31
  15. package/dist/common/atlas/accessListUtils.js +36 -0
  16. package/dist/common/atlas/accessListUtils.js.map +1 -0
  17. package/dist/common/atlas/apiClient.js +25 -6
  18. package/dist/common/atlas/apiClient.js.map +1 -1
  19. package/dist/common/atlas/cluster.js +1 -1
  20. package/dist/common/atlas/cluster.js.map +1 -1
  21. package/dist/{config.js → common/config.js} +8 -1
  22. package/dist/common/config.js.map +1 -0
  23. package/dist/{errors.js → common/errors.js} +1 -0
  24. package/dist/common/errors.js.map +1 -0
  25. package/dist/{logger.js → common/logger.js} +20 -19
  26. package/dist/common/logger.js.map +1 -0
  27. package/dist/common/managedTimeout.js +20 -0
  28. package/dist/common/managedTimeout.js.map +1 -0
  29. package/dist/common/packageInfo.js.map +1 -0
  30. package/dist/{session.js → common/session.js} +20 -21
  31. package/dist/common/session.js.map +1 -0
  32. package/dist/common/sessionStore.js +73 -0
  33. package/dist/common/sessionStore.js.map +1 -0
  34. package/dist/helpers/container.js +28 -0
  35. package/dist/helpers/container.js.map +1 -0
  36. package/dist/helpers/generatePassword.js.map +1 -0
  37. package/dist/helpers/indexCheck.js +63 -0
  38. package/dist/helpers/indexCheck.js.map +1 -0
  39. package/dist/index.js +30 -37
  40. package/dist/index.js.map +1 -1
  41. package/dist/server.js +44 -7
  42. package/dist/server.js.map +1 -1
  43. package/dist/telemetry/constants.js +1 -1
  44. package/dist/telemetry/constants.js.map +1 -1
  45. package/dist/telemetry/telemetry.js +86 -116
  46. package/dist/telemetry/telemetry.js.map +1 -1
  47. package/dist/tools/atlas/atlasTool.js +3 -3
  48. package/dist/tools/atlas/atlasTool.js.map +1 -1
  49. package/dist/tools/atlas/connect/connectCluster.js +198 -0
  50. package/dist/tools/atlas/connect/connectCluster.js.map +1 -0
  51. package/dist/tools/atlas/create/createAccessList.js +9 -10
  52. package/dist/tools/atlas/create/createAccessList.js.map +1 -1
  53. package/dist/tools/atlas/create/createDBUser.js +3 -1
  54. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  55. package/dist/tools/atlas/create/createFreeCluster.js +2 -0
  56. package/dist/tools/atlas/create/createFreeCluster.js.map +1 -1
  57. package/dist/tools/atlas/create/createProject.js.map +1 -1
  58. package/dist/tools/atlas/read/inspectAccessList.js.map +1 -1
  59. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  60. package/dist/tools/atlas/read/listAlerts.js.map +1 -1
  61. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  62. package/dist/tools/atlas/read/listDBUsers.js.map +1 -1
  63. package/dist/tools/atlas/read/listOrgs.js.map +1 -1
  64. package/dist/tools/atlas/read/listProjects.js.map +1 -1
  65. package/dist/tools/atlas/tools.js +1 -1
  66. package/dist/tools/atlas/tools.js.map +1 -1
  67. package/dist/tools/mongodb/{metadata → connect}/connect.js +7 -4
  68. package/dist/tools/mongodb/connect/connect.js.map +1 -0
  69. package/dist/tools/mongodb/create/createCollection.js.map +1 -1
  70. package/dist/tools/mongodb/create/createIndex.js +1 -1
  71. package/dist/tools/mongodb/create/createIndex.js.map +1 -1
  72. package/dist/tools/mongodb/create/insertMany.js +1 -1
  73. package/dist/tools/mongodb/create/insertMany.js.map +1 -1
  74. package/dist/tools/mongodb/delete/deleteMany.js +20 -1
  75. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  76. package/dist/tools/mongodb/delete/dropCollection.js.map +1 -1
  77. package/dist/tools/mongodb/delete/dropDatabase.js.map +1 -1
  78. package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
  79. package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
  80. package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
  81. package/dist/tools/mongodb/metadata/explain.js +2 -2
  82. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  83. package/dist/tools/mongodb/metadata/listCollections.js.map +1 -1
  84. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
  85. package/dist/tools/mongodb/metadata/logs.js.map +1 -1
  86. package/dist/tools/mongodb/mongodbTool.js +47 -10
  87. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  88. package/dist/tools/mongodb/read/aggregate.js +10 -1
  89. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  90. package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
  91. package/dist/tools/mongodb/read/count.js +15 -1
  92. package/dist/tools/mongodb/read/count.js.map +1 -1
  93. package/dist/tools/mongodb/read/find.js +14 -4
  94. package/dist/tools/mongodb/read/find.js.map +1 -1
  95. package/dist/tools/mongodb/tools.js +1 -1
  96. package/dist/tools/mongodb/tools.js.map +1 -1
  97. package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
  98. package/dist/tools/mongodb/update/updateMany.js +24 -2
  99. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  100. package/dist/tools/tool.js +12 -9
  101. package/dist/tools/tool.js.map +1 -1
  102. package/dist/transports/base.js +26 -0
  103. package/dist/transports/base.js.map +1 -0
  104. package/dist/{helpers/EJsonTransport.js → transports/stdio.js} +24 -2
  105. package/dist/transports/stdio.js.map +1 -0
  106. package/dist/transports/streamableHttp.js +140 -0
  107. package/dist/transports/streamableHttp.js.map +1 -0
  108. package/eslint.config.js +13 -4
  109. package/package.json +43 -33
  110. package/resources/test-summary-template.html +415 -0
  111. package/scripts/accuracy/generateTestSummary.ts +335 -0
  112. package/scripts/accuracy/runAccuracyTests.sh +45 -0
  113. package/scripts/accuracy/updateAccuracyRunStatus.ts +21 -0
  114. package/src/common/atlas/accessListUtils.ts +54 -0
  115. package/src/common/atlas/apiClient.ts +25 -6
  116. package/src/common/atlas/cluster.ts +1 -1
  117. package/src/{config.ts → common/config.ts} +16 -2
  118. package/src/{errors.ts → common/errors.ts} +1 -0
  119. package/src/{logger.ts → common/logger.ts} +21 -24
  120. package/src/common/managedTimeout.ts +27 -0
  121. package/src/{session.ts → common/session.ts} +24 -26
  122. package/src/common/sessionStore.ts +111 -0
  123. package/src/helpers/container.ts +35 -0
  124. package/src/helpers/indexCheck.ts +83 -0
  125. package/src/index.ts +30 -40
  126. package/src/server.ts +55 -11
  127. package/src/telemetry/constants.ts +1 -1
  128. package/src/telemetry/telemetry.ts +109 -153
  129. package/src/telemetry/types.ts +2 -1
  130. package/src/tools/atlas/atlasTool.ts +4 -4
  131. package/src/tools/atlas/connect/connectCluster.ts +259 -0
  132. package/src/tools/atlas/create/createAccessList.ts +15 -13
  133. package/src/tools/atlas/create/createDBUser.ts +5 -3
  134. package/src/tools/atlas/create/createFreeCluster.ts +4 -2
  135. package/src/tools/atlas/create/createProject.ts +2 -2
  136. package/src/tools/atlas/read/inspectAccessList.ts +2 -2
  137. package/src/tools/atlas/read/inspectCluster.ts +2 -2
  138. package/src/tools/atlas/read/listAlerts.ts +2 -2
  139. package/src/tools/atlas/read/listClusters.ts +2 -2
  140. package/src/tools/atlas/read/listDBUsers.ts +2 -2
  141. package/src/tools/atlas/read/listOrgs.ts +2 -2
  142. package/src/tools/atlas/read/listProjects.ts +2 -2
  143. package/src/tools/atlas/tools.ts +1 -1
  144. package/src/tools/mongodb/{metadata → connect}/connect.ts +12 -9
  145. package/src/tools/mongodb/create/createCollection.ts +2 -2
  146. package/src/tools/mongodb/create/createIndex.ts +3 -3
  147. package/src/tools/mongodb/create/insertMany.ts +3 -3
  148. package/src/tools/mongodb/delete/deleteMany.ts +24 -3
  149. package/src/tools/mongodb/delete/dropCollection.ts +2 -2
  150. package/src/tools/mongodb/delete/dropDatabase.ts +2 -2
  151. package/src/tools/mongodb/metadata/collectionSchema.ts +2 -2
  152. package/src/tools/mongodb/metadata/collectionStorageSize.ts +2 -2
  153. package/src/tools/mongodb/metadata/dbStats.ts +2 -2
  154. package/src/tools/mongodb/metadata/explain.ts +4 -4
  155. package/src/tools/mongodb/metadata/listCollections.ts +2 -2
  156. package/src/tools/mongodb/metadata/listDatabases.ts +2 -2
  157. package/src/tools/mongodb/metadata/logs.ts +2 -2
  158. package/src/tools/mongodb/mongodbTool.ts +60 -14
  159. package/src/tools/mongodb/read/aggregate.ts +14 -3
  160. package/src/tools/mongodb/read/collectionIndexes.ts +2 -2
  161. package/src/tools/mongodb/read/count.ts +19 -3
  162. package/src/tools/mongodb/read/find.ts +20 -6
  163. package/src/tools/mongodb/tools.ts +1 -1
  164. package/src/tools/mongodb/update/renameCollection.ts +2 -2
  165. package/src/tools/mongodb/update/updateMany.ts +28 -4
  166. package/src/tools/tool.ts +23 -18
  167. package/src/transports/base.ts +34 -0
  168. package/src/{helpers/EJsonTransport.ts → transports/stdio.ts} +30 -1
  169. package/src/transports/streamableHttp.ts +178 -0
  170. package/tests/accuracy/aggregate.test.ts +27 -0
  171. package/tests/accuracy/collectionIndexes.test.ts +40 -0
  172. package/tests/accuracy/collectionSchema.test.ts +28 -0
  173. package/tests/accuracy/collectionStorageSize.test.ts +41 -0
  174. package/tests/accuracy/count.test.ts +44 -0
  175. package/tests/accuracy/createCollection.test.ts +46 -0
  176. package/tests/accuracy/createIndex.test.ts +37 -0
  177. package/tests/accuracy/dbStats.test.ts +15 -0
  178. package/tests/accuracy/deleteMany.test.ts +44 -0
  179. package/tests/accuracy/dropCollection.test.ts +74 -0
  180. package/tests/accuracy/dropDatabase.test.ts +41 -0
  181. package/tests/accuracy/explain.test.ts +73 -0
  182. package/tests/accuracy/find.test.ts +114 -0
  183. package/tests/accuracy/insertMany.test.ts +48 -0
  184. package/tests/accuracy/listCollections.test.ts +60 -0
  185. package/tests/accuracy/listDatabases.test.ts +31 -0
  186. package/tests/accuracy/logs.test.ts +28 -0
  187. package/tests/accuracy/renameCollection.test.ts +31 -0
  188. package/tests/accuracy/sdk/accuracyResultStorage/diskStorage.ts +189 -0
  189. package/tests/accuracy/sdk/accuracyResultStorage/getAccuracyResultStorage.ts +11 -0
  190. package/tests/accuracy/sdk/accuracyResultStorage/mongodbStorage.ts +151 -0
  191. package/tests/accuracy/sdk/accuracyResultStorage/resultStorage.ts +117 -0
  192. package/tests/accuracy/sdk/accuracyScorer.ts +93 -0
  193. package/tests/accuracy/sdk/accuracyTestingClient.ts +94 -0
  194. package/tests/accuracy/sdk/agent.ts +56 -0
  195. package/tests/accuracy/sdk/constants.ts +26 -0
  196. package/tests/accuracy/sdk/describeAccuracyTests.ts +126 -0
  197. package/tests/accuracy/sdk/gitInfo.ts +7 -0
  198. package/tests/accuracy/sdk/matcher.ts +193 -0
  199. package/tests/accuracy/sdk/models.ts +95 -0
  200. package/tests/accuracy/test-data-dumps/comics.books.json +417 -0
  201. package/tests/accuracy/test-data-dumps/comics.characters.json +402 -0
  202. package/tests/accuracy/test-data-dumps/mflix.movies.json +496 -0
  203. package/tests/accuracy/test-data-dumps/mflix.shows.json +572 -0
  204. package/tests/accuracy/updateMany.test.ts +42 -0
  205. package/tests/integration/helpers.ts +9 -9
  206. package/tests/integration/indexCheck.test.ts +464 -0
  207. package/tests/integration/server.test.ts +6 -4
  208. package/tests/integration/telemetry.test.ts +29 -0
  209. package/tests/integration/tools/atlas/accessLists.test.ts +22 -2
  210. package/tests/integration/tools/atlas/alerts.test.ts +3 -2
  211. package/tests/integration/tools/atlas/atlasHelpers.ts +3 -0
  212. package/tests/integration/tools/atlas/clusters.test.ts +68 -16
  213. package/tests/integration/tools/atlas/dbUsers.test.ts +14 -1
  214. package/tests/integration/tools/atlas/orgs.test.ts +2 -1
  215. package/tests/integration/tools/atlas/projects.test.ts +4 -3
  216. package/tests/integration/tools/mongodb/{metadata → connect}/connect.test.ts +34 -3
  217. package/tests/integration/tools/mongodb/create/createCollection.test.ts +1 -0
  218. package/tests/integration/tools/mongodb/create/createIndex.test.ts +1 -0
  219. package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -0
  220. package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +1 -0
  221. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
  222. package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +1 -0
  223. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +1 -0
  224. package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +1 -0
  225. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +1 -0
  226. package/tests/integration/tools/mongodb/metadata/explain.test.ts +1 -0
  227. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -0
  228. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +3 -2
  229. package/tests/integration/tools/mongodb/metadata/logs.test.ts +1 -0
  230. package/tests/integration/tools/mongodb/mongodbHelpers.ts +67 -2
  231. package/tests/integration/tools/mongodb/read/aggregate.test.ts +2 -1
  232. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +1 -0
  233. package/tests/integration/tools/mongodb/read/count.test.ts +1 -0
  234. package/tests/integration/tools/mongodb/read/find.test.ts +2 -1
  235. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +1 -0
  236. package/tests/integration/tools/mongodb/update/updateMany.test.ts +1 -0
  237. package/tests/integration/transports/stdio.test.ts +40 -0
  238. package/tests/integration/transports/streamableHttp.test.ts +56 -0
  239. package/tests/matchers/toIncludeSameMembers.test.ts +59 -0
  240. package/tests/matchers/toIncludeSameMembers.ts +12 -0
  241. package/tests/setup.ts +7 -0
  242. package/tests/unit/accessListUtils.test.ts +39 -0
  243. package/tests/unit/accuracyScorer.test.ts +390 -0
  244. package/tests/unit/{apiClient.test.ts → common/apiClient.test.ts} +15 -15
  245. package/tests/unit/common/managedTimeout.test.ts +67 -0
  246. package/tests/unit/{session.test.ts → common/session.test.ts} +7 -12
  247. package/tests/unit/helpers/indexCheck.test.ts +150 -0
  248. package/tests/unit/telemetry.test.ts +99 -137
  249. package/tests/unit/{EJsonTransport.test.ts → transports/stdio.test.ts} +4 -4
  250. package/tests/vitest.d.ts +11 -0
  251. package/tsconfig.json +0 -1
  252. package/{tsconfig.jest.json → tsconfig.test.json} +1 -2
  253. package/vitest.config.ts +41 -0
  254. package/dist/common/atlas/generatePassword.js.map +0 -1
  255. package/dist/config.js.map +0 -1
  256. package/dist/errors.js.map +0 -1
  257. package/dist/helpers/EJsonTransport.js.map +0 -1
  258. package/dist/helpers/packageInfo.js.map +0 -1
  259. package/dist/logger.js.map +0 -1
  260. package/dist/session.js.map +0 -1
  261. package/dist/tools/atlas/metadata/connectCluster.js +0 -100
  262. package/dist/tools/atlas/metadata/connectCluster.js.map +0 -1
  263. package/dist/tools/mongodb/metadata/connect.js.map +0 -1
  264. package/global.d.ts +0 -1
  265. package/jest.config.cjs +0 -22
  266. package/src/tools/atlas/metadata/connectCluster.ts +0 -121
  267. /package/dist/{helpers → common}/packageInfo.js +0 -0
  268. /package/dist/{common/atlas → helpers}/generatePassword.js +0 -0
  269. /package/src/{helpers → common}/packageInfo.ts +0 -0
  270. /package/src/{common/atlas → helpers}/generatePassword.ts +0 -0
@@ -1,158 +1,128 @@
1
- import { Session } from "../session.js";
1
+ import { Session } from "../common/session.js";
2
2
  import { BaseEvent, CommonProperties } from "./types.js";
3
- import { UserConfig } from "../config.js";
4
- import logger, { LogId } from "../logger.js";
3
+ import { UserConfig } from "../common/config.js";
4
+ import logger, { LogId } from "../common/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
8
  import nodeMachineId from "node-machine-id";
9
9
  import { getDeviceId } from "@mongodb-js/device-id";
10
- import fs from "fs/promises";
11
-
12
- async function fileExists(filePath: string): Promise<boolean> {
13
- try {
14
- await fs.access(filePath, fs.constants.F_OK);
15
- return true; // File exists
16
- } catch (e: unknown) {
17
- if (
18
- e instanceof Error &&
19
- (
20
- e as Error & {
21
- code: string;
22
- }
23
- ).code === "ENOENT"
24
- ) {
25
- return false; // File does not exist
26
- }
27
- throw e; // Re-throw unexpected errors
28
- }
29
- }
10
+ import { detectContainerEnv } from "../helpers/container.js";
30
11
 
31
- async function isContainerized(): Promise<boolean> {
32
- if (process.env.container) {
33
- return true;
34
- }
35
-
36
- const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(fileExists));
12
+ type EventResult = {
13
+ success: boolean;
14
+ error?: Error;
15
+ };
37
16
 
38
- return exists.includes(true);
39
- }
17
+ export const DEVICE_ID_TIMEOUT = 3000;
40
18
 
41
19
  export class Telemetry {
20
+ private isBufferingEvents: boolean = true;
21
+ /** Resolves when the setup is complete or a timeout occurs */
22
+ public setupPromise: Promise<[string, boolean]> | undefined;
42
23
  private deviceIdAbortController = new AbortController();
43
24
  private eventCache: EventCache;
44
25
  private getRawMachineId: () => Promise<string>;
45
- private getContainerEnv: () => Promise<boolean>;
46
- private cachedCommonProperties?: CommonProperties;
47
- private flushing: boolean = false;
48
26
 
49
27
  private constructor(
50
28
  private readonly session: Session,
51
29
  private readonly userConfig: UserConfig,
52
- {
53
- eventCache,
54
- getRawMachineId,
55
- getContainerEnv,
56
- }: {
57
- eventCache: EventCache;
58
- getRawMachineId: () => Promise<string>;
59
- getContainerEnv: () => Promise<boolean>;
60
- }
30
+ private readonly commonProperties: CommonProperties,
31
+ { eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
61
32
  ) {
62
33
  this.eventCache = eventCache;
63
34
  this.getRawMachineId = getRawMachineId;
64
- this.getContainerEnv = getContainerEnv;
65
35
  }
66
36
 
67
37
  static create(
68
38
  session: Session,
69
39
  userConfig: UserConfig,
70
40
  {
41
+ commonProperties = { ...MACHINE_METADATA },
71
42
  eventCache = EventCache.getInstance(),
72
43
  getRawMachineId = () => nodeMachineId.machineId(true),
73
- getContainerEnv = isContainerized,
74
44
  }: {
75
45
  eventCache?: EventCache;
76
46
  getRawMachineId?: () => Promise<string>;
77
- getContainerEnv?: () => Promise<boolean>;
47
+ commonProperties?: CommonProperties;
78
48
  } = {}
79
49
  ): Telemetry {
80
- const instance = new Telemetry(session, userConfig, {
81
- eventCache,
82
- getRawMachineId,
83
- getContainerEnv,
84
- });
50
+ const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
85
51
 
52
+ void instance.setup();
86
53
  return instance;
87
54
  }
88
55
 
56
+ private async setup(): Promise<void> {
57
+ if (!this.isTelemetryEnabled()) {
58
+ return;
59
+ }
60
+ this.setupPromise = Promise.all([
61
+ getDeviceId({
62
+ getMachineId: () => this.getRawMachineId(),
63
+ onError: (reason, error) => {
64
+ switch (reason) {
65
+ case "resolutionError":
66
+ logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
67
+ break;
68
+ case "timeout":
69
+ logger.debug(LogId.telemetryDeviceIdTimeout, "telemetry", "Device ID retrieval timed out");
70
+ break;
71
+ case "abort":
72
+ // No need to log in the case of aborts
73
+ break;
74
+ }
75
+ },
76
+ abortSignal: this.deviceIdAbortController.signal,
77
+ }),
78
+ detectContainerEnv(),
79
+ ]);
80
+
81
+ const [deviceId, containerEnv] = await this.setupPromise;
82
+
83
+ this.commonProperties.device_id = deviceId;
84
+ this.commonProperties.is_container_env = containerEnv;
85
+
86
+ this.isBufferingEvents = false;
87
+ }
88
+
89
89
  public async close(): Promise<void> {
90
90
  this.deviceIdAbortController.abort();
91
- await this.flush();
91
+ this.isBufferingEvents = false;
92
+ await this.emitEvents(this.eventCache.getEvents());
92
93
  }
93
94
 
94
95
  /**
95
96
  * Emits events through the telemetry pipeline
96
97
  * @param events - The events to emit
97
98
  */
98
- public emitEvents(events: BaseEvent[]): void {
99
- void this.flush(events);
99
+ public async emitEvents(events: BaseEvent[]): Promise<void> {
100
+ try {
101
+ if (!this.isTelemetryEnabled()) {
102
+ logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
103
+ return;
104
+ }
105
+
106
+ await this.emit(events);
107
+ } catch {
108
+ logger.debug(LogId.telemetryEmitFailure, "telemetry", `Error emitting telemetry events.`);
109
+ }
100
110
  }
101
111
 
102
112
  /**
103
113
  * Gets the common properties for events
104
114
  * @returns Object containing common properties for all events
105
115
  */
106
- private async getCommonProperties(): Promise<CommonProperties> {
107
- if (!this.cachedCommonProperties) {
108
- let deviceId: string | undefined;
109
- let containerEnv: boolean | undefined;
110
- try {
111
- await Promise.all([
112
- getDeviceId({
113
- getMachineId: () => this.getRawMachineId(),
114
- onError: (reason, error) => {
115
- switch (reason) {
116
- case "resolutionError":
117
- logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
118
- break;
119
- case "timeout":
120
- logger.debug(
121
- LogId.telemetryDeviceIdTimeout,
122
- "telemetry",
123
- "Device ID retrieval timed out"
124
- );
125
- break;
126
- case "abort":
127
- // No need to log in the case of aborts
128
- break;
129
- }
130
- },
131
- abortSignal: this.deviceIdAbortController.signal,
132
- }).then((id) => {
133
- deviceId = id;
134
- }),
135
- this.getContainerEnv().then((env) => {
136
- containerEnv = env;
137
- }),
138
- ]);
139
- } catch (error: unknown) {
140
- const err = error instanceof Error ? error : new Error(String(error));
141
- logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", err.message);
142
- }
143
- this.cachedCommonProperties = {
144
- ...MACHINE_METADATA,
145
- mcp_client_version: this.session.agentRunner?.version,
146
- mcp_client_name: this.session.agentRunner?.name,
147
- session_id: this.session.sessionId,
148
- config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
149
- config_connection_string: this.userConfig.connectionString ? "true" : "false",
150
- is_container_env: containerEnv ? "true" : "false",
151
- device_id: deviceId,
152
- };
153
- }
154
-
155
- return this.cachedCommonProperties;
116
+ public getCommonProperties(): CommonProperties {
117
+ return {
118
+ ...this.commonProperties,
119
+ transport: this.userConfig.transport,
120
+ mcp_client_version: this.session.agentRunner?.version,
121
+ mcp_client_name: this.session.agentRunner?.name,
122
+ session_id: this.session.sessionId,
123
+ config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
124
+ config_connection_string: this.userConfig.connectionString ? "true" : "false",
125
+ };
156
126
  }
157
127
 
158
128
  /**
@@ -173,74 +143,60 @@ export class Telemetry {
173
143
  }
174
144
 
175
145
  /**
176
- * Attempts to flush events through authenticated and unauthenticated clients
146
+ * Attempts to emit events through authenticated and unauthenticated clients
177
147
  * Falls back to caching if both attempts fail
178
148
  */
179
- public async flush(events?: BaseEvent[]): Promise<void> {
180
- if (!this.isTelemetryEnabled()) {
181
- logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
182
- return;
183
- }
184
-
185
- if (this.flushing) {
186
- this.eventCache.appendEvents(events ?? []);
187
- process.nextTick(async () => {
188
- // try again if in the middle of a flush
189
- await this.flush();
190
- });
149
+ private async emit(events: BaseEvent[]): Promise<void> {
150
+ if (this.isBufferingEvents) {
151
+ this.eventCache.appendEvents(events);
191
152
  return;
192
153
  }
193
154
 
194
- this.flushing = true;
155
+ const cachedEvents = this.eventCache.getEvents();
156
+ const allEvents = [...cachedEvents, ...events];
195
157
 
196
- try {
197
- const cachedEvents = this.eventCache.getEvents();
198
- const allEvents = [...cachedEvents, ...(events ?? [])];
199
- if (allEvents.length <= 0) {
200
- this.flushing = false;
201
- return;
202
- }
203
-
204
- logger.debug(
205
- LogId.telemetryEmitStart,
206
- "telemetry",
207
- `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
208
- );
158
+ logger.debug(
159
+ LogId.telemetryEmitStart,
160
+ "telemetry",
161
+ `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
162
+ );
209
163
 
210
- await this.sendEvents(this.session.apiClient, allEvents);
164
+ const result = await this.sendEvents(this.session.apiClient, allEvents);
165
+ if (result.success) {
211
166
  this.eventCache.clearEvents();
212
167
  logger.debug(
213
168
  LogId.telemetryEmitSuccess,
214
169
  "telemetry",
215
170
  `Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
216
171
  );
217
- } catch (error: unknown) {
218
- logger.debug(
219
- LogId.telemetryEmitFailure,
220
- "telemetry",
221
- `Error sending event to client: ${error instanceof Error ? error.message : String(error)}`
222
- );
223
- this.eventCache.appendEvents(events ?? []);
224
- process.nextTick(async () => {
225
- // try again
226
- await this.flush();
227
- });
172
+ return;
228
173
  }
229
174
 
230
- this.flushing = false;
175
+ logger.debug(
176
+ LogId.telemetryEmitFailure,
177
+ "telemetry",
178
+ `Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}`
179
+ );
180
+ this.eventCache.appendEvents(events);
231
181
  }
232
182
 
233
183
  /**
234
184
  * Attempts to send events through the provided API client
235
185
  */
236
- private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<void> {
237
- const commonProperties = await this.getCommonProperties();
238
-
239
- await client.sendEvents(
240
- events.map((event) => ({
241
- ...event,
242
- properties: { ...commonProperties, ...event.properties },
243
- }))
244
- );
186
+ private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
187
+ try {
188
+ await client.sendEvents(
189
+ events.map((event) => ({
190
+ ...event,
191
+ properties: { ...this.getCommonProperties(), ...event.properties },
192
+ }))
193
+ );
194
+ return { success: true };
195
+ } catch (error) {
196
+ return {
197
+ success: false,
198
+ error: error instanceof Error ? error : new Error(String(error)),
199
+ };
200
+ }
245
201
  }
246
202
  }
@@ -66,10 +66,11 @@ export type CommonStaticProperties = {
66
66
  */
67
67
  export type CommonProperties = {
68
68
  device_id?: string;
69
+ is_container_env?: boolean;
69
70
  mcp_client_version?: string;
70
71
  mcp_client_name?: string;
72
+ transport?: "stdio" | "http";
71
73
  config_atlas_auth?: TelemetryBoolSet;
72
74
  config_connection_string?: TelemetryBoolSet;
73
75
  session_id?: string;
74
- is_container_env?: TelemetryBoolSet;
75
76
  } & CommonStaticProperties;
@@ -1,12 +1,12 @@
1
1
  import { ToolBase, ToolCategory, TelemetryToolMetadata, ToolArgs } from "../tool.js";
2
2
  import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
- import logger, { LogId } from "../../logger.js";
4
+ import logger, { LogId } from "../../common/logger.js";
5
5
  import { z } from "zod";
6
6
  import { ApiClientError } from "../../common/atlas/apiClientError.js";
7
7
 
8
8
  export abstract class AtlasToolBase extends ToolBase {
9
- protected category: ToolCategory = "atlas";
9
+ public category: ToolCategory = "atlas";
10
10
 
11
11
  protected verifyAllowed(): boolean {
12
12
  if (!this.config.apiClientId || !this.config.apiClientSecret) {
@@ -29,7 +29,7 @@ export abstract class AtlasToolBase extends ToolBase {
29
29
  type: "text",
30
30
  text: `Unable to authenticate with MongoDB Atlas, API error: ${error.message}
31
31
 
32
- Hint: Your API credentials may be invalid, expired or lack permissions.
32
+ Hint: Your API credentials may be invalid, expired or lack permissions.
33
33
  Please check your Atlas API credentials and ensure they have the appropriate permissions.
34
34
  For more information on setting up API keys, visit: https://www.mongodb.com/docs/atlas/configure-api-access/`,
35
35
  },
@@ -44,7 +44,7 @@ For more information on setting up API keys, visit: https://www.mongodb.com/docs
44
44
  {
45
45
  type: "text",
46
46
  text: `Received a Forbidden API Error: ${error.message}
47
-
47
+
48
48
  You don't have sufficient permissions to perform this action in MongoDB Atlas
49
49
  Please ensure your API key has the necessary roles assigned.
50
50
  For more information on Atlas API access roles, visit: https://www.mongodb.com/docs/atlas/api/service-accounts-overview/`,
@@ -0,0 +1,259 @@
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
+ import { generateSecurePassword } from "../../../helpers/generatePassword.js";
6
+ import logger, { LogId } from "../../../common/logger.js";
7
+ import { inspectCluster } from "../../../common/atlas/cluster.js";
8
+ import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
9
+
10
+ const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
11
+
12
+ function sleep(ms: number): Promise<void> {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
15
+
16
+ export class ConnectClusterTool extends AtlasToolBase {
17
+ public name = "atlas-connect-cluster";
18
+ protected description = "Connect to MongoDB Atlas cluster";
19
+ public operationType: OperationType = "connect";
20
+ protected argsShape = {
21
+ projectId: z.string().describe("Atlas project ID"),
22
+ clusterName: z.string().describe("Atlas cluster name"),
23
+ };
24
+
25
+ private async queryConnection(
26
+ projectId: string,
27
+ clusterName: string
28
+ ): Promise<"connected" | "disconnected" | "connecting" | "connected-to-other-cluster" | "unknown"> {
29
+ if (!this.session.connectedAtlasCluster) {
30
+ if (this.session.serviceProvider) {
31
+ return "connected-to-other-cluster";
32
+ }
33
+ return "disconnected";
34
+ }
35
+
36
+ if (
37
+ this.session.connectedAtlasCluster.projectId !== projectId ||
38
+ this.session.connectedAtlasCluster.clusterName !== clusterName
39
+ ) {
40
+ return "connected-to-other-cluster";
41
+ }
42
+
43
+ if (!this.session.serviceProvider) {
44
+ return "connecting";
45
+ }
46
+
47
+ try {
48
+ await this.session.serviceProvider.runCommand("admin", {
49
+ ping: 1,
50
+ });
51
+
52
+ return "connected";
53
+ } catch (err: unknown) {
54
+ const error = err instanceof Error ? err : new Error(String(err));
55
+ logger.debug(
56
+ LogId.atlasConnectFailure,
57
+ "atlas-connect-cluster",
58
+ `error querying cluster: ${error.message}`
59
+ );
60
+ return "unknown";
61
+ }
62
+ }
63
+
64
+ private async prepareClusterConnection(projectId: string, clusterName: string): Promise<string> {
65
+ const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName);
66
+
67
+ if (!cluster.connectionString) {
68
+ throw new Error("Connection string not available");
69
+ }
70
+
71
+ const username = `mcpUser${Math.floor(Math.random() * 100000)}`;
72
+ const password = await generateSecurePassword();
73
+
74
+ const expiryDate = new Date(Date.now() + EXPIRY_MS);
75
+
76
+ const readOnly =
77
+ this.config.readOnly ||
78
+ (this.config.disabledTools?.includes("create") &&
79
+ this.config.disabledTools?.includes("update") &&
80
+ this.config.disabledTools?.includes("delete") &&
81
+ !this.config.disabledTools?.includes("read") &&
82
+ !this.config.disabledTools?.includes("metadata"));
83
+
84
+ const roleName = readOnly ? "readAnyDatabase" : "readWriteAnyDatabase";
85
+
86
+ await this.session.apiClient.createDatabaseUser({
87
+ params: {
88
+ path: {
89
+ groupId: projectId,
90
+ },
91
+ },
92
+ body: {
93
+ databaseName: "admin",
94
+ groupId: projectId,
95
+ roles: [
96
+ {
97
+ roleName,
98
+ databaseName: "admin",
99
+ },
100
+ ],
101
+ scopes: [{ type: "CLUSTER", name: clusterName }],
102
+ username,
103
+ password,
104
+ awsIAMType: "NONE",
105
+ ldapAuthType: "NONE",
106
+ oidcAuthType: "NONE",
107
+ x509Type: "NONE",
108
+ deleteAfterDate: expiryDate.toISOString(),
109
+ },
110
+ });
111
+
112
+ this.session.connectedAtlasCluster = {
113
+ username,
114
+ projectId,
115
+ clusterName,
116
+ expiryDate,
117
+ };
118
+
119
+ const cn = new URL(cluster.connectionString);
120
+ cn.username = username;
121
+ cn.password = password;
122
+ cn.searchParams.set("authSource", "admin");
123
+ return cn.toString();
124
+ }
125
+
126
+ private async connectToCluster(projectId: string, clusterName: string, connectionString: string): Promise<void> {
127
+ let lastError: Error | undefined = undefined;
128
+
129
+ logger.debug(
130
+ LogId.atlasConnectAttempt,
131
+ "atlas-connect-cluster",
132
+ `attempting to connect to cluster: ${this.session.connectedAtlasCluster?.clusterName}`
133
+ );
134
+
135
+ // try to connect for about 5 minutes
136
+ for (let i = 0; i < 600; i++) {
137
+ if (
138
+ !this.session.connectedAtlasCluster ||
139
+ this.session.connectedAtlasCluster.projectId != projectId ||
140
+ this.session.connectedAtlasCluster.clusterName != clusterName
141
+ ) {
142
+ throw new Error("Cluster connection aborted");
143
+ }
144
+
145
+ try {
146
+ lastError = undefined;
147
+
148
+ await this.session.connectToMongoDB(connectionString, this.config.connectOptions);
149
+ break;
150
+ } catch (err: unknown) {
151
+ const error = err instanceof Error ? err : new Error(String(err));
152
+
153
+ lastError = error;
154
+
155
+ logger.debug(
156
+ LogId.atlasConnectFailure,
157
+ "atlas-connect-cluster",
158
+ `error connecting to cluster: ${error.message}`
159
+ );
160
+
161
+ await sleep(500); // wait for 500ms before retrying
162
+ }
163
+ }
164
+
165
+ if (lastError) {
166
+ if (
167
+ this.session.connectedAtlasCluster?.projectId == projectId &&
168
+ this.session.connectedAtlasCluster?.clusterName == clusterName &&
169
+ this.session.connectedAtlasCluster?.username
170
+ ) {
171
+ void this.session.apiClient
172
+ .deleteDatabaseUser({
173
+ params: {
174
+ path: {
175
+ groupId: this.session.connectedAtlasCluster.projectId,
176
+ username: this.session.connectedAtlasCluster.username,
177
+ databaseName: "admin",
178
+ },
179
+ },
180
+ })
181
+ .catch((err: unknown) => {
182
+ const error = err instanceof Error ? err : new Error(String(err));
183
+ logger.debug(
184
+ LogId.atlasConnectFailure,
185
+ "atlas-connect-cluster",
186
+ `error deleting database user: ${error.message}`
187
+ );
188
+ });
189
+ }
190
+ this.session.connectedAtlasCluster = undefined;
191
+ throw lastError;
192
+ }
193
+
194
+ logger.debug(
195
+ LogId.atlasConnectSucceeded,
196
+ "atlas-connect-cluster",
197
+ `connected to cluster: ${this.session.connectedAtlasCluster?.clusterName}`
198
+ );
199
+ }
200
+
201
+ protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
202
+ await ensureCurrentIpInAccessList(this.session.apiClient, projectId);
203
+ for (let i = 0; i < 60; i++) {
204
+ const state = await this.queryConnection(projectId, clusterName);
205
+ switch (state) {
206
+ case "connected": {
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text",
211
+ text: `Connected to cluster "${clusterName}".`,
212
+ },
213
+ ],
214
+ };
215
+ }
216
+ case "connecting":
217
+ case "unknown": {
218
+ break;
219
+ }
220
+ case "connected-to-other-cluster":
221
+ case "disconnected":
222
+ default: {
223
+ await this.session.disconnect();
224
+ const connectionString = await this.prepareClusterConnection(projectId, clusterName);
225
+
226
+ // try to connect for about 5 minutes asynchronously
227
+ void this.connectToCluster(projectId, clusterName, connectionString).catch((err: unknown) => {
228
+ const error = err instanceof Error ? err : new Error(String(err));
229
+ logger.error(
230
+ LogId.atlasConnectFailure,
231
+ "atlas-connect-cluster",
232
+ `error connecting to cluster: ${error.message}`
233
+ );
234
+ });
235
+ break;
236
+ }
237
+ }
238
+
239
+ await sleep(500);
240
+ }
241
+
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text" as const,
246
+ text: `Attempting to connect to cluster "${clusterName}"...`,
247
+ },
248
+ {
249
+ type: "text" as const,
250
+ text: `Warning: Provisioning a user and connecting to the cluster may take more time, please check again in a few seconds.`,
251
+ },
252
+ {
253
+ type: "text" as const,
254
+ text: `Warning: Make sure your IP address was enabled in the allow list setting of the Atlas cluster.`,
255
+ },
256
+ ],
257
+ };
258
+ }
259
+ }