Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 17b544b

Browse files
authored
fix: add argument validation - MCP-188 (#542)
1 parent ce02189 commit 17b544b

File tree

14 files changed

+573
-73
lines changed

14 files changed

+573
-73
lines changed

‎src/tools/args.ts‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { z, type ZodString } from "zod";
2+
3+
const NO_UNICODE_REGEX = /^[\x20-\x7E]*$/;
4+
export const NO_UNICODE_ERROR = "String cannot contain special characters or Unicode symbols";
5+
6+
const ALLOWED_USERNAME_CHARACTERS_REGEX = /^[a-zA-Z0-9._-]+$/;
7+
export const ALLOWED_USERNAME_CHARACTERS_ERROR =
8+
"Username can only contain letters, numbers, dots, hyphens, and underscores";
9+
10+
const ALLOWED_REGION_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/;
11+
export const ALLOWED_REGION_CHARACTERS_ERROR = "Region can only contain letters, numbers, hyphens, and underscores";
12+
13+
const ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/;
14+
export const ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR =
15+
"Cluster names can only contain ASCII letters, numbers, and hyphens.";
16+
17+
const ALLOWED_PROJECT_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9\s()@&+:._',-]+$/;
18+
export const ALLOWED_PROJECT_NAME_CHARACTERS_ERROR =
19+
"Project names can't be longer than 64 characters and can only contain letters, numbers, spaces, and the following symbols: ( ) @ & + : . _ - ' ,";
20+
export const CommonArgs = {
21+
string: (): ZodString => z.string().regex(NO_UNICODE_REGEX, NO_UNICODE_ERROR),
22+
23+
objectId: (fieldName: string): z.ZodString =>
24+
z
25+
.string()
26+
.min(1, `${fieldName} is required`)
27+
.length(24, `${fieldName} must be exactly 24 characters`)
28+
.regex(/^[0-9a-fA-F]+$/, `${fieldName} must contain only hexadecimal characters`),
29+
};
30+
31+
export const AtlasArgs = {
32+
projectId: (): z.ZodString => CommonArgs.objectId("projectId"),
33+
34+
organizationId: (): z.ZodString => CommonArgs.objectId("organizationId"),
35+
36+
clusterName: (): z.ZodString =>
37+
z
38+
.string()
39+
.min(1, "Cluster name is required")
40+
.max(64, "Cluster name must be 64 characters or less")
41+
.regex(ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX, ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR),
42+
43+
projectName: (): z.ZodString =>
44+
z
45+
.string()
46+
.min(1, "Project name is required")
47+
.max(64, "Project name must be 64 characters or less")
48+
.regex(ALLOWED_PROJECT_NAME_CHARACTERS_REGEX, ALLOWED_PROJECT_NAME_CHARACTERS_ERROR),
49+
50+
username: (): z.ZodString =>
51+
z
52+
.string()
53+
.min(1, "Username is required")
54+
.max(100, "Username must be 100 characters or less")
55+
.regex(ALLOWED_USERNAME_CHARACTERS_REGEX, ALLOWED_USERNAME_CHARACTERS_ERROR),
56+
57+
ipAddress: (): z.ZodString => z.string().ip({ version: "v4" }),
58+
59+
cidrBlock: (): z.ZodString => z.string().cidr(),
60+
61+
region: (): z.ZodString =>
62+
z
63+
.string()
64+
.min(1, "Region is required")
65+
.max(50, "Region must be 50 characters or less")
66+
.regex(ALLOWED_REGION_CHARACTERS_REGEX, ALLOWED_REGION_CHARACTERS_ERROR),
67+
68+
password: (): z.ZodString =>
69+
z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"),
70+
};

‎src/tools/atlas/atlasTool.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { ToolCategory, TelemetryToolMetadata, ToolArgs } from "../tool.js";
2-
import { ToolBase } from "../tool.js";
31
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { ToolBase, type ToolArgs, type ToolCategory, type TelemetryToolMetadata } from "../tool.js";
43
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
54
import { LogId } from "../../common/logger.js";
65
import { z } from "zod";

‎src/tools/atlas/connect/connectCluster.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type OperationType, type ToolArgs } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
54
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
65
import { LogId } from "../../../common/logger.js";
76
import { inspectCluster } from "../../../common/atlas/cluster.js";
87
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
98
import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js";
109
import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js";
10+
import { AtlasArgs } from "../../args.js";
1111

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

23+
export const ConnectClusterArgs = {
24+
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
25+
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
26+
};
27+
2328
export class ConnectClusterTool extends AtlasToolBase {
2429
public name = "atlas-connect-cluster";
2530
protected description = "Connect to MongoDB Atlas cluster";
2631
public operationType: OperationType = "connect";
2732
protected argsShape = {
28-
projectId: z.string().describe("Atlas project ID"),
29-
clusterName: z.string().describe("Atlas cluster name"),
33+
...ConnectClusterArgs,
3034
};
3135

3236
private queryConnection(

‎src/tools/atlas/create/createAccessList.ts‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import { z } from "zod";
2+
import { type OperationType, type ToolArgs } from "../../tool.js";
23
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
34
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
55
import { makeCurrentIpAccessListEntry, DEFAULT_ACCESS_LIST_COMMENT } from "../../../common/atlas/accessListUtils.js";
6+
import { AtlasArgs, CommonArgs } from "../../args.js";
7+
8+
export const CreateAccessListArgs = {
9+
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
10+
ipAddresses: z.array(AtlasArgs.ipAddress()).describe("IP addresses to allow access from").optional(),
11+
cidrBlocks: z.array(AtlasArgs.cidrBlock()).describe("CIDR blocks to allow access from").optional(),
12+
currentIpAddress: z.boolean().describe("Add the current IP address").default(false),
13+
comment: CommonArgs.string()
14+
.describe("Comment for the access list entries")
15+
.default(DEFAULT_ACCESS_LIST_COMMENT)
16+
.optional(),
17+
};
618

719
export class CreateAccessListTool extends AtlasToolBase {
820
public name = "atlas-create-access-list";
921
protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters.";
1022
public operationType: OperationType = "create";
1123
protected argsShape = {
12-
projectId: z.string().describe("Atlas project ID"),
13-
ipAddresses: z
14-
.array(z.string().ip({ version: "v4" }))
15-
.describe("IP addresses to allow access from")
16-
.optional(),
17-
cidrBlocks: z.array(z.string().cidr()).describe("CIDR blocks to allow access from").optional(),
18-
currentIpAddress: z.boolean().describe("Add the current IP address").default(false),
19-
comment: z
20-
.string()
21-
.describe("Comment for the access list entries")
22-
.default(DEFAULT_ACCESS_LIST_COMMENT)
23-
.optional(),
24+
...CreateAccessListArgs,
2425
};
2526

2627
protected async execute({

‎src/tools/atlas/create/createDBUser.ts‎

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,45 @@
11
import { z } from "zod";
2+
import type { ToolArgs, OperationType } from "../../tool.js";
23
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
34
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
55
import type { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
66
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
77
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
8+
import { AtlasArgs, CommonArgs } from "../../args.js";
9+
10+
export const CreateDBUserArgs = {
11+
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
12+
username: AtlasArgs.username().describe("Username for the new user"),
13+
// Models will generate overly simplistic passwords like SecurePassword123 or
14+
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
15+
// the model not to try and generate anything and instead leave the field unset.
16+
password: AtlasArgs.password()
17+
.optional()
18+
.nullable()
19+
.describe(
20+
"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."
21+
),
22+
roles: z
23+
.array(
24+
z.object({
25+
roleName: CommonArgs.string().describe("Role name"),
26+
databaseName: CommonArgs.string().describe("Database name").default("admin"),
27+
collectionName: CommonArgs.string().describe("Collection name").optional(),
28+
})
29+
)
30+
.describe("Roles for the new user"),
31+
clusters: z
32+
.array(AtlasArgs.clusterName())
33+
.describe("Clusters to assign the user to, leave empty for access to all clusters")
34+
.optional(),
35+
};
836

937
export class CreateDBUserTool extends AtlasToolBase {
1038
public name = "atlas-create-db-user";
1139
protected description = "Create an MongoDB Atlas database user";
1240
public operationType: OperationType = "create";
1341
protected argsShape = {
14-
projectId: z.string().describe("Atlas project ID"),
15-
username: z.string().describe("Username for the new user"),
16-
// Models will generate overly simplistic passwords like SecurePassword123 or
17-
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
18-
// the model not to try and generate anything and instead leave the field unset.
19-
password: z
20-
.string()
21-
.optional()
22-
.nullable()
23-
.describe(
24-
"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."
25-
),
26-
roles: z
27-
.array(
28-
z.object({
29-
roleName: z.string().describe("Role name"),
30-
databaseName: z.string().describe("Database name").default("admin"),
31-
collectionName: z.string().describe("Collection name").optional(),
32-
})
33-
)
34-
.describe("Roles for the new user"),
35-
clusters: z
36-
.array(z.string())
37-
.describe("Clusters to assign the user to, leave empty for access to all clusters")
38-
.optional(),
42+
...CreateDBUserArgs,
3943
};
4044

4145
protected async execute({

‎src/tools/atlas/create/createFreeCluster.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type ToolArgs, type OperationType } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
54
import type { ClusterDescription20240805 } from "../../../common/atlas/openapi.js";
65
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
6+
import { AtlasArgs } from "../../args.js";
77

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

1818
protected async execute({ projectId, name, region }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {

‎src/tools/atlas/create/createProject.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type OperationType, type ToolArgs } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
54
import type { Group } from "../../../common/atlas/openapi.js";
5+
import { AtlasArgs } from "../../args.js";
6+
7+
export const CreateProjectArgs = {
8+
projectName: AtlasArgs.projectName().optional().describe("Name for the new project"),
9+
organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"),
10+
};
611

712
export class CreateProjectTool extends AtlasToolBase {
813
public name = "atlas-create-project";
914
protected description = "Create a MongoDB Atlas project";
1015
public operationType: OperationType = "create";
1116
protected argsShape = {
12-
projectName: z.string().optional().describe("Name for the new project"),
13-
organizationId: z.string().optional().describe("Organization ID for the new project"),
17+
...CreateProjectArgs,
1418
};
1519

1620
protected async execute({ projectName, organizationId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {

‎src/tools/atlas/read/inspectAccessList.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
5-
import { formatUntrustedData } from "../../tool.js";
4+
import { AtlasArgs } from "../../args.js";
5+
6+
export const InspectAccessListArgs = {
7+
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
8+
};
69

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

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

‎src/tools/atlas/read/inspectCluster.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
5-
import { formatUntrustedData } from "../../tool.js";
64
import type { Cluster } from "../../../common/atlas/cluster.js";
75
import { inspectCluster } from "../../../common/atlas/cluster.js";
6+
import { AtlasArgs } from "../../args.js";
7+
8+
export const InspectClusterArgs = {
9+
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
10+
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
11+
};
812

913
export class InspectClusterTool extends AtlasToolBase {
1014
public name = "atlas-inspect-cluster";
1115
protected description = "Inspect MongoDB Atlas cluster";
1216
public operationType: OperationType = "read";
1317
protected argsShape = {
14-
projectId: z.string().describe("Atlas project ID"),
15-
clusterName: z.string().describe("Atlas cluster name"),
18+
...InspectClusterArgs,
1619
};
1720

1821
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {

‎src/tools/atlas/read/listAlerts.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { z } from "zod";
21
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { type OperationType, type ToolArgs, formatUntrustedData } from "../../tool.js";
33
import { AtlasToolBase } from "../atlasTool.js";
4-
import type { ToolArgs, OperationType } from "../../tool.js";
5-
import { formatUntrustedData } from "../../tool.js";
4+
import { AtlasArgs } from "../../args.js";
5+
6+
export const ListAlertsArgs = {
7+
projectId: AtlasArgs.projectId().describe("Atlas project ID to list alerts for"),
8+
};
69

710
export class ListAlertsTool extends AtlasToolBase {
811
public name = "atlas-list-alerts";
912
protected description = "List MongoDB Atlas alerts";
1013
public operationType: OperationType = "read";
1114
protected argsShape = {
12-
projectId: z.string().describe("Atlas project ID to list alerts for"),
15+
...ListAlertsArgs,
1316
};
1417

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

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /