-
Notifications
You must be signed in to change notification settings - Fork 171
chore: adopt strict validation for all command line arguments MCP-298 #777
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 3 commits
d8f2e9d
866b8fb
228da47
19e9a44
0046732
437e719
b2dd137
b33f697
1068d03
da4bb95
e3dcf83
9263486
29c0b34
e4427c6
52b5c39
7ebf7f1
305c353
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 |
|---|---|---|
|
|
@@ -5,15 +5,15 @@ | |
| * - server.json arrays | ||
| * - README.md configuration table | ||
| * | ||
| * It uses the Zod schema and OPTIONS defined in src/common/config.ts | ||
| * It uses the UserConfig Zod Schema. | ||
| */ | ||
|
|
||
| import { readFileSync, writeFileSync } from "fs"; | ||
| import { join, dirname } from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| import { UserConfigSchema, configRegistry } from "../src/common/config/userConfig.js"; | ||
| import { UserConfigSchemaWithCliOptions, configRegistry } from "../src/common/config/userConfig.js"; | ||
|
||
| import { execSync } from "child_process"; | ||
| import { OPTIONS } from "../src/common/config/argsParserOptions.js"; | ||
| import type { z as z4 } from "zod/v4"; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
|
|
@@ -54,19 +54,69 @@ interface ConfigMetadata { | |
| defaultValue?: unknown; | ||
| defaultValueDescription?: string; | ||
| isSecret?: boolean; | ||
| type: "string" | "number" | "boolean" | "array"; | ||
| } | ||
|
|
||
| /** | ||
| * Derives the primitive type from a Zod schema by unwrapping wrappers like default, optional, preprocess, etc. | ||
| */ | ||
| function deriveZodType(schema: z4.ZodType): "string" | "number" | "boolean" | "array" { | ||
| const def = schema.def as unknown as Record<string, unknown>; | ||
| const typeName = def.type as string; | ||
|
|
||
| // Handle wrapped types (default, optional, nullable, etc.) | ||
| if (typeName === "default" || typeName === "optional" || typeName === "nullable") { | ||
| const innerType = def.innerType as z4.ZodType; | ||
| return deriveZodType(innerType); | ||
| } | ||
|
|
||
| // Handle preprocess - look at the schema being processed into | ||
| if (typeName === "pipe") { | ||
| const out = def.out as z4.ZodType; | ||
| return deriveZodType(out); | ||
| } | ||
|
|
||
| // Handle coerce wrapper | ||
| if (typeName === "coerce") { | ||
| const innerType = def.innerType as z4.ZodType; | ||
| return deriveZodType(innerType); | ||
| } | ||
|
|
||
| // Handle primitive types | ||
| if (typeName === "string" || typeName === "enum") { | ||
| return "string"; | ||
| } | ||
| if (typeName === "number" || typeName === "int") { | ||
| return "number"; | ||
| } | ||
| if (typeName === "boolean") { | ||
| return "boolean"; | ||
| } | ||
| if (typeName === "array") { | ||
| return "array"; | ||
| } | ||
| if (typeName === "object") { | ||
| // Objects are treated as strings (JSON strings) | ||
| return "string"; | ||
| } | ||
|
|
||
| // Default fallback | ||
| return "string"; | ||
| } | ||
|
|
||
| function extractZodDescriptions(): Record<string, ConfigMetadata> { | ||
| const result: Record<string, ConfigMetadata> = {}; | ||
|
|
||
| // Get the shape of the Zod schema | ||
| const shape = UserConfigSchema.shape; | ||
| const shape = UserConfigSchemaWithCliOptions.shape; | ||
|
|
||
| for (const [key, fieldSchema] of Object.entries(shape)) { | ||
| const schema = fieldSchema; | ||
| // Extract description from Zod schema | ||
| let description = schema.description || `Configuration option: ${key}`; | ||
|
|
||
| const derivedType = deriveZodType(schema); | ||
|
|
||
| if ("innerType" in schema.def) { | ||
| // "pipe" is also used for our comma-separated arrays | ||
| if (schema.def.innerType.def.type === "pipe") { | ||
|
|
@@ -93,31 +143,22 @@ function extractZodDescriptions(): Record<string, ConfigMetadata> { | |
| defaultValue, | ||
| defaultValueDescription, | ||
| isSecret, | ||
| type: derivedType, | ||
| }; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] { | ||
| function getArgumentInfo(zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] { | ||
| const argumentInfos: ArgumentInfo[] = []; | ||
| const processedKeys = new Set<string>(); | ||
|
|
||
| // Helper to add env var | ||
| const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => { | ||
| if (processedKeys.has(key)) return; | ||
| processedKeys.add(key); | ||
|
|
||
| for (const [key, metadata] of Object.entries(zodMetadata)) { | ||
| const envVarName = `MDB_MCP_${camelCaseToSnakeCase(key)}`; | ||
|
|
||
| // Get description and default value from Zod metadata | ||
| const metadata = zodMetadata[key] || { | ||
| description: `Configuration option: ${key}`, | ||
| }; | ||
|
|
||
| // Determine format based on type | ||
| let format = type; | ||
| if (type === "array") { | ||
| let format: string = metadata.type; | ||
| if (metadata.type === "array") { | ||
| format = "string"; // Arrays are passed as comma-separated strings | ||
| } | ||
|
|
||
|
|
@@ -131,26 +172,6 @@ function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, Co | |
| defaultValue: metadata.defaultValue, | ||
| defaultValueDescription: metadata.defaultValueDescription, | ||
| }); | ||
| }; | ||
|
|
||
| // Process all string options | ||
| for (const key of options.string) { | ||
| addEnvVar(key, "string"); | ||
| } | ||
|
|
||
| // Process all number options | ||
| for (const key of options.number) { | ||
| addEnvVar(key, "number"); | ||
| } | ||
|
|
||
| // Process all boolean options | ||
| for (const key of options.boolean) { | ||
| addEnvVar(key, "boolean"); | ||
| } | ||
|
|
||
| // Process all array options | ||
| for (const key of options.array) { | ||
| addEnvVar(key, "array"); | ||
| } | ||
|
|
||
| // Sort by name for consistent output | ||
|
|
@@ -270,6 +291,9 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { | |
| case "string": | ||
| defaultValueString = `\`"${defaultValue}"\``; | ||
| break; | ||
| case "object": | ||
| defaultValueString = `\`${JSON.stringify(defaultValue)}\``; | ||
| break; | ||
| default: | ||
| throw new Error(`Unsupported default value type: ${typeof defaultValue}`); | ||
| } | ||
|
|
@@ -307,7 +331,7 @@ function updateReadmeConfigTable(envVars: ArgumentInfo[]): void { | |
| function main(): void { | ||
| const zodMetadata = extractZodDescriptions(); | ||
|
|
||
| const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata); | ||
| const argumentInfo = getArgumentInfo(zodMetadata); | ||
| updateServerJsonEnvVars(argumentInfo); | ||
| updateReadmeConfigTable(argumentInfo); | ||
| } | ||
|
|
||
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.
I like yalc so would be great if this is gitgnored