-
Notifications
You must be signed in to change notification settings - Fork 139
fix: add argument validation - MCP-188 #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b89637f
7682f5c
dd003e6
d4fec26
92f8d2f
db75e4f
2f99c5a
9374570
4db1b1f
9526a4d
8a90fd6
b988118
0b9c32f
91b1caa
e24937a
126852a
8dc0359
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { z, type ZodString } from "zod"; | ||
|
||
const NO_UNICODE_REGEX = /^[\x20-\x7E]*$/; | ||
export const NO_UNICODE_ERROR = "String cannot contain special characters or Unicode symbols"; | ||
|
||
const ALLOWED_USERNAME_CHARACTERS_REGEX = /^[a-zA-Z0-9._-]+$/; | ||
export const ALLOWED_USERNAME_CHARACTERS_ERROR = | ||
"Username can only contain letters, numbers, dots, hyphens, and underscores"; | ||
|
||
const ALLOWED_REGION_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/; | ||
export const ALLOWED_REGION_CHARACTERS_ERROR = "Region can only contain letters, numbers, hyphens, and underscores"; | ||
|
||
const ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/; | ||
export const ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR = | ||
"Cluster names can only contain ASCII letters, numbers, and hyphens."; | ||
|
||
const ALLOWED_PROJECT_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9\s()@&+:._',-]+$/; | ||
export const ALLOWED_PROJECT_NAME_CHARACTERS_ERROR = | ||
"Project names can't be longer than 64 characters and can only contain letters, numbers, spaces, and the following symbols: ( ) @ & + : . _ - ' ,"; | ||
export const CommonArgs = { | ||
string: (): ZodString => z.string().regex(NO_UNICODE_REGEX, NO_UNICODE_ERROR), | ||
|
||
objectId: (fieldName: string): z.ZodString => | ||
blva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
z | ||
.string() | ||
.min(1, `${fieldName} is required`) | ||
blva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.length(24, `${fieldName} must be exactly 24 characters`) | ||
.regex(/^[0-9a-fA-F]+$/, `${fieldName} must contain only hexadecimal characters`), | ||
}; | ||
|
||
export const AtlasArgs = { | ||
projectId: (): z.ZodString => CommonArgs.objectId("projectId"), | ||
|
||
organizationId: (): z.ZodString => CommonArgs.objectId("organizationId"), | ||
|
||
clusterName: (): z.ZodString => | ||
z | ||
.string() | ||
.min(1, "Cluster name is required") | ||
.max(64, "Cluster name must be 64 characters or less") | ||
.regex(ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX, ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR), | ||
|
||
projectName: (): z.ZodString => | ||
z | ||
.string() | ||
.min(1, "Project name is required") | ||
.max(64, "Project name must be 64 characters or less") | ||
.regex(ALLOWED_PROJECT_NAME_CHARACTERS_REGEX, ALLOWED_PROJECT_NAME_CHARACTERS_ERROR), | ||
|
||
username: (): z.ZodString => | ||
z | ||
.string() | ||
.min(1, "Username is required") | ||
.max(100, "Username must be 100 characters or less") | ||
.regex(ALLOWED_USERNAME_CHARACTERS_REGEX, ALLOWED_USERNAME_CHARACTERS_ERROR), | ||
|
||
ipAddress: (): z.ZodString => z.string().ip({ version: "v4" }), | ||
|
||
cidrBlock: (): z.ZodString => z.string().cidr(), | ||
|
||
region: (): z.ZodString => | ||
z | ||
.string() | ||
.min(1, "Region is required") | ||
.max(50, "Region must be 50 characters or less") | ||
.regex(ALLOWED_REGION_CHARACTERS_REGEX, ALLOWED_REGION_CHARACTERS_ERROR), | ||
|
||
password: (): z.ZodString => | ||
z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"), | ||
}; |
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"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
went into atlas UI and copied the exact same validation text