Skip to content

Commit ecbbb38

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 a1e6200 commit ecbbb38

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';
@@ -631,24 +631,38 @@ export function parseArguments(
631631
console.error(`Unknown arguments: ${unknownArgs.map(arg => `--${arg}`)}`);
632632
}
633633

634-
// Step 5: Validation. Conflicts are only checked against explicit user input
635-
// so that dynamic defaults never conflict.
636-
const activeArgs = new Set<string>();
637-
for (const [key, val] of Object.entries(explicitConfig)) {
638-
if (val !== undefined && val !== false) {
639-
activeArgs.add(key);
640-
}
641-
}
642-
for (const group of CONFLICTING_ARGS) {
643-
const activeInGroup = group.filter(arg => activeArgs.has(arg));
644-
if (activeInGroup.length > 1) {
645-
const [arg1, arg2] = activeInGroup;
646-
throw new Error(`Arguments ${arg1} and ${arg2} are mutually exclusive`);
647-
}
648-
}
634+
// Step 5: Final Schema Validation & Type Safety. Conflicts are only checked
635+
// against explicit user input so that dynamic defaults never conflict.
636+
const ConfigSchema = z
637+
.object({})
638+
.passthrough()
639+
.superRefine((_config, ctx) => {
640+
const activeArgs = new Set<string>();
641+
for (const [key, val] of Object.entries(explicitConfig)) {
642+
if (val !== undefined && val !== false) {
643+
activeArgs.add(key);
644+
}
645+
}
646+
647+
for (const group of CONFLICTING_ARGS) {
648+
const activeInGroup = group.filter(arg => activeArgs.has(arg));
649+
if (activeInGroup.length > 1) {
650+
const [arg1, arg2] = activeInGroup;
651+
ctx.addIssue({
652+
code: z.ZodIssueCode.custom,
653+
message: `Arguments ${arg1} and ${arg2} are mutually exclusive`,
654+
path: [arg1, arg2],
655+
});
656+
}
657+
}
658+
});
649659

660+
const result = ConfigSchema.safeParse(resolvedConfig);
661+
if (!result.success) {
662+
throw new Error(result.error.issues[0].message);
663+
}
650664
// The merge and the default loop lose the static type that yargs infers from
651665
// `mcpOptions`. Every value was produced by the same option definitions (CLI
652666
// parser, strict config-file parser, option defaults), so the shape matches.
653-
return resolvedConfig as ParsedArguments;
667+
return result.data as ParsedArguments;
654668
}

0 commit comments

Comments
 (0)