Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/tools/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { z, type ZodString } from "zod";

export const CommonArgs = {
string: (): ZodString =>
z.string().regex(/^[\x20-\x7E]*$/, "String cannot contain special characters or Unicode symbols"),
};

export const AtlasArgs = {
objectId: (fieldName: string): z.ZodString =>
CommonArgs.string()
.min(1, `${fieldName} is required`)
.regex(/^[0-9a-fA-F]{24}$/, `${fieldName} must be a valid 24-character hexadecimal string`),

projectId: (): z.ZodString => AtlasArgs.objectId("projectId"),

organizationId: (): z.ZodString => AtlasArgs.objectId("organizationId"),

clusterName: (): z.ZodString =>
CommonArgs.string()
.min(1, "Cluster name is required")
.max(64, "Cluster name must be 64 characters or less")
.regex(/^[^/]*$/, "String cannot contain '/'")
.regex(/^[a-zA-Z0-9_-]+$/, "Cluster name can only contain letters, numbers, hyphens, and underscores"),

projectName: (): z.ZodString =>
CommonArgs.string()
.min(1, "Project name is required")
.max(64, "Project name must be 64 characters or less")
.regex(/^[^/]*$/, "String cannot contain '/'")
.regex(/^[a-zA-Z0-9_-]+$/, "Project name can only contain letters, numbers, hyphens, and underscores"),

username: (): z.ZodString =>
CommonArgs.string()
.min(1, "Username is required")
.max(100, "Username must be 100 characters or less")
.regex(/^[^/]*$/, "String cannot contain '/'")
.regex(/^[a-zA-Z0-9._-]+$/, "Username can only contain letters, numbers, dots, hyphens, and underscores"),

ipAddress: (): z.ZodString => CommonArgs.string().ip({ version: "v4" }),

cidrBlock: (): z.ZodString => CommonArgs.string().cidr(),

region: (): z.ZodString =>
CommonArgs.string()
.min(1, "Region is required")
.max(50, "Region must be 50 characters or less")
.regex(/^[a-zA-Z0-9_-]+$/, "Region can only contain letters, numbers, hyphens, and underscores"),

password: (): z.ZodString =>
CommonArgs.string()
.min(1, "Password is required")
.max(100, "Password must be 100 characters or less")
.regex(/^[^/]*$/, "String cannot contain '/'")
.regex(/^[a-zA-Z0-9._-]+$/, "Password can only contain letters, numbers, dots, hyphens, and underscores"),
};
3 changes: 1 addition & 2 deletions src/tools/atlas/atlasTool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ToolCategory, TelemetryToolMetadata, ToolArgs } from "../tool.js";
import { ToolBase } from "../tool.js";
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ToolBase, type ToolArgs, type ToolCategory, type TelemetryToolMetadata } from "../tool.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { LogId } from "../../common/logger.js";
import { z } from "zod";
Expand Down
12 changes: 8 additions & 4 deletions src/tools/atlas/connect/connectCluster.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type OperationType, type ToolArgs } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
import { LogId } from "../../../common/logger.js";
import { inspectCluster } from "../../../common/atlas/cluster.js";
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js";
import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js";
import { AtlasArgs } from "../../args.js";

const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
const addedIpAccessListMessage =
Expand All @@ -20,13 +20,17 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export const ConnectClusterArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
};

export class ConnectClusterTool extends AtlasToolBase {
public name = "atlas-connect-cluster";
protected description = "Connect to MongoDB Atlas cluster";
public operationType: OperationType = "connect";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
clusterName: z.string().describe("Atlas cluster name"),
...ConnectClusterArgs,
};

private queryConnection(
Expand Down
27 changes: 14 additions & 13 deletions src/tools/atlas/create/createAccessList.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { z } from "zod";
import { type OperationType, type ToolArgs } from "../../tool.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { makeCurrentIpAccessListEntry, DEFAULT_ACCESS_LIST_COMMENT } from "../../../common/atlas/accessListUtils.js";
import { AtlasArgs, CommonArgs } from "../../args.js";

export const CreateAccessListArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
ipAddresses: z.array(AtlasArgs.ipAddress()).describe("IP addresses to allow access from").optional(),
cidrBlocks: z.array(AtlasArgs.cidrBlock()).describe("CIDR blocks to allow access from").optional(),
currentIpAddress: z.boolean().describe("Add the current IP address").default(false),
comment: CommonArgs.string()
.describe("Comment for the access list entries")
.default(DEFAULT_ACCESS_LIST_COMMENT)
.optional(),
};

export class CreateAccessListTool extends AtlasToolBase {
public name = "atlas-create-access-list";
protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters.";
public operationType: OperationType = "create";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
ipAddresses: z
.array(z.string().ip({ version: "v4" }))
.describe("IP addresses to allow access from")
.optional(),
cidrBlocks: z.array(z.string().cidr()).describe("CIDR blocks to allow access from").optional(),
currentIpAddress: z.boolean().describe("Add the current IP address").default(false),
comment: z
.string()
.describe("Comment for the access list entries")
.default(DEFAULT_ACCESS_LIST_COMMENT)
.optional(),
...CreateAccessListArgs,
};

protected async execute({
Expand Down
56 changes: 30 additions & 26 deletions src/tools/atlas/create/createDBUser.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
import { z } from "zod";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
import { AtlasArgs, CommonArgs } from "../../args.js";

export const CreateDBUserArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
username: AtlasArgs.username().describe("Username for the new user"),
// Models will generate overly simplistic passwords like SecurePassword123 or
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
// the model not to try and generate anything and instead leave the field unset.
password: AtlasArgs.password()
.optional()
.nullable()
.describe(
"Password for the new user. IMPORTANT: If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
),
roles: z
.array(
z.object({
roleName: CommonArgs.string().describe("Role name"),
databaseName: CommonArgs.string().describe("Database name").default("admin"),
collectionName: CommonArgs.string().describe("Collection name").optional(),
})
)
.describe("Roles for the new user"),
clusters: z
.array(CommonArgs.string())
.describe("Clusters to assign the user to, leave empty for access to all clusters")
.optional(),
};

export class CreateDBUserTool extends AtlasToolBase {
public name = "atlas-create-db-user";
protected description = "Create an MongoDB Atlas database user";
public operationType: OperationType = "create";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
username: z.string().describe("Username for the new user"),
// Models will generate overly simplistic passwords like SecurePassword123 or
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
// the model not to try and generate anything and instead leave the field unset.
password: z
.string()
.optional()
.nullable()
.describe(
"Password for the new user. IMPORTANT: If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
),
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(),
...CreateDBUserArgs,
};

protected async execute({
Expand Down
10 changes: 5 additions & 5 deletions src/tools/atlas/create/createFreeCluster.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type ToolArgs, type OperationType } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ClusterDescription20240805 } from "../../../common/atlas/openapi.js";
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
import { AtlasArgs } from "../../args.js";

export class CreateFreeClusterTool extends AtlasToolBase {
public name = "atlas-create-free-cluster";
protected description = "Create a free MongoDB Atlas cluster";
public operationType: OperationType = "create";
protected argsShape = {
projectId: z.string().describe("Atlas project ID to create the cluster in"),
name: z.string().describe("Name of the cluster"),
region: z.string().describe("Region of the cluster").default("US_EAST_1"),
projectId: AtlasArgs.projectId().describe("Atlas project ID to create the cluster in"),
name: AtlasArgs.clusterName().describe("Name of the cluster"),
region: AtlasArgs.region().describe("Region of the cluster").default("US_EAST_1"),
};

protected async execute({ projectId, name, region }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
12 changes: 8 additions & 4 deletions src/tools/atlas/create/createProject.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type OperationType, type ToolArgs } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { Group } from "../../../common/atlas/openapi.js";
import { AtlasArgs } from "../../args.js";

export const CreateProjectArgs = {
projectName: AtlasArgs.projectName().optional().describe("Name for the new project"),
organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"),
};

export class CreateProjectTool extends AtlasToolBase {
public name = "atlas-create-project";
protected description = "Create a MongoDB Atlas project";
public operationType: OperationType = "create";
protected argsShape = {
projectName: z.string().optional().describe("Name for the new project"),
organizationId: z.string().optional().describe("Organization ID for the new project"),
...CreateProjectArgs,
};

protected async execute({ projectName, organizationId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
11 changes: 7 additions & 4 deletions src/tools/atlas/read/inspectAccessList.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { AtlasArgs } from "../../args.js";

export const InspectAccessListArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
};

export class InspectAccessListTool extends AtlasToolBase {
public name = "atlas-inspect-access-list";
protected description = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters.";
public operationType: OperationType = "read";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
...InspectAccessListArgs,
};

protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
13 changes: 8 additions & 5 deletions src/tools/atlas/read/inspectCluster.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import type { Cluster } from "../../../common/atlas/cluster.js";
import { inspectCluster } from "../../../common/atlas/cluster.js";
import { AtlasArgs } from "../../args.js";

export const InspectClusterArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions of projectId and clusterName seem to be the same everywhere - should we just include them in the AtlasArgs helpers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately they differ with some extra tiny context per tool, I added this as a future improvement to keep the scope of the task

clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
};

export class InspectClusterTool extends AtlasToolBase {
public name = "atlas-inspect-cluster";
protected description = "Inspect MongoDB Atlas cluster";
public operationType: OperationType = "read";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
clusterName: z.string().describe("Atlas cluster name"),
...InspectClusterArgs,
};

protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
11 changes: 7 additions & 4 deletions src/tools/atlas/read/listAlerts.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { AtlasArgs } from "../../args.js";

export const ListAlertsArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID to list alerts for"),
};

export class ListAlertsTool extends AtlasToolBase {
public name = "atlas-list-alerts";
protected description = "List MongoDB Atlas alerts";
public operationType: OperationType = "read";
protected argsShape = {
projectId: z.string().describe("Atlas project ID to list alerts for"),
...ListAlertsArgs,
};

protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
8 changes: 6 additions & 2 deletions src/tools/atlas/read/listClusters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
Expand All @@ -10,13 +9,18 @@ import type {
PaginatedFlexClusters20241113,
} from "../../../common/atlas/openapi.js";
import { formatCluster, formatFlexCluster } from "../../../common/atlas/cluster.js";
import { AtlasArgs } from "../../args.js";

export const ListClustersArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID to filter clusters").optional(),
};

export class ListClustersTool extends AtlasToolBase {
public name = "atlas-list-clusters";
protected description = "List MongoDB Atlas clusters";
public operationType: OperationType = "read";
protected argsShape = {
projectId: z.string().describe("Atlas project ID to filter clusters").optional(),
...ListClustersArgs,
};

protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
8 changes: 6 additions & 2 deletions src/tools/atlas/read/listDBUsers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "../atlasTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import type { DatabaseUserRole, UserScope } from "../../../common/atlas/openapi.js";
import { AtlasArgs } from "../../args.js";

export const ListDBUsersArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID to filter DB users"),
};

export class ListDBUsersTool extends AtlasToolBase {
public name = "atlas-list-db-users";
protected description = "List MongoDB Atlas database users";
public operationType: OperationType = "read";
protected argsShape = {
projectId: z.string().describe("Atlas project ID to filter DB users"),
...ListDBUsersArgs,
};

protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
Loading
Loading