Add --raw-strings flag to prevent numeric string coercion

Values like phone numbers, PINs, or codes with leading zeros (e.g. 000123)
are auto-coerced to numbers by default. The --raw-strings flag (or --no-coerce
alias) preserves them as strings.

Note: Cannot use --raw as the flag name because it conflicts with the
existing --raw output format shortcut in output-format.ts.

Fixes: numeric strings being silently converted to numbers
This commit is contained in:
nobrainer-tech 2026-02-04 22:37:11 +01:00 committed by Peter Steinberger
parent 30abe3d9a5
commit 3325d32d03
2 changed files with 36 additions and 4 deletions

View File

@ -17,6 +17,7 @@ export interface CallArgsParseResult {
output: OutputFormat;
timeoutMs?: number;
ephemeral?: EphemeralServerSpec;
rawStrings?: boolean;
}
export function parseCallArguments(args: string[]): CallArgsParseResult {
@ -68,6 +69,11 @@ export function parseCallArguments(args: string[]): CallArgsParseResult {
index += 1;
continue;
}
if (token === '--raw-strings' || token === '--no-coerce') {
result.rawStrings = true;
index += 1;
continue;
}
if (token === '--args') {
const value = args[index + 1];
if (!value) {
@ -168,12 +174,12 @@ export function parseCallArguments(args: string[]): CallArgsParseResult {
}
const parsed = parseKeyValueToken(token, positional[index + 1]);
if (!parsed) {
trailingPositional.push(coerceValue(token));
trailingPositional.push(coerceValue(token, result.rawStrings));
index += 1;
continue;
}
index += parsed.consumed;
const value = coerceValue(parsed.rawValue);
const value = coerceValue(parsed.rawValue, result.rawStrings);
if (parsed.key === 'tool' && !result.tool) {
if (typeof value !== 'string') {
throw new Error("Argument 'tool' must be a string value.");
@ -273,7 +279,7 @@ function extractHttpCallExpression(raw: string): ReturnType<typeof parseCallExpr
};
}
function coerceValue(value: string): unknown {
function coerceValue(value: string, rawStrings = false): unknown {
const trimmed = value.trim();
if (trimmed === '') {
return '';
@ -284,7 +290,8 @@ function coerceValue(value: string): unknown {
if (trimmed === 'null' || trimmed === 'none') {
return null;
}
if (!Number.isNaN(Number(trimmed)) && trimmed === `${Number(trimmed)}`) {
// Skip numeric coercion when --raw-strings (or --no-coerce) flag is used
if (!rawStrings && !Number.isNaN(Number(trimmed)) && trimmed === `${Number(trimmed)}`) {
return Number(trimmed);
}
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {

View File

@ -49,4 +49,29 @@ describe('parseCallArguments', () => {
);
warnSpy.mockRestore();
});
it('coerces numeric strings to numbers by default', () => {
const parsed = parseCallArguments(['server.tool', 'code=123456']);
expect(parsed.args.code).toBe(123456);
expect(typeof parsed.args.code).toBe('number');
});
it('preserves numeric strings when --raw-strings flag is used', () => {
const parsed = parseCallArguments(['--raw-strings', 'server.tool', 'code=123456']);
expect(parsed.args.code).toBe('123456');
expect(typeof parsed.args.code).toBe('string');
expect(parsed.rawStrings).toBe(true);
});
it('preserves leading zeros when --raw-strings flag is used', () => {
const parsed = parseCallArguments(['--raw-strings', 'server.tool', 'pin=000123']);
expect(parsed.args.pin).toBe('000123');
expect(typeof parsed.args.pin).toBe('string');
});
it('preserves numeric strings when --no-coerce alias is used', () => {
const parsed = parseCallArguments(['--no-coerce', 'server.tool', 'id=007']);
expect(parsed.args.id).toBe('007');
expect(typeof parsed.args.id).toBe('string');
});
});