feat(bootstrap): auto-hide commands with no hook configured
All checks were successful
CI / test (push) Successful in 11s
Release / release (push) Successful in 5s

Commands are now hidden from help and return ErrUnknownCommand when
invoked if their hook is nil (and for version, if Version string is
also empty). No explicit DisabledCommands needed for MCPs that don't
use login/setup/config/etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-lclr 2026-05-12 10:37:46 +02:00
parent 4a7248cfa9
commit 9a52b5dce1
2 changed files with 60 additions and 7 deletions

View file

@ -205,6 +205,11 @@ func normalize(opts Options) Options {
}
opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias)
opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, opts.Aliases, opts.EnableDoctorAlias)
for _, cmd := range autoDisabledCommands(opts) {
if !isCommandDisabled(cmd, opts.DisabledCommands) {
opts.DisabledCommands = append(opts.DisabledCommands, cmd)
}
}
return opts
}
@ -461,14 +466,14 @@ func printHelp(opts Options, command string, args ...[]string) error {
return printGlobalHelp(opts)
}
if command == CommandConfig {
return printConfigHelp(opts, commandArgs)
}
if isCommandDisabled(command, opts.DisabledCommands) {
return fmt.Errorf("%w: %s", ErrUnknownCommand, command)
}
if command == CommandConfig {
return printConfigHelp(opts, commandArgs)
}
for _, def := range commands {
if def.Name != command {
continue
@ -583,6 +588,30 @@ func printGlobalHelp(opts Options) error {
return err
}
func autoDisabledCommands(opts Options) []string {
h := opts.Hooks
var disabled []string
if h.Setup == nil {
disabled = append(disabled, CommandSetup)
}
if h.Login == nil {
disabled = append(disabled, CommandLogin)
}
if h.MCP == nil {
disabled = append(disabled, CommandMCP)
}
if h.Config == nil && h.ConfigShow == nil && h.ConfigTest == nil && h.ConfigDelete == nil {
disabled = append(disabled, CommandConfig)
}
if h.Update == nil {
disabled = append(disabled, CommandUpdate)
}
if h.Version == nil && strings.TrimSpace(opts.Version) == "" {
disabled = append(disabled, CommandVersion)
}
return disabled
}
func isCommandDisabled(command string, disabled []string) bool {
for _, d := range disabled {
if d == command {

View file

@ -91,11 +91,13 @@ func TestRunReturnsCommandNotConfigured(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"config"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
})
if !errors.Is(err, ErrSubcommandRequired) {
t.Fatalf("Run error = %v, want ErrSubcommandRequired", err)
@ -148,7 +150,7 @@ func TestRunVersionHookOverridesDefault(t *testing.T) {
}
}
func TestRunRequiresVersionWithoutVersionHook(t *testing.T) {
func TestRunVersionAutoHiddenWithoutHookOrVersionString(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
@ -158,8 +160,8 @@ func TestRunRequiresVersionWithoutVersionHook(t *testing.T) {
Stdout: &stdout,
Stderr: &stderr,
})
if !errors.Is(err, ErrVersionRequired) {
t.Fatalf("Run error = %v, want ErrVersionRequired", err)
if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("Run error = %v, want ErrUnknownCommand", err)
}
}
@ -167,12 +169,15 @@ func TestRunPrintsGlobalHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Description: "Binaire MCP de test.",
Version: "v1.2.3",
Args: []string{"help"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -197,12 +202,15 @@ func TestRunPrintsGlobalHelpWhenNoArgs(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Description: "Binaire MCP de test.",
Version: "v1.2.3",
Args: []string{},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -227,11 +235,13 @@ func TestRunPrintsCommandHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"help", "update"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{Update: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -250,11 +260,13 @@ func TestRunPrintsLoginCommandHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"help", "login"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{Login: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -273,11 +285,13 @@ func TestRunPrintsConfigHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"help", "config"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -299,11 +313,13 @@ func TestRunPrintsConfigSubcommandHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"help", "config", "show"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -349,11 +365,13 @@ func TestRunConfigShowReturnsCommandNotConfigured(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"config", "show"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
})
if !errors.Is(err, ErrCommandNotConfigured) {
t.Fatalf("Run error = %v, want ErrCommandNotConfigured", err)
@ -364,11 +382,13 @@ func TestRunConfigReturnsUnknownSubcommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"config", "sync"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
})
if !errors.Is(err, ErrUnknownSubcommand) {
t.Fatalf("Run error = %v, want ErrUnknownSubcommand", err)
@ -412,6 +432,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Aliases: map[string][]string{
@ -420,6 +441,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) {
Args: []string{"help", "doctor"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
@ -435,6 +457,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Aliases: map[string][]string{
@ -443,6 +466,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) {
Args: []string{"doctor", "--help"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)