From fe93f59d53d0b346465a08846c57f4d835ac92ca Mon Sep 17 00:00:00 2001 From: Luke Sanderson Date: Thu, 9 Oct 2025 11:29:52 +0100 Subject: [PATCH] Update z.string to CommonArgs.string --- src/tools/args.ts | 83 +++++++++++++++++++ .../atlasLocal/connect/connectDeployment.ts | 4 +- .../atlasLocal/create/createDeployment.ts | 4 +- .../atlasLocal/delete/deleteDeployment.ts | 4 +- 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/tools/args.ts diff --git a/src/tools/args.ts b/src/tools/args.ts new file mode 100644 index 00000000..653f72da --- /dev/null +++ b/src/tools/args.ts @@ -0,0 +1,83 @@ +import { z, type ZodString } from "zod"; +import { EJSON } from "bson"; + +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 => + z + .string() + .min(1, `${fieldName} is required`) + .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"), +}; + +function toEJSON(value: T): T { + if (!value) { + return value; + } + + return EJSON.deserialize(value, { relaxed: false }) as T; +} + +export function zEJSON(): z.AnyZodObject { + return z.object({}).passthrough().transform(toEJSON) as unknown as z.AnyZodObject; +} diff --git a/src/tools/atlasLocal/connect/connectDeployment.ts b/src/tools/atlasLocal/connect/connectDeployment.ts index 1936f193..9ea4ced6 100644 --- a/src/tools/atlasLocal/connect/connectDeployment.ts +++ b/src/tools/atlasLocal/connect/connectDeployment.ts @@ -2,14 +2,14 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasLocalToolBase } from "../atlasLocalTool.js"; import type { OperationType, ToolArgs } from "../../tool.js"; import type { Client } from "@mongodb-js-preview/atlas-local"; -import { z } from "zod"; +import { CommonArgs } from "../../args.js"; export class ConnectDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-connect-deployment"; protected description = "Connect to a MongoDB Atlas Local deployment"; public operationType: OperationType = "connect"; protected argsShape = { - deploymentName: z.string().describe("Name of the deployment to connect to"), + deploymentName: CommonArgs.string().describe("Name of the deployment to connect to"), }; protected async executeWithAtlasLocalClient( diff --git a/src/tools/atlasLocal/create/createDeployment.ts b/src/tools/atlasLocal/create/createDeployment.ts index 1ee76a44..36d5290f 100644 --- a/src/tools/atlasLocal/create/createDeployment.ts +++ b/src/tools/atlasLocal/create/createDeployment.ts @@ -2,14 +2,14 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasLocalToolBase } from "../atlasLocalTool.js"; import type { OperationType, ToolArgs } from "../../tool.js"; import type { Client, CreateDeploymentOptions, CreationSourceType } from "@mongodb-js-preview/atlas-local"; -import z from "zod"; +import { CommonArgs } from "../../args.js"; export class CreateDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-create-deployment"; protected description = "Create a MongoDB Atlas local deployment"; public operationType: OperationType = "create"; protected argsShape = { - deploymentName: z.string().describe("Name of the deployment to create").optional(), + deploymentName: CommonArgs.string().describe("Name of the deployment to create").optional(), }; protected async executeWithAtlasLocalClient( diff --git a/src/tools/atlasLocal/delete/deleteDeployment.ts b/src/tools/atlasLocal/delete/deleteDeployment.ts index bf6c890f..cd3f99f4 100644 --- a/src/tools/atlasLocal/delete/deleteDeployment.ts +++ b/src/tools/atlasLocal/delete/deleteDeployment.ts @@ -1,15 +1,15 @@ -import { z } from "zod"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasLocalToolBase } from "../atlasLocalTool.js"; import type { OperationType, ToolArgs } from "../../tool.js"; import type { Client } from "@mongodb-js-preview/atlas-local"; +import { CommonArgs } from "../../args.js"; export class DeleteDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-delete-deployment"; protected description = "Delete a MongoDB Atlas local deployment"; public operationType: OperationType = "delete"; protected argsShape = { - deploymentName: z.string().describe("Name of the deployment to delete"), + deploymentName: CommonArgs.string().describe("Name of the deployment to delete"), }; protected async executeWithAtlasLocalClient(