From eb5760883ee87236dfb6dea17c61c683208bc2c9 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 11:29:40 +0100 Subject: [PATCH 1/3] feat: add DB users tools --- scripts/filter.ts | 2 + src/common/atlas/{client.ts => apiClient.ts} | 13 ++ src/common/atlas/auth.ts | 2 +- src/common/atlas/openapi.d.ts | 229 +++++++++++++++++++ src/config.ts | 1 - src/server.ts | 2 +- src/state.ts | 2 +- src/tools/atlas/atlasTool.ts | 2 +- src/tools/atlas/createDBUser.ts | 63 +++++ src/tools/atlas/listClusters.ts | 7 +- src/tools/atlas/listDBUsers.ts | 50 ++++ src/tools/atlas/tools.ts | 6 +- 12 files changed, 369 insertions(+), 10 deletions(-) rename src/common/atlas/{client.ts => apiClient.ts} (94%) create mode 100644 src/tools/atlas/createDBUser.ts create mode 100644 src/tools/atlas/listDBUsers.ts diff --git a/scripts/filter.ts b/scripts/filter.ts index 01d7c226..c5280451 100644 --- a/scripts/filter.ts +++ b/scripts/filter.ts @@ -24,6 +24,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document { "listClusters", "createCluster", "listClustersForAllProjects", + "createDatabaseUser", + "listDatabaseUsers", ]; const filteredPaths = {}; diff --git a/src/common/atlas/client.ts b/src/common/atlas/apiClient.ts similarity index 94% rename from src/common/atlas/client.ts rename to src/common/atlas/apiClient.ts index 7629f258..9e889d6f 100644 --- a/src/common/atlas/client.ts +++ b/src/common/atlas/apiClient.ts @@ -6,6 +6,8 @@ import { PaginatedAtlasGroupView, ClusterDescription20240805, PaginatedClusterDescription20240805, + CloudDatabaseUser, + PaginatedApiAtlasDatabaseUserView, } from "./openapi.js"; export interface OAuthToken { @@ -294,4 +296,15 @@ export class ApiClient { body: JSON.stringify(cluster), }); } + + async createDatabaseUser(groupId: string, user: CloudDatabaseUser): Promise { + return await this.do(`/groups/${groupId}/databaseUsers`, { + method: "POST", + body: JSON.stringify(user), + }); + } + + async listDatabaseUsers(groupId: string): Promise { + return await this.do(`/groups/${groupId}/databaseUsers`); + } } diff --git a/src/common/atlas/auth.ts b/src/common/atlas/auth.ts index 3516820b..baeaf1ef 100644 --- a/src/common/atlas/auth.ts +++ b/src/common/atlas/auth.ts @@ -1,4 +1,4 @@ -import { ApiClient } from "./client"; +import { ApiClient } from "./apiClient"; import { State } from "../../state"; export async function ensureAuthenticated(state: State, apiClient: ApiClient): Promise { diff --git a/src/common/atlas/openapi.d.ts b/src/common/atlas/openapi.d.ts index e9458381..75623859 100644 --- a/src/common/atlas/openapi.d.ts +++ b/src/common/atlas/openapi.d.ts @@ -96,6 +96,30 @@ export interface paths { patch?: never; trace?: never; }; + "/api/atlas/v2/groups/{groupId}/databaseUsers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return All Database Users from One Project + * @description Returns all database users that belong to the specified project. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. + */ + get: operations["listDatabaseUsers"]; + put?: never; + /** + * Create One Database User in One Project + * @description Creates one database user in the specified project. This MongoDB Cloud supports a maximum of 100 database users per project. If you require more than 100 database users on a project, contact Support. To use this resource, the requesting Service Account or API Key must have the Project Owner role, the Project Charts Admin role, Project Stream Processing Owner role, or the Project Database Access Admin role. + */ + post: operations["createDatabaseUser"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1601,6 +1625,77 @@ export interface components { /** @description List that contains the versions of MongoDB that each node in the cluster runs. */ readonly versions?: string[]; }; + CloudDatabaseUser: { + /** + * @description Human-readable label that indicates whether the new database user authenticates with the Amazon Web Services (AWS) Identity and Access Management (IAM) credentials associated with the user or the user's role. + * @default NONE + * @enum {string} + */ + awsIAMType: "NONE" | "USER" | "ROLE"; + /** + * @description The database against which the database user authenticates. Database users must provide both a username and authentication database to log into MongoDB. If the user authenticates with AWS IAM, x.509, LDAP, or OIDC Workload this value should be `$external`. If the user authenticates with SCRAM-SHA or OIDC Workforce, this value should be `admin`. + * @default admin + * @enum {string} + */ + databaseName: "admin" | "$external"; + /** + * Format: date-time + * @description Date and time when MongoDB Cloud deletes the user. This parameter expresses its value in the ISO 8601 timestamp format in UTC and can include the time zone designation. You must specify a future date that falls within one week of making the Application Programming Interface (API) request. + */ + deleteAfterDate?: string; + /** @description Description of this database user. */ + description?: string; + /** @description Unique 24-hexadecimal digit string that identifies the project. */ + groupId: string; + /** @description List that contains the key-value pairs for tagging and categorizing the MongoDB database user. The labels that you define do not appear in the console. */ + labels?: components["schemas"]["ComponentLabel"][]; + /** + * @description Part of the Lightweight Directory Access Protocol (LDAP) record that the database uses to authenticate this database user on the LDAP host. + * @default NONE + * @enum {string} + */ + ldapAuthType: "NONE" | "GROUP" | "USER"; + /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ + readonly links?: components["schemas"]["Link"][]; + /** + * @description Human-readable label that indicates whether the new database user or group authenticates with OIDC federated authentication. To create a federated authentication user, specify the value of USER in this field. To create a federated authentication group, specify the value of IDP_GROUP in this field. + * @default NONE + * @enum {string} + */ + oidcAuthType: "NONE" | "IDP_GROUP" | "USER"; + /** @description Alphanumeric string that authenticates this database user against the database specified in `databaseName`. To authenticate with SCRAM-SHA, you must specify this parameter. This parameter doesn't appear in this response. */ + password?: string; + /** @description List that provides the pairings of one role with one applicable database. */ + roles?: components["schemas"]["DatabaseUserRole"][]; + /** @description List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project. */ + scopes?: components["schemas"]["UserScope"][]; + /** @description Human-readable label that represents the user that authenticates to MongoDB. The format of this label depends on the method of authentication: + * + * | Authentication Method | Parameter Needed | Parameter Value | username Format | + * |---|---|---|---| + * | AWS IAM | awsIAMType | ROLE | ARN | + * | AWS IAM | awsIAMType | USER | ARN | + * | x.509 | x509Type | CUSTOMER | [RFC 2253](https://tools.ietf.org/html/2253) Distinguished Name | + * | x.509 | x509Type | MANAGED | [RFC 2253](https://tools.ietf.org/html/2253) Distinguished Name | + * | LDAP | ldapAuthType | USER | [RFC 2253](https://tools.ietf.org/html/2253) Distinguished Name | + * | LDAP | ldapAuthType | GROUP | [RFC 2253](https://tools.ietf.org/html/2253) Distinguished Name | + * | OIDC Workforce | oidcAuthType | IDP_GROUP | Atlas OIDC IdP ID (found in federation settings), followed by a '/', followed by the IdP group name | + * | OIDC Workload | oidcAuthType | USER | Atlas OIDC IdP ID (found in federation settings), followed by a '/', followed by the IdP user name | + * | SCRAM-SHA | awsIAMType, x509Type, ldapAuthType, oidcAuthType | NONE | Alphanumeric string | + * */ + username: string; + /** + * @description X.509 method that MongoDB Cloud uses to authenticate the database user. + * + * - For application-managed X.509, specify `MANAGED`. + * - For self-managed X.509, specify `CUSTOMER`. + * + * Users created with the `CUSTOMER` method require a Common Name (CN) in the **username** parameter. You must create externally authenticated users on the `$external` database. + * @default NONE + * @enum {string} + */ + x509Type: "NONE" | "CUSTOMER" | "MANAGED"; + }; CloudGCPProviderSettings: Omit & { autoScaling?: components["schemas"]["CloudProviderGCPAutoScaling"]; /** @@ -3386,6 +3481,32 @@ export interface components { */ readonly cloudProvider?: "AWS" | "AZURE" | "GCP"; }; + /** + * Database User Role + * @description Range of resources available to this database user. + */ + DatabaseUserRole: { + /** @description Collection on which this role applies. */ + collectionName?: string; + /** @description Database to which the user is granted access privileges. */ + databaseName: string; + /** + * @description Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role. + * @enum {string} + */ + roleName: + | "atlasAdmin" + | "backup" + | "clusterMonitor" + | "dbAdmin" + | "dbAdminAnyDatabase" + | "enableSharding" + | "read" + | "readAnyDatabase" + | "readWrite" + | "readWriteAnyDatabase" + | ""; + }; /** * Archival Criteria * @description **DATE criteria.type**. @@ -4876,6 +4997,18 @@ export interface components { | "ORG_MEMBER" )[]; }; + /** @description List of MongoDB Database users granted access to databases in the specified project. */ + PaginatedApiAtlasDatabaseUserView: { + /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ + readonly links?: components["schemas"]["Link"][]; + /** @description List of returned documents that MongoDB Cloud provides when completing this request. */ + readonly results?: components["schemas"]["CloudDatabaseUser"][]; + /** + * Format: int32 + * @description Total number of documents available. MongoDB Cloud omits this value if `includeCount` is set to `false`. The total number is an estimate and may not be exact. + */ + readonly totalCount?: number; + }; PaginatedAtlasGroupView: { /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ readonly links?: components["schemas"]["Link"][]; @@ -6049,6 +6182,19 @@ export interface components { */ type: "kStemming"; }; + /** + * Database User Scope + * @description Range of resources available to this database user. + */ + UserScope: { + /** @description Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access. */ + name: string; + /** + * @description Category of resource that this database user can access. + * @enum {string} + */ + type: "CLUSTER" | "DATA_LAKE" | "STREAM"; + }; /** Vector Search Host Status Detail */ VectorSearchHostStatusDetail: { /** @description Hostname that corresponds to the status detail. */ @@ -6742,6 +6888,7 @@ export type BillingInvoiceMetadata = components["schemas"]["BillingInvoiceMetada export type BillingPayment = components["schemas"]["BillingPayment"]; export type BillingRefund = components["schemas"]["BillingRefund"]; export type CloudCluster = components["schemas"]["CloudCluster"]; +export type CloudDatabaseUser = components["schemas"]["CloudDatabaseUser"]; export type CloudGcpProviderSettings = components["schemas"]["CloudGCPProviderSettings"]; export type CloudProviderAwsAutoScaling = components["schemas"]["CloudProviderAWSAutoScaling"]; export type CloudProviderAccessAwsiamRole = components["schemas"]["CloudProviderAccessAWSIAMRole"]; @@ -6812,6 +6959,7 @@ export type DataLakePipelinesPartitionField = components["schemas"]["DataLakePip export type DataLakeS3StoreSettings = components["schemas"]["DataLakeS3StoreSettings"]; export type DataLakeStoreSettings = components["schemas"]["DataLakeStoreSettings"]; export type DataProcessRegionView = components["schemas"]["DataProcessRegionView"]; +export type DatabaseUserRole = components["schemas"]["DatabaseUserRole"]; export type DateCriteriaView = components["schemas"]["DateCriteriaView"]; export type DedicatedHardwareSpec = components["schemas"]["DedicatedHardwareSpec"]; export type DedicatedHardwareSpec20240805 = components["schemas"]["DedicatedHardwareSpec20240805"]; @@ -6858,6 +7006,7 @@ export type OrgGroup = components["schemas"]["OrgGroup"]; export type OrgPendingUserResponse = components["schemas"]["OrgPendingUserResponse"]; export type OrgUserResponse = components["schemas"]["OrgUserResponse"]; export type OrgUserRolesResponse = components["schemas"]["OrgUserRolesResponse"]; +export type PaginatedApiAtlasDatabaseUserView = components["schemas"]["PaginatedApiAtlasDatabaseUserView"]; export type PaginatedAtlasGroupView = components["schemas"]["PaginatedAtlasGroupView"]; export type PaginatedClusterDescription20240805 = components["schemas"]["PaginatedClusterDescription20240805"]; export type PaginatedOrgGroupView = components["schemas"]["PaginatedOrgGroupView"]; @@ -6908,6 +7057,7 @@ export type TokenFilterSpanishPluralStemming = components["schemas"]["TokenFilte export type TokenFilterStempel = components["schemas"]["TokenFilterStempel"]; export type TokenFilterWordDelimiterGraph = components["schemas"]["TokenFilterWordDelimiterGraph"]; export type TokenFilterkStemming = components["schemas"]["TokenFilterkStemming"]; +export type UserScope = components["schemas"]["UserScope"]; export type VectorSearchHostStatusDetail = components["schemas"]["VectorSearchHostStatusDetail"]; export type VectorSearchIndex = components["schemas"]["VectorSearchIndex"]; export type VectorSearchIndexCreateRequest = components["schemas"]["VectorSearchIndexCreateRequest"]; @@ -7175,6 +7325,85 @@ export interface operations { 500: components["responses"]["internalServerError"]; }; }; + listDatabaseUsers: { + parameters: { + query?: { + /** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */ + envelope?: components["parameters"]["envelope"]; + /** @description Flag that indicates whether the response returns the total number of items (**totalCount**) in the response. */ + includeCount?: components["parameters"]["includeCount"]; + /** @description Number of items that the response returns per page. */ + itemsPerPage?: components["parameters"]["itemsPerPage"]; + /** @description Number of the page that displays the current set of the total objects that the response returns. */ + pageNum?: components["parameters"]["pageNum"]; + /** @description Flag that indicates whether the response body should be in the prettyprint format. */ + pretty?: components["parameters"]["pretty"]; + }; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2023-01-01+json": components["schemas"]["PaginatedApiAtlasDatabaseUserView"]; + }; + }; + 401: components["responses"]["unauthorized"]; + 500: components["responses"]["internalServerError"]; + }; + }; + createDatabaseUser: { + parameters: { + query?: { + /** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */ + envelope?: components["parameters"]["envelope"]; + /** @description Flag that indicates whether the response body should be in the prettyprint format. */ + pretty?: components["parameters"]["pretty"]; + }; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + }; + cookie?: never; + }; + /** @description Creates one database user in the specified project. */ + requestBody: { + content: { + "application/vnd.atlas.2023-01-01+json": components["schemas"]["CloudDatabaseUser"]; + }; + }; + responses: { + /** @description OK */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2023-01-01+json": components["schemas"]["CloudDatabaseUser"]; + }; + }; + 400: components["responses"]["badRequest"]; + 401: components["responses"]["unauthorized"]; + 403: components["responses"]["forbidden"]; + 404: components["responses"]["notFound"]; + 409: components["responses"]["conflict"]; + 500: components["responses"]["internalServerError"]; + }; + }; } type WithRequired = T & { [P in K]-?: T[P]; diff --git a/src/config.ts b/src/config.ts index 75e22803..0b12eccd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,7 +13,6 @@ export const config = { apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/", clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297", stateFile: process.env.STATE_FILE || path.resolve("./state.json"), - projectID: process.env.PROJECT_ID, userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }; diff --git a/src/server.ts b/src/server.ts index c9e9a662..1e7725c3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { ApiClient } from "./common/atlas/client.js"; +import { ApiClient } from "./common/atlas/apiClient.js"; import { State, saveState, loadState } from "./state.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { registerAtlasTools } from "./tools/atlas/tools.js"; diff --git a/src/state.ts b/src/state.ts index 534f7bd6..b020479e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,6 +1,6 @@ import fs from "fs"; import config from "./config.js"; -import { OauthDeviceCode, OAuthToken } from "./common/atlas/client.js"; +import { OauthDeviceCode, OAuthToken } from "./common/atlas/apiClient.js"; export interface State { auth: { diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index ea27ad6f..5622cff8 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,5 +1,5 @@ import { ToolBase } from "../tool.js"; -import { ApiClient } from "../../common/atlas/client.js"; +import { ApiClient } from "../../common/atlas/apiClient.js"; import { State } from "../../state.js"; import { ensureAuthenticated } from "../../common/atlas/auth.js"; diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts new file mode 100644 index 00000000..d8953cae --- /dev/null +++ b/src/tools/atlas/createDBUser.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { AtlasToolBase } from "./atlasTool.js"; +import { ToolArgs } from "../tool.js"; +import { CloudDatabaseUser, DatabaseUserRole, UserScope } from "../../common/atlas/openapi.js"; + +export class CreateDBUserTool extends AtlasToolBase { + protected name = "atlas-create-db-user"; + protected description = "Create an MongoDB Atlas user"; + protected argsShape = { + projectId: z.string().describe("Atlas project ID"), + username: z.string().describe("Username for the new user"), + password: z.string().describe("Password for the new user"), + roles: z.array(z.object({ + roleName: z.string().describe("Role name"), + databaseName: z.string().describe("Database name").default("admin"), + collectionName: z.string().describe("Collection name").optional(), + })).describe("Roles for the new user"), + clusters: z.array(z.string()).describe("Clusters to assign the user to, leave empty for access to all clusters").optional(), + }; + + protected async execute({ projectId, username, password, roles, clusters }: ToolArgs): Promise { + await this.ensureAuthenticated(); + + const input = { + groupId: projectId, + awsIAMType: "NONE", + databaseName: "admin", + ldapAuthType: "NONE", + oidcAuthType: "NONE", + x509Type: "NONE", + username, + password, + roles: roles as unknown as DatabaseUserRole[], + scopes: clusters?.length ? clusters.map(cluster => ({ + type: "CLUSTER", + name: cluster, + })) : undefined, + } as CloudDatabaseUser; + + await this.apiClient!.createDatabaseUser(projectId, input); + + return { + content: [ + { type: "text", text: `User "${username}" created sucessfully.` }, + ], + }; + } +} + +function formatRoles(roles?: DatabaseUserRole[]) { + if (!roles?.length) { + return "N/A"; + } + return roles.map(role => `${role.roleName}@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}`).join(", "); +} + +function formatScopes(scopes?: UserScope[]) { + if (!scopes?.length) { + return "All"; + } + return scopes.map(scope => `${scope.type}:${scope.name}`).join(", "); +} diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 19a38d14..448ce759 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -15,16 +15,15 @@ export class ListClustersTool extends AtlasToolBase { protected async execute({ projectId }: ToolArgs): Promise { await this.ensureAuthenticated(); - const selectedProjectId = projectId || config.projectID; - if (!selectedProjectId) { + if (!projectId) { const data = await this.apiClient.listClustersForAllProjects(); return this.formatAllClustersTable(data); } else { - const project = await this.apiClient.getProject(selectedProjectId); + const project = await this.apiClient.getProject(projectId); if (!project?.id) { - throw new Error(`Project with ID "${selectedProjectId}" not found.`); + throw new Error(`Project with ID "${projectId}" not found.`); } const data = await this.apiClient.listClusters(project.id || ""); diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts new file mode 100644 index 00000000..d7d71b7f --- /dev/null +++ b/src/tools/atlas/listDBUsers.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { AtlasToolBase } from "./atlasTool.js"; +import { ToolArgs } from "../tool.js"; +import { DatabaseUserRole, UserScope } from "../../common/atlas/openapi.js"; + +export class ListDBUsersTool extends AtlasToolBase { + protected name = "atlas-list-db-users"; + protected description = "List MongoDB Atlas users"; + protected argsShape = { + projectId: z.string().describe("Atlas project ID to filter DB users"), + }; + + protected async execute({ projectId }: ToolArgs): Promise { + await this.ensureAuthenticated(); + + const data = await this.apiClient!.listDatabaseUsers(projectId); + + if (!data.results?.length) { + throw new Error("No database users found."); + } + + const output = `Username | Roles | Scopes +----------------|----------------|---------------- +` + data.results + .map((user) => { + return `${user.username} | ${formatRoles(user.roles) } | ${formatScopes(user.scopes)}`; + }) + .join("\n"); + return { + content: [ + { type: "text", text: output }, + ], + }; + } +} + +function formatRoles(roles?: DatabaseUserRole[]) { + if (!roles?.length) { + return "N/A"; + } + return roles.map(role => `${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}`).join(", "); +} + +function formatScopes(scopes?: UserScope[]) { + if (!scopes?.length) { + return "All"; + } + return scopes.map(scope => `${scope.type}:${scope.name}`).join(", "); +} diff --git a/src/tools/atlas/tools.ts b/src/tools/atlas/tools.ts index 1b4894f8..c702f53b 100644 --- a/src/tools/atlas/tools.ts +++ b/src/tools/atlas/tools.ts @@ -1,5 +1,5 @@ import { ToolBase } from "../tool.js"; -import { ApiClient } from "../../common/atlas/client.js"; +import { ApiClient } from "../../common/atlas/apiClient.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { State } from "../../state.js"; import { AuthTool } from "./auth.js"; @@ -7,6 +7,8 @@ import { ListClustersTool } from "./listClusters.js"; import { ListProjectsTool } from "./listProjects.js"; import { InspectClusterTool } from "./inspectCluster.js"; import { CreateFreeClusterTool } from "./createFreeCluster.js"; +import { ListDBUsersTool } from "./listDBUsers.js"; +import { CreateDBUserTool } from "./createDBUser.js"; export function registerAtlasTools(server: McpServer, state: State, apiClient: ApiClient) { const tools: ToolBase[] = [ @@ -15,6 +17,8 @@ export function registerAtlasTools(server: McpServer, state: State, apiClient: A new ListProjectsTool(state, apiClient), new InspectClusterTool(state, apiClient), new CreateFreeClusterTool(state, apiClient), + new ListDBUsersTool(state, apiClient), + new CreateDBUserTool(state, apiClient), ]; for (const tool of tools) { From aed3bba7b546b8ef5d776a2250186cb703381eeb Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 11:31:27 +0100 Subject: [PATCH 2/3] fix: styles --- src/tools/atlas/createDBUser.ts | 49 +++++++++++++++++++++------------ src/tools/atlas/listDBUsers.ts | 27 ++++++++++-------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index d8953cae..34703b19 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -11,15 +11,28 @@ export class CreateDBUserTool extends AtlasToolBase { projectId: z.string().describe("Atlas project ID"), username: z.string().describe("Username for the new user"), password: z.string().describe("Password for the new user"), - roles: z.array(z.object({ - roleName: z.string().describe("Role name"), - databaseName: z.string().describe("Database name").default("admin"), - collectionName: z.string().describe("Collection name").optional(), - })).describe("Roles for the new user"), - clusters: z.array(z.string()).describe("Clusters to assign the user to, leave empty for access to all clusters").optional(), + roles: z + .array( + z.object({ + roleName: z.string().describe("Role name"), + databaseName: z.string().describe("Database name").default("admin"), + collectionName: z.string().describe("Collection name").optional(), + }) + ) + .describe("Roles for the new user"), + clusters: z + .array(z.string()) + .describe("Clusters to assign the user to, leave empty for access to all clusters") + .optional(), }; - protected async execute({ projectId, username, password, roles, clusters }: ToolArgs): Promise { + protected async execute({ + projectId, + username, + password, + roles, + clusters, + }: ToolArgs): Promise { await this.ensureAuthenticated(); const input = { @@ -32,18 +45,18 @@ export class CreateDBUserTool extends AtlasToolBase { username, password, roles: roles as unknown as DatabaseUserRole[], - scopes: clusters?.length ? clusters.map(cluster => ({ - type: "CLUSTER", - name: cluster, - })) : undefined, + scopes: clusters?.length + ? clusters.map((cluster) => ({ + type: "CLUSTER", + name: cluster, + })) + : undefined, } as CloudDatabaseUser; - + await this.apiClient!.createDatabaseUser(projectId, input); return { - content: [ - { type: "text", text: `User "${username}" created sucessfully.` }, - ], + content: [{ type: "text", text: `User "${username}" created sucessfully.` }], }; } } @@ -52,12 +65,14 @@ function formatRoles(roles?: DatabaseUserRole[]) { if (!roles?.length) { return "N/A"; } - return roles.map(role => `${role.roleName}@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}`).join(", "); + return roles + .map((role) => `${role.roleName}@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}`) + .join(", "); } function formatScopes(scopes?: UserScope[]) { if (!scopes?.length) { return "All"; } - return scopes.map(scope => `${scope.type}:${scope.name}`).join(", "); + return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", "); } diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index d7d71b7f..95677ea7 100644 --- a/src/tools/atlas/listDBUsers.ts +++ b/src/tools/atlas/listDBUsers.ts @@ -20,17 +20,17 @@ export class ListDBUsersTool extends AtlasToolBase { throw new Error("No database users found."); } - const output = `Username | Roles | Scopes + const output = + `Username | Roles | Scopes ----------------|----------------|---------------- -` + data.results - .map((user) => { - return `${user.username} | ${formatRoles(user.roles) } | ${formatScopes(user.scopes)}`; - }) - .join("\n"); +` + + data.results + .map((user) => { + return `${user.username} | ${formatRoles(user.roles)} | ${formatScopes(user.scopes)}`; + }) + .join("\n"); return { - content: [ - { type: "text", text: output }, - ], + content: [{ type: "text", text: output }], }; } } @@ -39,12 +39,17 @@ function formatRoles(roles?: DatabaseUserRole[]) { if (!roles?.length) { return "N/A"; } - return roles.map(role => `${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}`).join(", "); + return roles + .map( + (role) => + `${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}` + ) + .join(", "); } function formatScopes(scopes?: UserScope[]) { if (!scopes?.length) { return "All"; } - return scopes.map(scope => `${scope.type}:${scope.name}`).join(", "); + return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", "); } From 8ad0c488ce787e9bac0f5e2fae096e08f7c62cc6 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 11:40:19 +0100 Subject: [PATCH 3/3] fix: lint --- src/tools/atlas/createDBUser.ts | 18 +----------------- src/tools/atlas/listClusters.ts | 1 - 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index 34703b19..c3b186ca 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "./atlasTool.js"; import { ToolArgs } from "../tool.js"; -import { CloudDatabaseUser, DatabaseUserRole, UserScope } from "../../common/atlas/openapi.js"; +import { CloudDatabaseUser, DatabaseUserRole } from "../../common/atlas/openapi.js"; export class CreateDBUserTool extends AtlasToolBase { protected name = "atlas-create-db-user"; @@ -60,19 +60,3 @@ export class CreateDBUserTool extends AtlasToolBase { }; } } - -function formatRoles(roles?: DatabaseUserRole[]) { - if (!roles?.length) { - return "N/A"; - } - return roles - .map((role) => `${role.roleName}@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}`) - .join(", "); -} - -function formatScopes(scopes?: UserScope[]) { - if (!scopes?.length) { - return "All"; - } - return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", "); -} diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 448ce759..2be4cea4 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { config } from "../../config.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "./atlasTool.js"; import { ToolArgs } from "../tool.js";