fix: wrap long-flag array values
This commit is contained in:
parent
25e68730a9
commit
41fa8cb06e
@ -7,7 +7,7 @@
|
||||
- Return a non-zero exit code when MCP tool results are marked `isError`, and preserve that status through the forced-exit cleanup path. (PR #154 / issue #153, thanks @jlapenna)
|
||||
- Give forced-exit cleanup a short stdout/stderr flush window so large JSON output is not truncated when `mcporter` is run from `child_process`. (PR #151 / issue #145, thanks @yuhp)
|
||||
- Treat `key:=value` as a compatibility alias for `key=value`, avoiding malformed keys such as `price:`. (PR #150 / issue #100, thanks @solomonneas)
|
||||
- Restore `mcporter call --key value` / `--key=value` tool arguments, including JSON array/object coercion, `--json -` stdin payloads, and kebab-case to camelCase field mapping. (Issues #119 and #126)
|
||||
- Restore `mcporter call --key value` / `--key=value` tool arguments, including JSON array/object coercion, `--json -` stdin payloads, schema-aware bare string-to-array wrapping, and kebab-case to camelCase field mapping. (Issues #119 and #126)
|
||||
- Quote generated `emit-ts` members for tool names that are not valid TypeScript identifiers. (PR #149 / issue #30, thanks @solomonneas)
|
||||
|
||||
### Config
|
||||
|
||||
@ -70,6 +70,7 @@ Key details:
|
||||
- `--key value`, `--key=value`, `key=value`, `key:value`, `key: value`, and `key:=value` all map to the same named-argument handling, so you can type whichever feels most natural for your shell. Long flag keys convert kebab-case to camelCase (`--save-to-drafts true` becomes `saveToDrafts: true`). The `:=` form is accepted as a compatibility alias for `=`.
|
||||
- By default, arguments keep the same validation pipeline as the function-call syntax—enums, numbers, and booleans are coerced automatically, and missing required fields raise errors.
|
||||
- `--args -` and `--json -` read a JSON object from stdin.
|
||||
- Bare string values supplied via long flags wrap into one-item arrays when the tool schema declares that field as an array.
|
||||
- Numeric-looking `key=value` arguments are restored to their original string spelling when the tool schema declares that parameter as a string, which keeps timestamp-like IDs such as Slack `thread_ts=1234567890.123456` intact.
|
||||
- `--raw-strings` disables numeric coercion for flag-style and positional values so IDs/codes stay literal strings (`code=12345` stays `"12345"`).
|
||||
- `--no-coerce` disables all coercion for flag-style and positional values (`true`, `null`, and JSON-like values remain strings).
|
||||
|
||||
@ -20,6 +20,7 @@ export interface CallArgsParseResult {
|
||||
tool?: string;
|
||||
args: Record<string, unknown>;
|
||||
schemaStringCoercionCandidates?: Record<string, string>;
|
||||
schemaArrayCoercionCandidates?: Record<string, string>;
|
||||
positionalArgs?: unknown[];
|
||||
tailLog: boolean;
|
||||
output: OutputFormat;
|
||||
@ -323,6 +324,9 @@ function handleNamedArgumentFlag(context: FlagHandlerContext): number {
|
||||
if (context.state.coercionMode === 'default' && typeof value === 'number') {
|
||||
context.result.schemaStringCoercionCandidates ??= {};
|
||||
context.result.schemaStringCoercionCandidates[key] = rawValue;
|
||||
} else if (context.state.coercionMode === 'default' && typeof value === 'string') {
|
||||
context.result.schemaArrayCoercionCandidates ??= {};
|
||||
context.result.schemaArrayCoercionCandidates[key] = rawValue;
|
||||
}
|
||||
context.result.args[key] = value;
|
||||
return context.index + (eqIndex === -1 ? 2 : 1);
|
||||
|
||||
@ -63,12 +63,13 @@ async function prepareCallRequest(runtime: Runtime, args: string[]): Promise<Pre
|
||||
|
||||
const timeoutMs = resolveCallTimeout(parsed.timeoutMs);
|
||||
const hydratedArgs = await hydratePositionalArguments(runtime, server, tool, parsed.args, parsed.positionalArgs);
|
||||
const schemaAwareArgs = await enforceSchemaStringTypes(
|
||||
const schemaAwareArgs = await enforceSchemaAwareArgumentTypes(
|
||||
runtime,
|
||||
server,
|
||||
tool,
|
||||
hydratedArgs,
|
||||
parsed.schemaStringCoercionCandidates,
|
||||
parsed.schemaArrayCoercionCandidates,
|
||||
timeoutMs
|
||||
);
|
||||
return { parsed, server, tool, hydratedArgs: schemaAwareArgs, timeoutMs };
|
||||
@ -272,15 +273,19 @@ function resolveCallTarget(
|
||||
return { server, tool };
|
||||
}
|
||||
|
||||
async function enforceSchemaStringTypes(
|
||||
async function enforceSchemaAwareArgumentTypes(
|
||||
runtime: Awaited<ReturnType<(typeof import('../runtime.js'))['createRuntime']>>,
|
||||
server: string,
|
||||
tool: string,
|
||||
args: Record<string, unknown>,
|
||||
rawCandidates: Record<string, string> | undefined,
|
||||
stringCandidates: Record<string, string> | undefined,
|
||||
arrayCandidates: Record<string, string> | undefined,
|
||||
timeoutMs: number
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!rawCandidates || Object.keys(rawCandidates).length === 0) {
|
||||
if (
|
||||
(!stringCandidates || Object.keys(stringCandidates).length === 0) &&
|
||||
(!arrayCandidates || Object.keys(arrayCandidates).length === 0)
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -297,7 +302,7 @@ async function enforceSchemaStringTypes(
|
||||
}
|
||||
|
||||
let corrected: Record<string, unknown> | undefined;
|
||||
for (const [key, rawValue] of Object.entries(rawCandidates)) {
|
||||
for (const [key, rawValue] of Object.entries(stringCandidates ?? {})) {
|
||||
if (typeof args[key] !== 'number') {
|
||||
continue;
|
||||
}
|
||||
@ -307,6 +312,17 @@ async function enforceSchemaStringTypes(
|
||||
corrected ??= { ...args };
|
||||
corrected[key] = rawValue;
|
||||
}
|
||||
for (const [key, rawValue] of Object.entries(arrayCandidates ?? {})) {
|
||||
if (typeof args[key] !== 'string') {
|
||||
continue;
|
||||
}
|
||||
const descriptor = schema.properties[key];
|
||||
if (!schemaAllowsArray(descriptor) || schemaAllowsString(descriptor)) {
|
||||
continue;
|
||||
}
|
||||
corrected ??= { ...args };
|
||||
corrected[key] = [rawValue];
|
||||
}
|
||||
return corrected ?? args;
|
||||
}
|
||||
|
||||
@ -331,6 +347,27 @@ function schemaAllowsString(descriptor: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function schemaAllowsArray(descriptor: unknown): boolean {
|
||||
if (!descriptor || typeof descriptor !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const record = descriptor as Record<string, unknown>;
|
||||
const type = record.type;
|
||||
if (type === 'array') {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(type) && type.includes('array')) {
|
||||
return true;
|
||||
}
|
||||
for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {
|
||||
const variants = record[key];
|
||||
if (Array.isArray(variants) && variants.some(schemaAllowsArray)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function hydratePositionalArguments(
|
||||
runtime: Awaited<ReturnType<(typeof import('../runtime.js'))['createRuntime']>>,
|
||||
server: string,
|
||||
|
||||
@ -86,6 +86,41 @@ describe('CLI call execution behavior', () => {
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('wraps bare long-flag strings when the schema declares an array', async () => {
|
||||
const { handleCall } = await cliModulePromise;
|
||||
const { runtime, callTool, listTools } = createRuntimeStub({
|
||||
email: [
|
||||
{
|
||||
name: 'send_email',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
to: { type: 'array', items: { type: 'string' } },
|
||||
subject: { type: 'string' },
|
||||
},
|
||||
required: ['to', 'subject'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await handleCall(runtime, ['email.send_email', '--to', 'miguel@example.com', '--subject', 'Test']);
|
||||
|
||||
expect(callTool).toHaveBeenCalledWith(
|
||||
'email',
|
||||
'send_email',
|
||||
expect.objectContaining({
|
||||
args: {
|
||||
to: ['miguel@example.com'],
|
||||
subject: 'Test',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(listTools).toHaveBeenCalledWith('email', { autoAuthorize: true, includeSchema: true });
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not load schemas for numeric values supplied via --args JSON', async () => {
|
||||
const { handleCall } = await cliModulePromise;
|
||||
const { runtime, callTool, listTools } = createRuntimeStub({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user