From 0ca8246592248d7020a9b862e4f64431ca58fb1f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 14:36:57 +0000 Subject: [PATCH] Accept `mcpc --skill` as an alias of `mcpc help --skill` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents reach for the bare flag first, and an unknown-option error there is a dead end. A bare `--skill` (no positional, no `--help`) is rewritten to `help --skill` before routing, so it prints the identical guide and reuses the existing error handling. Anything else — `mcpc --skill connect`, `mcpc @s --skill`, `mcpc help --skill connect` — keeps its current error. The alias stays out of the help output: there is still one documented way to print the guide. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011avkXVjDri1w1H2nW2xTv3 --- src/cli/index.ts | 4 ++- src/cli/parser.ts | 26 +++++++++++++++ test/e2e/suites/basic/help.test.sh | 28 ++++++++++++++++ test/unit/cli/parser.test.ts | 52 ++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index 43384981..3c3d18d1 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -37,6 +37,7 @@ import type { OutputMode, X402SchemePreference } from '../lib/index.js'; import { X402_SCHEME_PREFERENCES } from '../lib/index.js'; import { extractOptions, + preProcessSkillArgv, preProcessX402Argv, getVerboseFromEnv, getJsonFromEnv, @@ -173,7 +174,8 @@ function getOptionsFromCommand(command: Command): HandlerOptions { async function main(): Promise { // Disambiguate `--x402 ` (URL, @session, etc.) so Commander's // greedy [optional] arg parser doesn't eat the next positional as the value. - process.argv = preProcessX402Argv(process.argv); + // Then accept `mcpc --skill` as an undocumented alias of `mcpc help --skill`. + process.argv = preProcessSkillArgv(preProcessX402Argv(process.argv)); const args = process.argv.slice(2); // Set up cleanup handlers for graceful shutdown diff --git a/src/cli/parser.ts b/src/cli/parser.ts index d1d8e66e..19bcee39 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -58,6 +58,32 @@ export function preProcessX402Argv(argv: string[]): string[] { return out; } +/** + * Rewrite a bare `mcpc --skill` into `mcpc help --skill`. + * + * `--skill` is an undocumented alias — agents reach for the flag form first, and + * an unknown-option error there is a dead end. Only an invocation with no + * positional token at all is rewritten, so `mcpc @s --skill`, `mcpc help --skill` + * and friends keep their existing behaviour (including their errors). `--help` + * also wins, so it keeps printing usage rather than the guide. + */ +export function preProcessSkillArgv(argv: string[]): string[] { + const args = argv.slice(2); + if (!args.includes('--skill') || hasPositionalArg(args)) return argv; + if (args.includes('--help') || args.includes('-h')) return argv; + return [...argv.slice(0, 2), 'help', ...args]; +} + +/** Whether `args` contains a non-option token (a command, session, or value). */ +function hasPositionalArg(args: string[]): boolean { + for (let i = 0; i < args.length; i++) { + const arg = args[i] as string; + if (!arg.startsWith('-')) return true; + if (optionTakesValue(arg) && !arg.includes('=')) i++; // skip option value + } + return false; +} + // Global options that take a value (not boolean flags) const GLOBAL_OPTIONS_WITH_VALUES = ['--timeout', '--profile', '--max-chars']; diff --git a/test/e2e/suites/basic/help.test.sh b/test/e2e/suites/basic/help.test.sh index 081e5f2b..352b0085 100755 --- a/test/e2e/suites/basic/help.test.sh +++ b/test/e2e/suites/basic/help.test.sh @@ -45,6 +45,34 @@ assert_contains "$STDOUT" "name: mcpc" assert_contains "$STDOUT" "Mental model" test_pass +# Test: mcpc --skill is an undocumented alias printing the exact same guide. +# Kept out of --help output on purpose (one documented way to print the guide), +# but agents reach for the bare flag, so it must not be a dead end. +test_case "--skill matches help --skill" +run_mcpc --skill +assert_success +SKILL_FLAG_OUTPUT="$STDOUT" +assert_contains "$SKILL_FLAG_OUTPUT" "name: mcpc" +run_mcpc help --skill +assert_success +assert_eq "$SKILL_FLAG_OUTPUT" "$STDOUT" "mcpc --skill should match mcpc help --skill" +test_pass + +# Test: the top-level --skill alias stays out of the documented options +test_case "--skill is not listed as a top-level option" +run_mcpc --help +assert_success +assert_not_contains "$STDOUT" "Print the agent skill" +assert_contains "$STDOUT" "mcpc help --skill" +test_pass + +# Test: --skill with a command is not silently treated as the guide +test_case "--skill with a command name errors" +run_mcpc --skill connect +assert_failure +assert_not_contains "$STDOUT" "Mental model" +test_pass + # Test: mcpc help (no args) still shows the overview, not the guide test_case "help with no args shows the command overview" run_mcpc help diff --git a/test/unit/cli/parser.test.ts b/test/unit/cli/parser.test.ts index 30abf641..e40ff8a5 100644 --- a/test/unit/cli/parser.test.ts +++ b/test/unit/cli/parser.test.ts @@ -13,6 +13,7 @@ import { suggestCommand, normalizeSlashCommand, normalizeSlashCommandArgs, + preProcessSkillArgv, preProcessX402Argv, } from '../../../src/cli/parser.js'; import { ClientError } from '../../../src/lib/errors.js'; @@ -734,3 +735,54 @@ describe('preProcessX402Argv', () => { ).toEqual([...head, '--x402=auto', 'mcp.apify.com', 'other', '--x402', 'exact']); }); }); + +describe('preProcessSkillArgv', () => { + // Mirrors process.argv where indices 0 and 1 are node + script path. + const head = ['node', 'mcpc']; + + it('rewrites bare `--skill` into `help --skill`', () => { + expect(preProcessSkillArgv([...head, '--skill'])).toEqual([...head, 'help', '--skill']); + }); + + it('keeps global flags around the rewritten command', () => { + expect(preProcessSkillArgv([...head, '--verbose', '--skill'])).toEqual([ + ...head, + 'help', + '--verbose', + '--skill', + ]); + expect(preProcessSkillArgv([...head, '--skill', '--timeout', '5'])).toEqual([ + ...head, + 'help', + '--skill', + '--timeout', + '5', + ]); + }); + + it('leaves `help --skill` untouched', () => { + expect(preProcessSkillArgv([...head, 'help', '--skill'])).toEqual([...head, 'help', '--skill']); + }); + + it('leaves argv with any other positional untouched', () => { + expect(preProcessSkillArgv([...head, '@s', '--skill'])).toEqual([...head, '@s', '--skill']); + expect(preProcessSkillArgv([...head, '--skill', 'connect'])).toEqual([ + ...head, + '--skill', + 'connect', + ]); + }); + + it('lets `--help` win over `--skill`', () => { + expect(preProcessSkillArgv([...head, '--skill', '--help'])).toEqual([ + ...head, + '--skill', + '--help', + ]); + expect(preProcessSkillArgv([...head, '-h', '--skill'])).toEqual([...head, '-h', '--skill']); + }); + + it('leaves argv without `--skill` untouched', () => { + expect(preProcessSkillArgv([...head, 'help'])).toEqual([...head, 'help']); + }); +});