diff --git a/CHANGELOG.md b/CHANGELOG.md index b419bef..251f7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/src/cli/call-arguments.ts b/src/cli/call-arguments.ts index ea705a3..425fc6a 100644 --- a/src/cli/call-arguments.ts +++ b/src/cli/call-arguments.ts @@ -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 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:', diff --git a/tests/call-arguments.test.ts b/tests/call-arguments.test.ts index a6d4664..b73e62c 100644 --- a/tests/call-arguments.test.ts +++ b/tests/call-arguments.test.ts @@ -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']); }); });