mcp-framework/bootstrap/bootstrap_test.go
thibaud-lclr 9a52b5dce1
All checks were successful
CI / test (push) Successful in 11s
Release / release (push) Successful in 5s
feat(bootstrap): auto-hide commands with no hook configured
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>
2026-05-12 10:37:46 +02:00

688 lines
17 KiB
Go

package bootstrap
import (
"bytes"
"context"
"errors"
"os"
"slices"
"strings"
"testing"
)
func TestRunRoutesSetupHook(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Version: "v1.2.3",
Args: []string{"setup", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
Setup: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != CommandSetup {
t.Fatalf("invocation command = %q, want %q", got.Command, CommandSetup)
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
func TestRunRoutesLoginHook(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Version: "v1.2.3",
Args: []string{"login", "--force"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
Login: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != CommandLogin {
t.Fatalf("invocation command = %q, want %q", got.Command, CommandLogin)
}
wantArgs := []string{"--force"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
func TestRunReturnsUnknownCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"boom"},
Stdout: &stdout,
Stderr: &stderr,
})
if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("Run error = %v, want ErrUnknownCommand", err)
}
}
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)
}
if !strings.Contains(err.Error(), "show, test, delete") {
t.Fatalf("Run error = %q, want mention of show, test, delete", err)
}
}
func TestRunPrintsVersionByDefault(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Version: "v1.2.3",
Args: []string{"version"},
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if stdout.String() != "v1.2.3\n" {
t.Fatalf("stdout = %q, want %q", stdout.String(), "v1.2.3\n")
}
}
func TestRunVersionHookOverridesDefault(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"version"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
Version: func(_ context.Context, inv Invocation) error {
_, err := inv.Stdout.Write([]byte("custom-version\n"))
return err
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if stdout.String() != "custom-version\n" {
t.Fatalf("stdout = %q, want %q", stdout.String(), "custom-version\n")
}
}
func TestRunVersionAutoHiddenWithoutHookOrVersionString(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"version"},
Stdout: &stdout,
Stderr: &stderr,
})
if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("Run error = %v, want ErrUnknownCommand", err)
}
}
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)
}
text := stdout.String()
for _, snippet := range []string{
"Usage:",
"setup",
"mcp",
"config",
"update",
"version",
} {
if !strings.Contains(text, snippet) {
t.Fatalf("help output missing %q: %s", snippet, text)
}
}
}
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)
}
text := stdout.String()
for _, snippet := range []string{
"Usage:",
"setup",
"mcp",
"config",
"update",
"version",
} {
if !strings.Contains(text, snippet) {
t.Fatalf("help output missing %q: %s", snippet, text)
}
}
}
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)
}
text := stdout.String()
if !strings.Contains(text, "my-mcp update [args]") {
t.Fatalf("command help output = %q", text)
}
if !strings.Contains(text, "auto-update") {
t.Fatalf("command help output missing update description: %q", text)
}
}
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)
}
text := stdout.String()
if !strings.Contains(text, "my-mcp login [args]") {
t.Fatalf("command help output = %q", text)
}
if !strings.Contains(strings.ToLower(text), "bitwarden") {
t.Fatalf("command help output missing bitwarden description: %q", text)
}
}
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)
}
text := stdout.String()
for _, snippet := range []string{
"my-mcp config <subcommand>",
"show",
"test",
} {
if !strings.Contains(text, snippet) {
t.Fatalf("config help output missing %q: %q", snippet, text)
}
}
}
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)
}
text := stdout.String()
if !strings.Contains(text, "my-mcp config show [args]") {
t.Fatalf("config subcommand help output = %q", text)
}
}
func TestRunRoutesConfigShowHook(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"config", "show", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
ConfigShow: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != "config show" {
t.Fatalf("invocation command = %q, want %q", got.Command, "config show")
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
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)
}
}
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)
}
}
func TestRunRoutesAliasToConfigSubcommandHook(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Aliases: map[string][]string{
"doctor": {CommandConfig, ConfigSubcommandTest},
},
Args: []string{"doctor", "--profile", "work"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
ConfigTest: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != "config test" {
t.Fatalf("invocation command = %q, want %q", got.Command, "config test")
}
wantArgs := []string{"--profile", "work"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
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{
"doctor": {CommandConfig, ConfigSubcommandTest},
},
Args: []string{"help", "doctor"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "my-mcp config test [args]") {
t.Fatalf("command help output = %q", text)
}
}
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{
"doctor": {CommandConfig, ConfigSubcommandTest},
},
Args: []string{"doctor", "--help"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "my-mcp config test [args]") {
t.Fatalf("command help output = %q", text)
}
}
func TestRunPrintsAliasesInGlobalHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Aliases: map[string][]string{
"doctor": {CommandConfig, ConfigSubcommandTest},
},
AliasDescriptions: map[string]string{
"doctor": "Diagnostiquer la configuration locale.",
},
Args: []string{"help"},
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "Alias:") {
t.Fatalf("global help output missing alias section: %q", text)
}
if !strings.Contains(text, `doctor Diagnostiquer la configuration locale. (alias de "config test").`) {
t.Fatalf("global help output missing doctor alias details: %q", text)
}
}
func TestRunRoutesDoctorAliasWhenEnabled(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
EnableDoctorAlias: true,
Args: []string{"doctor", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
ConfigTest: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != "config test" {
t.Fatalf("invocation command = %q, want %q", got.Command, "config test")
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
func TestRunPrintsDoctorAliasInGlobalHelpWhenEnabled(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
EnableDoctorAlias: true,
Args: []string{"help"},
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "Alias:") {
t.Fatalf("global help output missing alias section: %q", text)
}
if !strings.Contains(text, `doctor Diagnostiquer la configuration locale. (alias de "config test").`) {
t.Fatalf("global help output missing default doctor alias details: %q", text)
}
}
func TestRunAcceptsGlobalDebugFlagAndRoutesCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
t.Setenv("MCP_FRAMEWORK_BITWARDEN_DEBUG", "")
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"--debug", "setup", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
Setup: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != CommandSetup {
t.Fatalf("invocation command = %q, want %q", got.Command, CommandSetup)
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
if os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG") != "1" {
t.Fatalf("MCP_FRAMEWORK_BITWARDEN_DEBUG = %q, want %q", os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG"), "1")
}
}
func TestRunAcceptsGlobalDebugFlagAfterCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
t.Setenv("MCP_FRAMEWORK_BITWARDEN_DEBUG", "")
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"setup", "--debug", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
Setup: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != CommandSetup {
t.Fatalf("invocation command = %q, want %q", got.Command, CommandSetup)
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
if os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG") != "1" {
t.Fatalf("MCP_FRAMEWORK_BITWARDEN_DEBUG = %q, want %q", os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG"), "1")
}
}
func TestDisabledCommandReturnsUnknown(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Args: []string{"login"},
DisabledCommands: []string{"login"},
Stdout: &stdout,
Stderr: &stderr,
})
if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("err = %v, want ErrUnknownCommand", err)
}
}
func TestDisabledCommandHiddenFromHelp(t *testing.T) {
var stdout bytes.Buffer
err := printGlobalHelp(Options{
BinaryName: "my-mcp",
DisabledCommands: []string{"login", "setup"},
Stdout: &stdout,
})
if err != nil {
t.Fatalf("printGlobalHelp error = %v", err)
}
out := stdout.String()
if strings.Contains(out, "login") {
t.Fatalf("help should not contain disabled command %q, got:\n%s", "login", out)
}
if strings.Contains(out, "setup") {
t.Fatalf("help should not contain disabled command %q, got:\n%s", "setup", out)
}
if !strings.Contains(out, "mcp") {
t.Fatalf("help should contain enabled command %q, got:\n%s", "mcp", out)
}
}
func TestDisabledCommandHelpReturnsUnknown(t *testing.T) {
var stdout bytes.Buffer
err := printHelp(Options{
BinaryName: "my-mcp",
DisabledCommands: []string{"login"},
Stdout: &stdout,
}, "login")
if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("err = %v, want ErrUnknownCommand", err)
}
}