Skip to content

Commit eb57608

Browse files
committed
feat: add DB users tools
1 parent aad8b01 commit eb57608

File tree

12 files changed

+369
-10
lines changed

12 files changed

+369
-10
lines changed

scripts/filter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
2424
"listClusters",
2525
"createCluster",
2626
"listClustersForAllProjects",
27+
"createDatabaseUser",
28+
"listDatabaseUsers",
2729
];
2830

2931
const filteredPaths = {};

src/common/atlas/client.ts renamed to src/common/atlas/apiClient.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
PaginatedAtlasGroupView,
77
ClusterDescription20240805,
88
PaginatedClusterDescription20240805,
9+
CloudDatabaseUser,
10+
PaginatedApiAtlasDatabaseUserView,
911
} from "./openapi.js";
1012

1113
export interface OAuthToken {
@@ -294,4 +296,15 @@ export class ApiClient {
294296
body: JSON.stringify(cluster),
295297
});
296298
}
299+
300+
async createDatabaseUser(groupId: string, user: CloudDatabaseUser): Promise<CloudDatabaseUser> {
301+
return await this.do<CloudDatabaseUser>(`/groups/${groupId}/databaseUsers`, {
302+
method: "POST",
303+
body: JSON.stringify(user),
304+
});
305+
}
306+
307+
async listDatabaseUsers(groupId: string): Promise<PaginatedApiAtlasDatabaseUserView> {
308+
return await this.do<PaginatedApiAtlasDatabaseUserView>(`/groups/${groupId}/databaseUsers`);
309+
}
297310
}

src/common/atlas/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApiClient } from "./client";
1+
import { ApiClient } from "./apiClient";
22
import { State } from "../../state";
33

44
export async function ensureAuthenticated(state: State, apiClient: ApiClient): Promise<void> {

src/common/atlas/openapi.d.ts

Lines changed: 229 additions & 0 deletions
Large diffs are not rendered by default.

src/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const config = {
1313
apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/",
1414
clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
1515
stateFile: process.env.STATE_FILE || path.resolve("./state.json"),
16-
projectID: process.env.PROJECT_ID,
1716
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
1817
};
1918

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { ApiClient } from "./common/atlas/client.js";
2+
import { ApiClient } from "./common/atlas/apiClient.js";
33
import { State, saveState, loadState } from "./state.js";
44
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
55
import { registerAtlasTools } from "./tools/atlas/tools.js";

src/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "fs";
22
import config from "./config.js";
3-
import { OauthDeviceCode, OAuthToken } from "./common/atlas/client.js";
3+
import { OauthDeviceCode, OAuthToken } from "./common/atlas/apiClient.js";
44

55
export interface State {
66
auth: {

src/tools/atlas/atlasTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ToolBase } from "../tool.js";
2-
import { ApiClient } from "../../common/atlas/client.js";
2+
import { ApiClient } from "../../common/atlas/apiClient.js";
33
import { State } from "../../state.js";
44
import { ensureAuthenticated } from "../../common/atlas/auth.js";
55

src/tools/atlas/createDBUser.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { AtlasToolBase } from "./atlasTool.js";
4+
import { ToolArgs } from "../tool.js";
5+
import { CloudDatabaseUser, DatabaseUserRole, UserScope } from "../../common/atlas/openapi.js";
6+
7+
export class CreateDBUserTool extends AtlasToolBase {
8+
protected name = "atlas-create-db-user";
9+
protected description = "Create an MongoDB Atlas user";
10+
protected argsShape = {
11+
projectId: z.string().describe("Atlas project ID"),
12+
username: z.string().describe("Username for the new user"),
13+
password: z.string().describe("Password for the new user"),
14+
roles: z.array(z.object({
15+
roleName: z.string().describe("Role name"),
16+
databaseName: z.string().describe("Database name").default("admin"),
17+
collectionName: z.string().describe("Collection name").optional(),
18+
})).describe("Roles for the new user"),
19+
clusters: z.array(z.string()).describe("Clusters to assign the user to, leave empty for access to all clusters").optional(),
20+
};
21+
22+
protected async execute({ projectId, username, password, roles, clusters }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
23+
await this.ensureAuthenticated();
24+
25+
const input = {
26+
groupId: projectId,
27+
awsIAMType: "NONE",
28+
databaseName: "admin",
29+
ldapAuthType: "NONE",
30+
oidcAuthType: "NONE",
31+
x509Type: "NONE",
32+
username,
33+
password,
34+
roles: roles as unknown as DatabaseUserRole[],
35+
scopes: clusters?.length ? clusters.map(cluster => ({
36+
type: "CLUSTER",
37+
name: cluster,
38+
})) : undefined,
39+
} as CloudDatabaseUser;
40+
41+
await this.apiClient!.createDatabaseUser(projectId, input);
42+
43+
return {
44+
content: [
45+
{ type: "text", text: `User "${username}" created sucessfully.` },
46+
],
47+
};
48+
}
49+
}
50+
51+
function formatRoles(roles?: DatabaseUserRole[]) {
52+
if (!roles?.length) {
53+
return "N/A";
54+
}
55+
return roles.map(role => `${role.roleName}@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}`).join(", ");
56+
}
57+
58+
function formatScopes(scopes?: UserScope[]) {
59+
if (!scopes?.length) {
60+
return "All";
61+
}
62+
return scopes.map(scope => `${scope.type}:${scope.name}`).join(", ");
63+
}

src/tools/atlas/listClusters.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@ export class ListClustersTool extends AtlasToolBase {
1515
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
1616
await this.ensureAuthenticated();
1717

18-
const selectedProjectId = projectId || config.projectID;
19-
if (!selectedProjectId) {
18+
if (!projectId) {
2019
const data = await this.apiClient.listClustersForAllProjects();
2120

2221
return this.formatAllClustersTable(data);
2322
} else {
24-
const project = await this.apiClient.getProject(selectedProjectId);
23+
const project = await this.apiClient.getProject(projectId);
2524

2625
if (!project?.id) {
27-
throw new Error(`Project with ID "${selectedProjectId}" not found.`);
26+
throw new Error(`Project with ID "${projectId}" not found.`);
2827
}
2928

3029
const data = await this.apiClient.listClusters(project.id || "");

0 commit comments

Comments
 (0)