Skip to content

Commit 1e520f7

Browse files
refactor: validate resolved config with a zod schema
Move the mutually exclusive argument check into a zod superRefine so schema validation is the single final gatekeeper for the resolved configuration. No behavior change.
1 parent 129749b commit 1e520f7

1 file changed

Lines changed: 31 additions & 17 deletions

File tree

‎src/config/mcp-options.ts‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import type {YargsOptions} from '../third_party/index.js';
8-
import {yargs, hideBin} from '../third_party/index.js';
8+
import {yargs, hideBin, zod as z} from '../third_party/index.js';
99
import os from 'node:os';
1010
import {readFileSync} from 'node:fs';
1111
import path from 'node:path';
@@ -610,24 +610,38 @@ export function parseArguments(
610610
console.error(`Unknown arguments: ${unknownArgs.map(arg => `--${arg}`)}`);
611611
}
612612

613-
// Step 5: Validation. Conflicts are only checked against explicit user input
614-
// so that dynamic defaults never conflict.
615-
const activeArgs = new Set<string>();
616-
for (const [key, val] of Object.entries(explicitConfig)) {
617-
if (val !== undefined && val !== false) {
618-
activeArgs.add(key);
619-
}
620-
}
621-
for (const group of CONFLICTING_ARGS) {
622-
const activeInGroup = group.filter(arg => activeArgs.has(arg));
623-
if (activeInGroup.length > 1) {
624-
const [arg1, arg2] = activeInGroup;
625-
throw new Error(`Arguments ${arg1} and ${arg2} are mutually exclusive`);
626-
}
627-
}
613+
// Step 5: Final Schema Validation & Type Safety. Conflicts are only checked
614+
// against explicit user input so that dynamic defaults never conflict.
615+
const ConfigSchema = z
616+
.object({})
617+
.passthrough()
618+
.superRefine((_config, ctx) => {
619+
const activeArgs = new Set<string>();
620+
for (const [key, val] of Object.entries(explicitConfig)) {
621+
if (val !== undefined && val !== false) {
622+
activeArgs.add(key);
623+
}
624+
}
625+
626+
for (const group of CONFLICTING_ARGS) {
627+
const activeInGroup = group.filter(arg => activeArgs.has(arg));
628+
if (activeInGroup.length > 1) {
629+
const [arg1, arg2] = activeInGroup;
630+
ctx.addIssue({
631+
code: z.ZodIssueCode.custom,
632+
message: `Arguments ${arg1} and ${arg2} are mutually exclusive`,
633+
path: [arg1, arg2],
634+
});
635+
}
636+
}
637+
});
628638

639+
const result = ConfigSchema.safeParse(resolvedConfig);
640+
if (!result.success) {
641+
throw new Error(result.error.issues[0].message);
642+
}
629643
// The merge and the default loop lose the static type that yargs infers from
630644
// `mcpOptions`. Every value was produced by the same option definitions (CLI
631645
// parser, strict config-file parser, option defaults), so the shape matches.
632-
return resolvedConfig as ParsedArguments;
646+
return result.data as ParsedArguments;
633647
}

0 commit comments

Comments
 (0)