Polish CLI argument parsing and clean lint warnings

This commit is contained in:
Peter Steinberger 2025-11-05 22:39:37 +00:00
parent 32a8c161a5
commit 189fca2d42
8 changed files with 29 additions and 14 deletions

View File

@ -13,7 +13,7 @@
- Generated CLIs now show full command signatures in help and support `--compile` without leaving template/bundle intermediates.
- StdIO-backed MCP servers now receive resolved environment overrides, so API keys flow through to launched processes like `obsidian-mcp-server`.
- Hardened the CLI generator to surface enum defaults/metadata and added regression tests around the new helper utilities.
- Fixed `mcporter call <server> <tool>` so the second positional is treated as the tool name instead of triggering the "Argument must be key=value" error; positional and `tool=` selectors now play nicely with additional key=value payloads.
- Fixed `mcporter call <server> <tool>` so the second positional is treated as the tool name instead of triggering the "Argument must be key=value" error, and accepted `tool=`/`command=` selectors now play nicely with additional key=value payloads.
## [0.1.0]

View File

@ -4,9 +4,9 @@ import fsPromises from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
import type { ServerSource } from './config.js';
import { generateCli } from './generate-cli.js';
import { createRuntime } from './runtime.js';
import type { ServerSource } from './config.js';
type FlagMap = Partial<Record<string, string>>;
@ -584,7 +584,7 @@ export function parseCallArguments(args: string[]): CallArgsParseResult {
throw new Error(`Argument '${token}' must be key=value format.`);
}
const value = coerceValue(raw);
if (key === 'tool' && !result.tool) {
if ((key === 'tool' || key === 'command') && !result.tool) {
if (typeof value !== 'string') {
throw new Error("Argument 'tool' must be a string value.");
}

View File

@ -796,7 +796,8 @@ function pickExampleValue(option: GeneratedOption): string {
return option.exampleValue;
}
if (option.enumValues && option.enumValues.length > 0) {
return option.enumValues[0]!;
const [first] = option.enumValues;
return first ?? option.property;
}
switch (option.type) {
case 'number':
@ -870,12 +871,6 @@ async function bundleOutput({
return absTarget;
}
function replaceExtension(file: string, extension: string): string {
const dirname = path.dirname(file);
const basename = path.basename(file, path.extname(file));
return path.join(dirname, `${basename}.${extension}`);
}
async function compileBundleWithBun(bundlePath: string, outputPath: string): Promise<void> {
const bunBin = await verifyBunAvailable();
await new Promise<void>((resolve, reject) => {

View File

@ -20,6 +20,14 @@ describe('CLI call argument parsing', () => {
expect(parsed.args).toEqual({});
});
it('treats command=NAME tokens as a tool alias for compatibility', async () => {
const { parseCallArguments } = await cliModulePromise;
const parsed = parseCallArguments(['chrome-devtools', 'command=list_pages']);
expect(parsed.selector).toBe('chrome-devtools');
expect(parsed.tool).toBe('list_pages');
expect(parsed.args).toEqual({});
});
it('retains key=value arguments after the selector and tool', async () => {
const { parseCallArguments } = await cliModulePromise;
const parsed = parseCallArguments(['chrome-devtools', 'list_pages', 'timeout=500']);

View File

@ -41,6 +41,9 @@ describe('command string parsing', () => {
expect(servers).toHaveLength(1);
const server = servers[0];
if (!server) {
throw new Error('expected server definition');
}
expect(server.command.kind).toBe('stdio');
if (server.command.kind !== 'stdio') {
throw new Error('expected stdio command');
@ -70,11 +73,15 @@ describe('command string parsing', () => {
})
);
const [server] = await loadServerDefinitions({
const servers = await loadServerDefinitions({
configPath,
rootDir: tmpDir,
});
const server = servers[0];
if (!server) {
throw new Error('expected server definition');
}
expect(server.command.kind).toBe('stdio');
if (server.command.kind !== 'stdio') {
throw new Error('expected stdio command');

View File

@ -6,7 +6,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import express from 'express';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { z } from 'zod';
import { __test as generateCliInternals, generateCli } from '../src/generate-cli.js';
import { generateCli, __test as generateCliInternals } from '../src/generate-cli.js';
let baseUrl: URL;
const tmpDir = path.join(process.cwd(), 'tmp', 'mcporter-cli-tests');

View File

@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { Runtime, ServerToolInfo } from '../src/runtime.js';
import type { CallResult } from '../src/index.js';
import { createServerProxy } from '../src/index.js';
import type { Runtime, ServerToolInfo } from '../src/runtime.js';
type CallLogEntry = {
server: string;
@ -55,7 +55,10 @@ function createComposableRuntime() {
callLog.push({ server, tool: toolName, options });
if (server === 'docs' && toolName === 'lookup') {
const query = typeof options?.args === 'object' && options?.args !== null ? (options.args as { query?: string }).query : undefined;
const query =
typeof options?.args === 'object' && options?.args !== null
? (options.args as { query?: string }).query
: undefined;
return {
content: [
{

View File

@ -233,7 +233,9 @@ describe('stdio transport environment', () => {
cwd: '/repo',
},
env: {
// biome-ignore lint/suspicious/noTemplateCurlyInString: placeholders resolve against process env at runtime
OBSIDIAN_API_KEY: '${OBSIDIAN_API_KEY}',
// biome-ignore lint/suspicious/noTemplateCurlyInString: placeholders resolve against process env at runtime
OBSIDIAN_BASE_URL: '${OBSIDIAN_BASE_URL:-https://127.0.0.1:27124}',
EMPTY_VAR: '',
},