mcp-framework/docs/cli-helpers.md
thibaud-lclr f8eb0d3449
All checks were successful
CI / test (push) Successful in 12s
docs: mettre à jour bootstrap-cli et cli-helpers
Documenter BitwardenLoginHandler, StandardConfigTestHandler et le
comportement opt-in de ManifestCheck dans RunDoctor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 10:55:52 +02:00

5.6 KiB

Helpers CLI

cli fournit des helpers simples pour les assistants interactifs :

profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile)

baseURL, err := cli.PromptLine(reader, os.Stdout, "Base URL", profile.BaseURL)
if err != nil {
	return err
}
if err := cli.ValidateBaseURL(baseURL); err != nil {
	return err
}

token, err := cli.PromptSecret(os.Stdin, os.Stdout, "API token", hasStoredSecret, storedToken)
if err != nil {
	return err
}
_ = token

Pour décrire un setup complet sans réécrire la boucle interactive :

result, err := cli.RunSetup(cli.SetupOptions{
	Stdin:  os.Stdin,
	Stdout: os.Stdout,
	Fields: []cli.SetupField{
		{
			Name:     "base_url",
			Label:    "Base URL",
			Type:     cli.SetupFieldURL,
			Required: true,
		},
		{
			Name:           "api_token",
			Label:          "API token",
			Type:           cli.SetupFieldSecret,
			Required:       true,
			ExistingSecret: storedToken,
		},
		{
			Name:    "enabled",
			Label:   "Enable integration",
			Type:    cli.SetupFieldBool,
			Default: "true",
		},
		{
			Name:      "scopes",
			Label:     "Scopes",
			Type:      cli.SetupFieldList,
			Default:   "read,write",
			Normalize: func(value string) string { return strings.TrimSpace(strings.ToLower(value)) },
		},
	},
})
if err != nil {
	return err
}

baseURL, _ := result.Get("base_url")
apiToken, _ := result.Get("api_token")
enabled, _ := result.Get("enabled")
scopes, _ := result.Get("scopes")

if apiToken.KeptStoredSecret {
	fmt.Println("Stored token kept.")
}

_ = baseURL
_ = enabled
_ = scopes

Pour persister un secret de setup avec write+read-back et messages homogènes :

if err := cli.WriteSetupSecretVerified(cli.SetupSecretWriteOptions{
	Store:       store,
	SecretName:  "api-token",
	SecretLabel: "API token",
	TokenEnv:    "MY_MCP_API_TOKEN",
	Value:       apiToken,
}); err != nil {
	return err
}

Chaque champ peut déclarer ses propres hooks de validation (Validate, ValidateBool, ValidateList). Les validations sont appliquées de manière cohérente en TTY et en stdin non interactif.

Pour standardiser la résolution flag > env > config > secret avec provenance :

store, err := secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
	ServiceName: "my-mcp",
})
if err != nil {
	return err
}

lookup := cli.ResolveLookup(cli.ResolveLookupOptions{
	Flag:   cli.MapLookup(flagValues),
	Env:    cli.EnvLookup(os.LookupEnv),
	Config: cli.ConfigMap(configValues),
	Secret: cli.SecretStore(store),
})

resolution, err := cli.ResolveFields(cli.ResolveOptions{
	Fields: []cli.FieldSpec{
		{Name: "base_url", Required: true, FlagKey: "base-url", EnvKey: "MY_MCP_BASE_URL"},
		{Name: "api_token", Required: true, EnvKey: "MY_MCP_API_TOKEN", SecretKey: "my-mcp-api-token"},
		{Name: "timeout", DefaultValue: "30s"},
	},
	Lookup: lookup,
})
if err != nil {
	return err
}

if err := cli.RenderResolutionProvenance(os.Stdout, resolution); err != nil {
	return err
}

ResolveOptions.Order permet de changer la priorité globale si nécessaire, et FieldSpec.Sources permet de définir un ordre spécifique pour un champ.

Le package fournit un socle réutilisable pour une commande doctor. Pour les cas standards (config, secret store, connectivité), préférer bootstrap.StandardConfigTestHandler qui câble RunDoctor sans boilerplate.

Pour un contrôle fin ou un config test impératif, utiliser RunDoctor directement :

report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
	ConfigCheck:      cli.NewConfigCheck(store),
	SecretStoreCheck: cli.SecretStoreAvailabilityCheck(func() (secretstore.Store, error) {
		return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
			ServiceName: "my-mcp",
		})
	}),
	ConnectivityCheck: func(context.Context) cli.DoctorResult {
		if err := pingBackend(); err != nil {
			return cli.DoctorResult{
				Name:    "connectivity",
				Status:  cli.DoctorStatusFail,
				Summary: "backend inaccessible",
				Detail:  err.Error(),
			}
		}
		return cli.DoctorResult{
			Name:    "connectivity",
			Status:  cli.DoctorStatusOK,
			Summary: "backend accessible",
		}
	},
})

if err := cli.RenderDoctorReport(os.Stdout, report); err != nil {
	return err
}

if report.HasFailures() {
	os.Exit(1)
}

ManifestDir est optionnel. Quand il est fourni, RunDoctor inclut un ManifestCheck qui vérifie la présence et la validité de mcp.toml dans ce répertoire. Ne l'inclure que si ce check est pertinent pour l'application.

Quand SecretBackendPolicy vaut bitwarden-cli, le check Bitwarden est ajouté automatiquement. Pour le désactiver explicitement :

report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
	DisableAutoBitwardenCheck: true,
})

Pour éviter des checks custom répétitifs sur les profils, doctor expose aussi un helper basé sur FieldSpec :

report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
	ExtraChecks: []cli.DoctorCheck{
		cli.RequiredResolvedFieldsCheck(cli.ResolveOptions{
			Fields: []cli.FieldSpec{
				{Name: "base_url", Required: true, EnvKey: "MY_MCP_BASE_URL"},
				{
					Name:      "api_token",
					Required:  true,
					EnvKey:    "MY_MCP_API_TOKEN",
					SecretKey: "my-mcp-api-token",
					Sources:   []cli.ValueSource{cli.SourceEnv, cli.SourceSecret},
				},
			},
			Lookup: cli.ResolveLookup(cli.ResolveLookupOptions{
				Env:    cli.EnvLookup(os.LookupEnv),
				Config: cli.ConfigMap(configValues),
				Secret: cli.SecretStore(secretStore),
			}),
		}),
	},
})

En cas d'échec de résolution, tu peux aussi réutiliser le formatteur cli.FormatResolveFieldsError(err) dans un check custom pour garder des messages homogènes.