fix: align --raw-strings and --no-coerce semantics (#59) (thanks @nobrainer-tech)

This commit is contained in:
Peter Steinberger 2026-03-02 21:35:33 +00:00
parent 079b217f63
commit 3a2952095d
4 changed files with 42 additions and 8 deletions

View File

@ -8,6 +8,7 @@
- Added optional `oauthScope`/`oauth_scope` config override as an escape hatch for providers that require explicit scopes.
- `createCallResult().json()` now collects all parseable JSON entries from MCP content arrays (single item stays backward-compatible), and raw inspect depth now stays readable without unbounded traversal. (PR #91, thanks @Blankdlh)
- OAuth wait/redirect now share one deferred to eliminate authorization race windows and preserve stable close-path errors, including wait-before-redirect and repeated-redirect flows. (PR #70, thanks @monotykamary)
- Added `--raw-strings` (numeric coercion off) and `--no-coerce` (all coercion off) for `mcporter call` argument parsing so IDs/codes can stay literal strings. (PR #59, thanks @nobrainer-tech)
### Tooling / Dependencies
- Updated dependencies to latest releases (including MCP SDK, Rolldown RC, Zod, Biome, Oxlint, Vitest, Bun types).

View File

@ -20,8 +20,11 @@ export interface CallArgsParseResult {
rawStrings?: boolean;
}
type CoercionMode = 'default' | 'raw-strings' | 'none';
export function parseCallArguments(args: string[]): CallArgsParseResult {
const result: CallArgsParseResult = { args: {}, tailLog: false, output: 'auto' };
let coercionMode: CoercionMode = 'default';
const ephemeral = extractEphemeralServerFlags(args);
result.ephemeral = ephemeral;
result.output = consumeOutputFormat(args, {
@ -69,7 +72,14 @@ export function parseCallArguments(args: string[]): CallArgsParseResult {
index += 1;
continue;
}
if (token === '--raw-strings' || token === '--no-coerce') {
if (token === '--raw-strings') {
coercionMode = 'raw-strings';
result.rawStrings = true;
index += 1;
continue;
}
if (token === '--no-coerce') {
coercionMode = 'none';
result.rawStrings = true;
index += 1;
continue;
@ -174,12 +184,12 @@ export function parseCallArguments(args: string[]): CallArgsParseResult {
}
const parsed = parseKeyValueToken(token, positional[index + 1]);
if (!parsed) {
trailingPositional.push(coerceValue(token, result.rawStrings));
trailingPositional.push(coerceValue(token, coercionMode));
index += 1;
continue;
}
index += parsed.consumed;
const value = coerceValue(parsed.rawValue, result.rawStrings);
const value = coerceValue(parsed.rawValue, coercionMode);
if (parsed.key === 'tool' && !result.tool) {
if (typeof value !== 'string') {
throw new Error("Argument 'tool' must be a string value.");
@ -279,19 +289,21 @@ function extractHttpCallExpression(raw: string): ReturnType<typeof parseCallExpr
};
}
function coerceValue(value: string, rawStrings = false): unknown {
function coerceValue(value: string, coercionMode: CoercionMode = 'default'): unknown {
const trimmed = value.trim();
if (trimmed === '') {
return '';
}
if (coercionMode === 'none') {
return trimmed;
}
if (trimmed === 'true' || trimmed === 'false') {
return trimmed === 'true';
}
if (trimmed === 'null' || trimmed === 'none') {
return null;
}
// Skip numeric coercion when --raw-strings (or --no-coerce) flag is used
if (!rawStrings && !Number.isNaN(Number(trimmed)) && trimmed === `${Number(trimmed)}`) {
if (coercionMode === 'default' && !Number.isNaN(Number(trimmed)) && trimmed === `${Number(trimmed)}`) {
return Number(trimmed);
}
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {

View File

@ -130,6 +130,8 @@ export function printCallHelp(): void {
'Runtime flags:',
' --timeout <ms> Override the call timeout.',
' --output text|markdown|json|raw Control formatting.',
' --raw-strings Keep numeric-looking argument values as strings.',
' --no-coerce Keep all key/value and positional arguments as raw strings.',
' --tail-log Stream returned log handles.',
'',
'Ad-hoc servers:',

View File

@ -69,9 +69,28 @@ describe('parseCallArguments', () => {
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']);
it('still coerces booleans, nulls, and JSON with --raw-strings', () => {
const parsed = parseCallArguments(['--raw-strings', 'server.tool', 'enabled=true', 'value=null', 'meta={"a":1}']);
expect(parsed.args.enabled).toBe(true);
expect(parsed.args.value).toBeNull();
expect(parsed.args.meta).toEqual({ a: 1 });
});
it('keeps every value as a string when --no-coerce alias is used', () => {
const parsed = parseCallArguments([
'--no-coerce',
'server.tool',
'id=007',
'enabled=true',
'value=null',
'meta={"a":1}',
'123',
]);
expect(parsed.args.id).toBe('007');
expect(parsed.args.enabled).toBe('true');
expect(parsed.args.value).toBe('null');
expect(parsed.args.meta).toBe('{"a":1}');
expect(typeof parsed.args.id).toBe('string');
expect(parsed.positionalArgs).toEqual(['123']);
});
});