Skip to content

Commit aa4b290

Browse files
committed
feat: add atlas-connect-cluster tool
1 parent a6f9ce2 commit aa4b290

File tree

5 files changed

+167
-17
lines changed

5 files changed

+167
-17
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
function generateSecurePassword(): string { // TODO: use a better password generator
7+
return `pwdMcp${Math.floor(Math.random() * 100000)}`;
8+
}
9+
10+
export class ConnectClusterTool extends AtlasToolBase {
11+
protected name = "atlas-connect-cluster";
12+
protected description = "Connect to MongoDB Atlas cluster";
13+
protected operationType: OperationType = "metadata";
14+
protected argsShape = {
15+
projectId: z.string().describe("Atlas project ID"),
16+
clusterName: z.string().describe("Atlas cluster name"),
17+
};
18+
19+
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
20+
const cluster = await this.session.apiClient.getCluster({
21+
params: {
22+
path: {
23+
groupId: projectId,
24+
clusterName,
25+
},
26+
},
27+
});
28+
29+
if (!cluster) {
30+
throw new Error("Cluster not found");
31+
}
32+
33+
if (!cluster.connectionStrings?.standardSrv || !cluster.connectionStrings?.standard) {
34+
throw new Error("Connection string not available");
35+
}
36+
37+
const username = `usrMcp${Math.floor(Math.random() * 100000)}`;
38+
const password = generateSecurePassword();
39+
40+
const expiryMs = 1000 * 60 * 60 * 12; // 12 hours
41+
const expiryDate = new Date(Date.now() + expiryMs);
42+
43+
await this.session.apiClient.createDatabaseUser({
44+
params: {
45+
path: {
46+
groupId: projectId,
47+
}
48+
},
49+
body: {
50+
databaseName: "admin",
51+
groupId: projectId,
52+
roles: [
53+
{
54+
roleName: "readWriteAnyDatabase",
55+
databaseName: "admin",
56+
},
57+
],
58+
scopes: [{type: "CLUSTER", name: clusterName}],
59+
username,
60+
password,
61+
awsIAMType: "NONE",
62+
ldapAuthType: "NONE",
63+
oidcAuthType: "NONE",
64+
x509Type: "NONE",
65+
deleteAfterDate: expiryDate.toISOString(),
66+
}
67+
});
68+
69+
setTimeout(async () => { // disconnect after 12 hours
70+
if (this.session.serviceProvider) {
71+
await this.session.serviceProvider?.close(true);
72+
this.session.serviceProvider = undefined;
73+
}
74+
}, expiryMs);
75+
76+
const connectionString = (cluster.connectionStrings.standardSrv || cluster.connectionStrings.standard || "").replace('://', `://${username}:${password}@`) + `?authSource=admin`;
77+
78+
await this.connectToMongoDB(connectionString);
79+
80+
return {
81+
content: [
82+
{
83+
type: "text",
84+
text: `Connected to cluster "${clusterName}"`,
85+
},
86+
]
87+
};
88+
}
89+
}

src/tools/atlas/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ListDBUsersTool } from "./read/listDBUsers.js";
88
import { CreateDBUserTool } from "./create/createDBUser.js";
99
import { CreateProjectTool } from "./create/createProject.js";
1010
import { ListOrganizationsTool } from "./read/listOrgs.js";
11+
import { ConnectClusterTool } from "./metadata/connectCluster.js";
1112

1213
export const AtlasTools = [
1314
ListClustersTool,
@@ -20,4 +21,5 @@ export const AtlasTools = [
2021
CreateDBUserTool,
2122
CreateProjectTool,
2223
ListOrganizationsTool,
24+
ConnectClusterTool
2325
];

src/tools/mongodb/mongodbTool.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,4 @@ export abstract class MongoDBToolBase extends ToolBase {
4646

4747
return super.handleError(error, args);
4848
}
49-
50-
protected async connectToMongoDB(connectionString: string): Promise<void> {
51-
const provider = await NodeDriverServiceProvider.connect(connectionString, {
52-
productDocsLink: "https://docs.mongodb.com/todo-mcp",
53-
productName: "MongoDB MCP",
54-
readConcern: {
55-
level: this.config.connectOptions.readConcern,
56-
},
57-
readPreference: this.config.connectOptions.readPreference,
58-
writeConcern: {
59-
w: this.config.connectOptions.writeConcern,
60-
},
61-
timeoutMS: this.config.connectOptions.timeoutMS,
62-
});
63-
64-
this.session.serviceProvider = provider;
65-
}
6649
}

src/tools/tool.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { mongoLogId } from "mongodb-log-writer";
77
import { Telemetry } from "../telemetry/telemetry.js";
88
import { type ToolEvent } from "../telemetry/types.js";
99
import { UserConfig } from "../config.js";
10+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1011

1112
export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
1213

@@ -123,4 +124,21 @@ export abstract class ToolBase {
123124
],
124125
};
125126
}
127+
128+
protected async connectToMongoDB(connectionString: string): Promise<void> {
129+
const provider = await NodeDriverServiceProvider.connect(connectionString, {
130+
productDocsLink: "https://docs.mongodb.com/todo-mcp",
131+
productName: "MongoDB MCP",
132+
readConcern: {
133+
level: this.config.connectOptions.readConcern,
134+
},
135+
readPreference: this.config.connectOptions.readPreference,
136+
writeConcern: {
137+
w: this.config.connectOptions.writeConcern,
138+
},
139+
timeoutMS: this.config.connectOptions.timeoutMS,
140+
});
141+
142+
this.session.serviceProvider = provider;
143+
}
126144
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ async function deleteAndWaitCluster(session: Session, projectId: string, cluster
2929
}
3030
}
3131

32+
async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) {
33+
while (true) {
34+
const cluster = await session.apiClient.getCluster({
35+
params: {
36+
path: {
37+
groupId: projectId,
38+
clusterName: clusterName,
39+
},
40+
},
41+
});
42+
if (cluster?.stateName === state) {
43+
return;
44+
}
45+
await sleep(1000);
46+
}
47+
}
48+
49+
3250
describeWithAtlas("clusters", (integration) => {
3351
withProject(integration, ({ getProjectId }) => {
3452
const clusterName = "ClusterTest-" + randomId;
@@ -117,5 +135,45 @@ describeWithAtlas("clusters", (integration) => {
117135
expect(response.content[1].text).toContain(`${clusterName} | `);
118136
});
119137
});
138+
139+
describe("atlas-connect-cluster", () => {
140+
beforeAll(async () => {
141+
const projectId = getProjectId();
142+
await waitClusterState(integration.mcpServer().session, projectId, clusterName, "IDLE");
143+
const cluster = await integration.mcpServer().session.apiClient.getCluster({
144+
params: {
145+
path: {
146+
groupId: projectId,
147+
clusterName: clusterName,
148+
},
149+
}
150+
});
151+
152+
console.log("Cluster connection string: ", cluster?.connectionStrings?.standardSrv || cluster?.connectionStrings?.standard);
153+
});
154+
155+
it("should have correct metadata", async () => {
156+
const { tools } = await integration.mcpClient().listTools();
157+
const connectCluster = tools.find((tool) => tool.name === "atlas-connect-cluster");
158+
159+
expectDefined(connectCluster);
160+
expect(connectCluster.inputSchema.type).toBe("object");
161+
expectDefined(connectCluster.inputSchema.properties);
162+
expect(connectCluster.inputSchema.properties).toHaveProperty("projectId");
163+
expect(connectCluster.inputSchema.properties).toHaveProperty("clusterName");
164+
});
165+
166+
it("connects to cluster", async () => {
167+
const projectId = getProjectId();
168+
169+
const response = (await integration.mcpClient().callTool({
170+
name: "atlas-connect-cluster",
171+
arguments: { projectId, clusterName },
172+
})) as CallToolResult;
173+
expect(response.content).toBeArray();
174+
expect(response.content).toHaveLength(1);
175+
expect(response.content[0].text).toContain(`Connected to cluster "${clusterName}"`);
176+
});
177+
});
120178
});
121179
});

0 commit comments

Comments
 (0)