Skip to content

Commit 2d1586a

Browse files
fmenezesblva
andauthored
fix: creating multiple users on atlas connect [MCP-280] (#721)
Co-authored-by: Bianca Lisle <40155621+blva@users.noreply.github.com>
1 parent 2a499f4 commit 2d1586a

File tree

5 files changed

+201
-35
lines changed

5 files changed

+201
-35
lines changed

src/tools/atlas/connect/connectCluster.ts

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,39 @@ export class ConnectClusterTool extends AtlasToolBase {
218218
const ipAccessListUpdated = await ensureCurrentIpInAccessList(this.session.apiClient, projectId);
219219
let createdUser = false;
220220

221+
const state = this.queryConnection(projectId, clusterName);
222+
switch (state) {
223+
case "connected-to-other-cluster":
224+
case "disconnected": {
225+
await this.session.disconnect();
226+
227+
const { connectionString, atlas } = await this.prepareClusterConnection(
228+
projectId,
229+
clusterName,
230+
connectionType
231+
);
232+
233+
createdUser = true;
234+
235+
// try to connect for about 5 minutes asynchronously
236+
void this.connectToCluster(connectionString, atlas).catch((err: unknown) => {
237+
const error = err instanceof Error ? err : new Error(String(err));
238+
this.session.logger.error({
239+
id: LogId.atlasConnectFailure,
240+
context: "atlas-connect-cluster",
241+
message: `error connecting to cluster: ${error.message}`,
242+
});
243+
});
244+
break;
245+
}
246+
case "connecting":
247+
case "connected":
248+
case "unknown":
249+
default: {
250+
break;
251+
}
252+
}
253+
221254
for (let i = 0; i < 60; i++) {
222255
const state = this.queryConnection(projectId, clusterName);
223256
switch (state) {
@@ -246,34 +279,15 @@ export class ConnectClusterTool extends AtlasToolBase {
246279
return { content };
247280
}
248281
case "connecting":
249-
case "unknown": {
250-
break;
251-
}
282+
case "unknown":
252283
case "connected-to-other-cluster":
253284
case "disconnected":
254285
default: {
255-
await this.session.disconnect();
256-
const { connectionString, atlas } = await this.prepareClusterConnection(
257-
projectId,
258-
clusterName,
259-
connectionType
260-
);
261-
262-
createdUser = true;
263-
// try to connect for about 5 minutes asynchronously
264-
void this.connectToCluster(connectionString, atlas).catch((err: unknown) => {
265-
const error = err instanceof Error ? err : new Error(String(err));
266-
this.session.logger.error({
267-
id: LogId.atlasConnectFailure,
268-
context: "atlas-connect-cluster",
269-
message: `error connecting to cluster: ${error.message}`,
270-
});
271-
});
272286
break;
273287
}
274288
}
275289

276-
await sleep(500);
290+
await sleep(500); // wait 500ms before checking the connection state again
277291
}
278292

279293
const content: CallToolResult["content"] = [

tests/integration/tools/atlas/atlasHelpers.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,16 @@ interface ProjectTestArgs {
3333
getIpAddress: () => string;
3434
}
3535

36+
interface ClusterTestArgs {
37+
getProjectId: () => string;
38+
getIpAddress: () => string;
39+
getClusterName: () => string;
40+
}
41+
3642
type ProjectTestFunction = (args: ProjectTestArgs) => void;
3743

44+
type ClusterTestFunction = (args: ClusterTestArgs) => void;
45+
3846
export function withCredentials(integration: IntegrationTest, fn: IntegrationTestFunction): SuiteCollector<object> {
3947
const describeFn =
4048
!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length
@@ -71,25 +79,25 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
7179
}
7280
});
7381

74-
afterAll(() => {
82+
afterAll(async () => {
7583
if (!projectId) {
7684
return;
7785
}
7886

7987
const apiClient = integration.mcpServer().session.apiClient;
8088

81-
// send the delete request and ignore errors
82-
apiClient
83-
.deleteGroup({
89+
try {
90+
await apiClient.deleteGroup({
8491
params: {
8592
path: {
8693
groupId: projectId,
8794
},
8895
},
89-
})
90-
.catch((error) => {
91-
console.log("Failed to delete project:", error);
9296
});
97+
} catch (error) {
98+
// send the delete request and ignore errors
99+
console.log("Failed to delete group:", error);
100+
}
93101
});
94102

95103
const args = {
@@ -101,10 +109,12 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
101109
});
102110
}
103111

104-
export const randomId = new ObjectId().toString();
112+
export function randomId(): string {
113+
return new ObjectId().toString();
114+
}
105115

106116
async function createGroup(apiClient: ApiClient): Promise<Group & Required<Pick<Group, "id">>> {
107-
const projectName: string = `testProj-` + randomId;
117+
const projectName: string = `testProj-` + randomId();
108118

109119
const orgs = await apiClient.listOrgs();
110120
if (!orgs?.results?.length || !orgs.results[0]?.id) {
@@ -229,3 +239,78 @@ export async function waitCluster(
229239
`Cluster wait timeout: ${clusterName} did not meet condition within ${maxPollingIterations} iterations`
230240
);
231241
}
242+
243+
export function withCluster(integration: IntegrationTest, fn: ClusterTestFunction): SuiteCollector<object> {
244+
return withProject(integration, ({ getProjectId, getIpAddress }) => {
245+
describe("with cluster", () => {
246+
const clusterName: string = `test-cluster-${randomId()}`;
247+
248+
beforeAll(async () => {
249+
const apiClient = integration.mcpServer().session.apiClient;
250+
251+
const projectId = getProjectId();
252+
253+
const input = {
254+
groupId: projectId,
255+
name: clusterName,
256+
clusterType: "REPLICASET",
257+
replicationSpecs: [
258+
{
259+
zoneName: "Zone 1",
260+
regionConfigs: [
261+
{
262+
providerName: "TENANT",
263+
backingProviderName: "AWS",
264+
regionName: "US_EAST_1",
265+
electableSpecs: {
266+
instanceSize: "M0",
267+
},
268+
},
269+
],
270+
},
271+
],
272+
terminationProtectionEnabled: false,
273+
} as unknown as ClusterDescription20240805;
274+
275+
await apiClient.createCluster({
276+
params: {
277+
path: {
278+
groupId: projectId,
279+
},
280+
},
281+
body: input,
282+
});
283+
284+
await waitCluster(integration.mcpServer().session, projectId, clusterName, (cluster) => {
285+
return cluster.stateName === "IDLE";
286+
});
287+
});
288+
289+
afterAll(async () => {
290+
const apiClient = integration.mcpServer().session.apiClient;
291+
292+
try {
293+
// send the delete request and ignore errors
294+
await apiClient.deleteCluster({
295+
params: {
296+
path: {
297+
groupId: getProjectId(),
298+
clusterName,
299+
},
300+
},
301+
});
302+
} catch (error) {
303+
console.log("Failed to delete cluster:", error);
304+
}
305+
});
306+
307+
const args = {
308+
getProjectId: (): string => getProjectId(),
309+
getIpAddress: (): string => getIpAddress(),
310+
getClusterName: (): string => clusterName,
311+
};
312+
313+
fn(args);
314+
});
315+
});
316+
}

tests/integration/tools/atlas/clusters.test.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import type { Session } from "../../../../src/common/session.js";
22
import { expectDefined, getResponseContent } from "../../helpers.js";
3-
import { describeWithAtlas, withProject, randomId, deleteCluster, waitCluster, sleep } from "./atlasHelpers.js";
4-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
3+
import {
4+
describeWithAtlas,
5+
withProject,
6+
withCluster,
7+
randomId,
8+
deleteCluster,
9+
waitCluster,
10+
sleep,
11+
} from "./atlasHelpers.js";
12+
import { afterAll, beforeAll, describe, expect, it, vitest } from "vitest";
513

614
describeWithAtlas("clusters", (integration) => {
715
withProject(integration, ({ getProjectId, getIpAddress }) => {
8-
const clusterName = "ClusterTest-" + randomId;
16+
const clusterName = "ClusterTest-" + randomId();
917

1018
afterAll(async () => {
1119
const projectId = getProjectId();
@@ -142,6 +150,11 @@ describeWithAtlas("clusters", (integration) => {
142150
});
143151

144152
it("connects to cluster", async () => {
153+
const createDatabaseUserSpy = vitest.spyOn(
154+
integration.mcpServer().session.apiClient,
155+
"createDatabaseUser"
156+
);
157+
145158
const projectId = getProjectId();
146159
const connectionType = "standard";
147160
let connected = false;
@@ -158,6 +171,8 @@ describeWithAtlas("clusters", (integration) => {
158171
if (content.includes(`Connected to cluster "${clusterName}"`)) {
159172
connected = true;
160173

174+
expect(createDatabaseUserSpy).toHaveBeenCalledTimes(1);
175+
161176
// assert that some of the element s have the message
162177
expect(content).toContain(
163178
"Note: A temporary user has been created to enable secure connection to the cluster. For more information, see https://dochub.mongodb.org/core/mongodb-mcp-server-tools-considerations"
@@ -172,6 +187,58 @@ describeWithAtlas("clusters", (integration) => {
172187
expect(connected).toBe(true);
173188
});
174189

190+
describe("when connected", () => {
191+
withCluster(
192+
integration,
193+
({ getProjectId: getSecondaryProjectId, getClusterName: getSecondaryClusterName }) => {
194+
beforeAll(async () => {
195+
let connected = false;
196+
for (let i = 0; i < 10; i++) {
197+
const response = await integration.mcpClient().callTool({
198+
name: "atlas-connect-cluster",
199+
arguments: {
200+
projectId: getSecondaryProjectId(),
201+
clusterName: getSecondaryClusterName(),
202+
connectionType: "standard",
203+
},
204+
});
205+
206+
const content = getResponseContent(response.content);
207+
208+
if (content.includes(`Connected to cluster "${getSecondaryClusterName()}"`)) {
209+
connected = true;
210+
break;
211+
}
212+
213+
await sleep(500);
214+
}
215+
216+
if (!connected) {
217+
throw new Error("Could not connect to cluster before tests");
218+
}
219+
});
220+
221+
it("disconnects and deletes the database user before connecting to another cluster", async () => {
222+
const deleteDatabaseUserSpy = vitest.spyOn(
223+
integration.mcpServer().session.apiClient,
224+
"deleteDatabaseUser"
225+
);
226+
227+
await integration.mcpClient().callTool({
228+
name: "atlas-connect-cluster",
229+
arguments: {
230+
projectId: getProjectId(),
231+
clusterName: clusterName,
232+
connectionType: "standard",
233+
},
234+
});
235+
236+
expect(deleteDatabaseUserSpy).toHaveBeenCalledTimes(1);
237+
});
238+
}
239+
);
240+
});
241+
175242
describe("when not connected", () => {
176243
it("prompts for atlas-connect-cluster when querying mongodb", async () => {
177244
const response = await integration.mcpClient().callTool({

tests/integration/tools/atlas/dbUsers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describeWithAtlas("db users", (integration) => {
88
withProject(integration, ({ getProjectId }) => {
99
let userName: string;
1010
beforeEach(() => {
11-
userName = "testuser-" + randomId;
11+
userName = "testuser-" + randomId();
1212
});
1313

1414
const createUserWithMCP = async (password?: string): Promise<unknown> => {

tests/integration/tools/atlas/performanceAdvisor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { BaseEvent, ToolEvent } from "../../../../src/telemetry/types.js";
1818

1919
describeWithAtlas("performanceAdvisor", (integration) => {
2020
withProject(integration, ({ getProjectId }) => {
21-
const clusterName = "ClusterTest-" + randomId;
21+
const clusterName = "ClusterTest-" + randomId();
2222

2323
afterAll(async () => {
2424
const projectId = getProjectId();

0 commit comments

Comments
 (0)