fix: improve generated CLI object parsing (#114) (thanks @v2nic)
This commit is contained in:
parent
855446ca39
commit
b8b90f221f
@ -3,6 +3,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### CLI
|
||||
- Generated CLIs now parse object-valued flags as JSON and render object placeholders/examples with JSON-shaped help text, so tools like Jira `fields` no longer receive raw strings. (PR #114, thanks @v2nic)
|
||||
- Deduplicate concurrent keep-alive daemon restarts per server so repeated fatal errors only force-close the cached daemon transport once before retrying. (PR #125, thanks @zm2231)
|
||||
- Keep `mcporter call --output json` parseable by emitting valid JSON even when the command falls back to raw output. (PR #128, thanks @armanddp)
|
||||
- Ignore static `Authorization` headers once OAuth is active so imported editor configs cannot override fresh OAuth tokens. (PR #123, thanks @ahonn)
|
||||
- Preserve full JSON/error payloads when `data` is just one field instead of collapsing the response to `data` alone. (PR #106, thanks @AielloChan)
|
||||
|
||||
@ -126,6 +126,8 @@ export function buildPlaceholder(
|
||||
return `<${normalized}:true|false>`;
|
||||
case 'array':
|
||||
return `<${normalized}:value1,value2>`;
|
||||
case 'object':
|
||||
return `<${normalized}:json>`;
|
||||
default:
|
||||
if (formatSlug) {
|
||||
return `<${normalized}:${formatSlug}>`;
|
||||
@ -157,6 +159,8 @@ export function buildExampleValue(
|
||||
return 'true';
|
||||
case 'array':
|
||||
return 'value1,value2';
|
||||
case 'object':
|
||||
return '{"key":"value"}';
|
||||
default:
|
||||
if (property.toLowerCase().includes('path')) {
|
||||
return '/path/to/file.md';
|
||||
@ -185,7 +189,7 @@ export function pickExampleLiteral(option: GeneratedOption): string | undefined
|
||||
}
|
||||
return `[${values.map((entry) => JSON.stringify(entry)).join(', ')}]`;
|
||||
}
|
||||
if (option.type === 'number' || option.type === 'boolean') {
|
||||
if (option.type === 'number' || option.type === 'boolean' || option.type === 'object') {
|
||||
return option.exampleValue;
|
||||
}
|
||||
try {
|
||||
@ -207,6 +211,8 @@ export function buildFallbackLiteral(option: GeneratedOption): string {
|
||||
return 'true';
|
||||
case 'array':
|
||||
return '["value1"]';
|
||||
case 'object':
|
||||
return '{"key":"value"}';
|
||||
default: {
|
||||
if (option.property.toLowerCase().includes('id')) {
|
||||
return JSON.stringify('example-id');
|
||||
|
||||
@ -192,6 +192,9 @@ function formatTypeAnnotation(option: GeneratedOption, colorize: boolean): strin
|
||||
case 'array':
|
||||
baseType = 'string[]';
|
||||
break;
|
||||
case 'object':
|
||||
baseType = 'Record<string, unknown>';
|
||||
break;
|
||||
case 'string':
|
||||
baseType = 'string';
|
||||
break;
|
||||
|
||||
@ -86,13 +86,16 @@ describe('generate helpers', () => {
|
||||
|
||||
expect(buildPlaceholder('myPath', 'string', ['s1', 's2'])).toBe('<my-path:s1|s2>');
|
||||
expect(buildPlaceholder('createdAt', 'string', undefined, 'iso-8601')).toBe('<created-at:iso-8601>');
|
||||
expect(buildPlaceholder('fields', 'object')).toBe('<fields:json>');
|
||||
expect(buildExampleValue('itemId', 'string', undefined, undefined)).toBe('example-id');
|
||||
expect(buildExampleValue('mode', 'string', ['fast'], undefined)).toBe('fast');
|
||||
expect(buildExampleValue('fields', 'object', undefined, undefined)).toBe('{"key":"value"}');
|
||||
|
||||
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({ type: 'object' })).toBe('object');
|
||||
expect(inferType({})).toBe('unknown');
|
||||
|
||||
expect(inferArrayItemType({ type: 'array', items: { type: 'integer' } })).toBe('number');
|
||||
@ -158,5 +161,14 @@ describe('generate helpers', () => {
|
||||
placeholder: '<labels>',
|
||||
})
|
||||
).toBe('["value1"]');
|
||||
expect(
|
||||
buildFallbackLiteral({
|
||||
type: 'object',
|
||||
property: 'fields',
|
||||
cliName: 'fields',
|
||||
required: false,
|
||||
placeholder: '<fields>',
|
||||
})
|
||||
).toBe('{"key":"value"}');
|
||||
});
|
||||
});
|
||||
|
||||
@ -96,6 +96,23 @@ if (process.platform !== 'win32') {
|
||||
structuredContent: { coords, flags, names, ids },
|
||||
})
|
||||
);
|
||||
server.registerTool(
|
||||
'object_tool',
|
||||
{
|
||||
title: 'Object Tool',
|
||||
description: 'Tool with object input',
|
||||
inputSchema: {
|
||||
fields: z.record(z.string(), z.unknown()),
|
||||
},
|
||||
outputSchema: {
|
||||
fields: z.record(z.string(), z.unknown()),
|
||||
},
|
||||
},
|
||||
async ({ fields }) => ({
|
||||
content: [{ type: 'text', text: JSON.stringify({ fields }) }],
|
||||
structuredContent: { fields },
|
||||
})
|
||||
);
|
||||
server.registerResource(
|
||||
'greeting',
|
||||
new ResourceTemplate('greeting://{name}', { list: undefined }),
|
||||
@ -448,6 +465,60 @@ describeGenerateCli('generateCli', () => {
|
||||
expect(parsed.ids).toEqual([1, 2, 3]);
|
||||
}, 30_000);
|
||||
|
||||
it('parses object option values as JSON in generated CLIs', async () => {
|
||||
const inline = JSON.stringify({
|
||||
name: 'object-test',
|
||||
description: 'Object parsing test',
|
||||
command: baseUrl.toString(),
|
||||
});
|
||||
const outputPath = path.join(tmpDir, 'object-test.ts');
|
||||
await fs.rm(outputPath, { force: true });
|
||||
|
||||
const { outputPath: renderedPath } = await generateCli({
|
||||
serverRef: inline,
|
||||
outputPath,
|
||||
runtime: 'node',
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
const content = await fs.readFile(renderedPath, 'utf8');
|
||||
|
||||
expect(content).toContain('--fields <fields:json>');
|
||||
expect(content).toContain('(value) => JSON.parse(value)');
|
||||
expect(content).toContain('"object-tool": "function object_tool(fields: Record<string, unknown>): object;"');
|
||||
|
||||
const { execFile } = await import('node:child_process');
|
||||
const { stdout } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
execFile(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'tsx',
|
||||
renderedPath,
|
||||
'object-tool',
|
||||
'--fields',
|
||||
'{"summary":"Ship it","done":true}',
|
||||
'--output',
|
||||
'json',
|
||||
],
|
||||
execOptions(),
|
||||
(error: import('node:child_process').ExecFileException | null, out: string, err: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: out, stderr: err });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
fields: {
|
||||
done: true,
|
||||
summary: 'Ship it',
|
||||
},
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
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