fix: coerce array option items
This commit is contained in:
parent
d2f0de130b
commit
ecfbaee7f9
@ -2,7 +2,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
No unreleased changes yet.
|
||||
### CLI
|
||||
- Coerce generated CLI array arguments based on JSON Schema item types (including integer arrays).
|
||||
|
||||
### Tests
|
||||
- Added regression coverage for typed array parsing in generated CLIs.
|
||||
|
||||
## [0.7.2] - 2025-12-29
|
||||
### CLI
|
||||
|
||||
@ -224,8 +224,27 @@ export function inferType(descriptor: unknown): GeneratedOption['type'] {
|
||||
return 'unknown';
|
||||
}
|
||||
const type = (descriptor as Record<string, unknown>).type;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'array') {
|
||||
return type;
|
||||
const resolveType = (value: unknown): GeneratedOption['type'] | undefined => {
|
||||
if (value === 'integer') {
|
||||
return 'number';
|
||||
}
|
||||
if (value === 'string' || value === 'number' || value === 'boolean' || value === 'array') {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
if (Array.isArray(type)) {
|
||||
for (const entry of type) {
|
||||
const resolved = resolveType(entry);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
const resolved = resolveType(type);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
@ -240,8 +259,27 @@ export function inferArrayItemType(descriptor: unknown): GeneratedOption['arrayI
|
||||
}
|
||||
const items = record.items as Record<string, unknown>;
|
||||
const itemType = items.type;
|
||||
if (itemType === 'string' || itemType === 'number' || itemType === 'boolean') {
|
||||
return itemType;
|
||||
const resolveItemType = (value: unknown): GeneratedOption['arrayItemType'] | undefined => {
|
||||
if (value === 'integer') {
|
||||
return 'number';
|
||||
}
|
||||
if (value === 'string' || value === 'number' || value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
if (Array.isArray(itemType)) {
|
||||
for (const entry of itemType) {
|
||||
const resolved = resolveItemType(entry);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
const resolved = resolveItemType(itemType);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
getDescriptorDescription,
|
||||
getDescriptorFormatHint,
|
||||
getEnumValues,
|
||||
inferArrayItemType,
|
||||
inferType,
|
||||
pickExampleLiteral,
|
||||
toCliOption,
|
||||
@ -89,8 +90,15 @@ describe('generate helpers', () => {
|
||||
expect(buildExampleValue('mode', 'string', ['fast'], undefined)).toBe('fast');
|
||||
|
||||
expect(inferType({ type: 'boolean' })).toBe('boolean');
|
||||
expect(inferType({ type: 'integer' })).toBe('number');
|
||||
expect(inferType({ type: ['null', 'integer'] })).toBe('number');
|
||||
expect(inferType({ type: ['null', 'array'] })).toBe('array');
|
||||
expect(inferType({})).toBe('unknown');
|
||||
|
||||
expect(inferArrayItemType({ type: 'array', items: { type: 'integer' } })).toBe('number');
|
||||
expect(inferArrayItemType({ type: 'array', items: { type: ['null', 'boolean'] } })).toBe('boolean');
|
||||
expect(inferArrayItemType({ type: 'array', items: { type: 'object' } })).toBe('unknown');
|
||||
|
||||
expect(getDescriptorDescription({ description: 'hi' })).toBe('hi');
|
||||
expect(getDescriptorDescription({})).toBeUndefined();
|
||||
expect(getDescriptorFormatHint({ format: 'uuid' })).toEqual({ display: 'UUID', slug: 'uuid' });
|
||||
|
||||
@ -72,6 +72,29 @@ if (process.platform !== 'win32') {
|
||||
structuredContent: { ok: true },
|
||||
})
|
||||
);
|
||||
server.registerTool(
|
||||
'array_tool',
|
||||
{
|
||||
title: 'Array Tool',
|
||||
description: 'Tool with typed array inputs',
|
||||
inputSchema: {
|
||||
coords: z.array(z.number()),
|
||||
flags: z.array(z.boolean()),
|
||||
names: z.array(z.string()),
|
||||
ids: z.array(z.number().int()),
|
||||
},
|
||||
outputSchema: {
|
||||
coords: z.array(z.number()),
|
||||
flags: z.array(z.boolean()),
|
||||
names: z.array(z.string()),
|
||||
ids: z.array(z.number().int()),
|
||||
},
|
||||
},
|
||||
async ({ coords, flags, names, ids }) => ({
|
||||
content: [{ type: 'text', text: JSON.stringify({ coords, flags, names, ids }) }],
|
||||
structuredContent: { coords, flags, names, ids },
|
||||
})
|
||||
);
|
||||
server.registerResource(
|
||||
'greeting',
|
||||
new ResourceTemplate('greeting://{name}', { list: undefined }),
|
||||
@ -365,6 +388,65 @@ describeGenerateCli('generateCli', () => {
|
||||
expect(content).not.toContain('cmdOpts.tls_1_3');
|
||||
});
|
||||
|
||||
it('coerces array option values using schema item types', async () => {
|
||||
const inline = JSON.stringify({
|
||||
name: 'array-test',
|
||||
description: 'Array coercion test',
|
||||
command: baseUrl.toString(),
|
||||
});
|
||||
const outputPath = path.join(tmpDir, 'array-test.ts');
|
||||
await fs.rm(outputPath, { force: true });
|
||||
|
||||
const { outputPath: renderedPath } = await generateCli({
|
||||
serverRef: inline,
|
||||
outputPath,
|
||||
runtime: 'node',
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
expect(renderedPath).toBe(outputPath);
|
||||
|
||||
const { execFile } = await import('node:child_process');
|
||||
const { stdout } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
execFile(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'tsx',
|
||||
renderedPath,
|
||||
'array-tool',
|
||||
'--coords',
|
||||
'1, 2.5',
|
||||
'--flags',
|
||||
'true, false',
|
||||
'--names',
|
||||
'alpha, beta',
|
||||
'--ids',
|
||||
'1,2,3',
|
||||
'--output',
|
||||
'json',
|
||||
],
|
||||
execOptions(),
|
||||
(error: import('node:child_process').ExecFileException | null, out: string, err: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: out, stderr: err });
|
||||
}
|
||||
);
|
||||
});
|
||||
const parsed = JSON.parse(stdout) as {
|
||||
coords: number[];
|
||||
flags: boolean[];
|
||||
names: string[];
|
||||
ids: number[];
|
||||
};
|
||||
expect(parsed.coords).toEqual([1, 2.5]);
|
||||
expect(parsed.flags).toEqual([true, false]);
|
||||
expect(parsed.names).toEqual(['alpha', 'beta']);
|
||||
expect(parsed.ids).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('accepts both kebab-case and underscore tool names for generated CLIs', async () => {
|
||||
const deepwikiRef = JSON.stringify({
|
||||
name: 'deepwiki',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user