package scaffold import ( "errors" "fmt" "os" "path/filepath" "slices" "sort" "strings" "text/template" "unicode" ) var ( ErrTargetDirRequired = errors.New("target directory is required") ErrFileExists = errors.New("target file already exists") ) type Options struct { TargetDir string ModulePath string BinaryName string Description string DocsURL string DefaultProfile string Profiles []string KnownEnvironmentVariables []string SecretStorePolicy string ReleaseDriver string ReleaseBaseURL string ReleaseRepository string ReleaseTokenEnv string Overwrite bool } type Result struct { Root string Files []string } type normalizedOptions struct { TargetDir string ModulePath string BinaryName string Description string DocsURL string DefaultProfile string Profiles []string KnownEnvironmentVariables []string ProfileEnv string BaseURLEnv string TokenEnv string SecretStorePolicy string ReleaseDriver string ReleaseBaseURL string ReleaseRepository string ReleaseTokenEnv string Overwrite bool } func Generate(options Options) (Result, error) { normalized, err := normalizeOptions(options) if err != nil { return Result{}, err } if err := os.MkdirAll(normalized.TargetDir, 0o755); err != nil { return Result{}, fmt.Errorf("create scaffold target %q: %w", normalized.TargetDir, err) } files := []generatedFile{ {Path: ".gitignore", Content: renderTemplate(gitignoreTemplate, normalized), Mode: 0o644}, {Path: "go.mod", Content: renderTemplate(goModTemplate, normalized), Mode: 0o644}, {Path: "README.md", Content: renderTemplate(readmeTemplate, normalized), Mode: 0o644}, {Path: "install.sh", Content: renderTemplate(installTemplate, normalized), Mode: 0o755}, {Path: "mcp.toml", Content: renderTemplate(manifestTemplate, normalized), Mode: 0o644}, {Path: filepath.Join("cmd", normalized.BinaryName, "main.go"), Content: renderTemplate(mainTemplate, normalized), Mode: 0o644}, {Path: filepath.Join("internal", "app", "app.go"), Content: renderTemplate(appTemplate, normalized), Mode: 0o644}, } written := make([]string, 0, len(files)) for _, file := range files { fullPath := filepath.Join(normalized.TargetDir, file.Path) if err := writeFile(fullPath, file.Content, file.Mode, normalized.Overwrite); err != nil { return Result{}, err } written = append(written, file.Path) } sort.Strings(written) return Result{ Root: normalized.TargetDir, Files: written, }, nil } type generatedFile struct { Path string Content string Mode os.FileMode } func writeFile(path, content string, mode os.FileMode, overwrite bool) error { if !overwrite { if _, err := os.Stat(path); err == nil { return fmt.Errorf("%w: %s", ErrFileExists, path) } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("stat scaffold file %q: %w", path, err) } } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create scaffold directory %q: %w", dir, err) } if mode == 0 { mode = 0o644 } if err := os.WriteFile(path, []byte(content), mode); err != nil { return fmt.Errorf("write scaffold file %q: %w", path, err) } return nil } func renderTemplate(src string, data normalizedOptions) string { tpl := template.Must(template.New("scaffold").Parse(src)) var builder strings.Builder if err := tpl.Execute(&builder, data); err != nil { panic(err) } return builder.String() } func normalizeOptions(options Options) (normalizedOptions, error) { targetDir := strings.TrimSpace(options.TargetDir) if targetDir == "" { return normalizedOptions{}, ErrTargetDirRequired } resolvedTarget, err := filepath.Abs(targetDir) if err != nil { return normalizedOptions{}, fmt.Errorf("resolve scaffold target %q: %w", targetDir, err) } binaryName := strings.TrimSpace(options.BinaryName) if binaryName == "" { binaryName = sanitizeSlug(filepath.Base(resolvedTarget)) } if binaryName == "" { binaryName = "my-mcp" } if strings.ContainsRune(binaryName, os.PathSeparator) { return normalizedOptions{}, fmt.Errorf("binary name %q must not contain path separators", binaryName) } modulePath := strings.TrimSpace(options.ModulePath) if modulePath == "" { modulePath = fmt.Sprintf("example.com/%s", sanitizeModuleSegment(binaryName)) } description := strings.TrimSpace(options.Description) if description == "" { description = fmt.Sprintf("Binaire MCP %s.", binaryName) } docsURL := strings.TrimSpace(options.DocsURL) if docsURL == "" { docsURL = fmt.Sprintf("https://docs.example.com/%s", binaryName) } defaultProfile := strings.TrimSpace(options.DefaultProfile) if defaultProfile == "" { defaultProfile = "default" } profiles := normalizeValues(options.Profiles) if !slices.Contains(profiles, defaultProfile) { profiles = append([]string{defaultProfile}, profiles...) } if len(profiles) == 0 { profiles = []string{defaultProfile} } envPrefix := environmentPrefix(binaryName) profileEnv := envPrefix + "_PROFILE" baseURLEnv := envPrefix + "_BASE_URL" tokenEnv := envPrefix + "_API_TOKEN" knownEnvironmentVariables := []string{profileEnv, baseURLEnv, tokenEnv} for _, name := range normalizeValues(options.KnownEnvironmentVariables) { if !slices.Contains(knownEnvironmentVariables, name) { knownEnvironmentVariables = append(knownEnvironmentVariables, name) } } secretStorePolicy := strings.TrimSpace(options.SecretStorePolicy) if secretStorePolicy == "" { secretStorePolicy = "auto" } releaseDriver := strings.TrimSpace(options.ReleaseDriver) if releaseDriver == "" { releaseDriver = "gitea" } releaseBaseURL := strings.TrimSpace(options.ReleaseBaseURL) if releaseBaseURL == "" { releaseBaseURL = "https://gitea.example.com" } releaseRepository := strings.Trim(strings.TrimSpace(options.ReleaseRepository), "/") if releaseRepository == "" { releaseRepository = fmt.Sprintf("org/%s", binaryName) } releaseTokenEnv := strings.TrimSpace(options.ReleaseTokenEnv) if releaseTokenEnv == "" { releaseTokenEnv = envPrefix + "_RELEASE_TOKEN" } return normalizedOptions{ TargetDir: resolvedTarget, ModulePath: modulePath, BinaryName: binaryName, Description: description, DocsURL: docsURL, DefaultProfile: defaultProfile, Profiles: profiles, KnownEnvironmentVariables: knownEnvironmentVariables, ProfileEnv: profileEnv, BaseURLEnv: baseURLEnv, TokenEnv: tokenEnv, SecretStorePolicy: secretStorePolicy, ReleaseDriver: releaseDriver, ReleaseBaseURL: releaseBaseURL, ReleaseRepository: releaseRepository, ReleaseTokenEnv: releaseTokenEnv, Overwrite: options.Overwrite, }, nil } func normalizeValues(values []string) []string { normalized := make([]string, 0, len(values)) for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { continue } normalized = append(normalized, trimmed) } return normalized } func sanitizeSlug(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "" } var builder strings.Builder lastDash := false for _, r := range value { switch { case unicode.IsLetter(r) || unicode.IsDigit(r): builder.WriteRune(r) lastDash = false case r == '-' || r == '_' || r == ' ' || r == '.': if !lastDash && builder.Len() > 0 { builder.WriteRune('-') lastDash = true } } } result := strings.Trim(builder.String(), "-") if result == "" { return "my-mcp" } return result } func sanitizeModuleSegment(binaryName string) string { segment := sanitizeSlug(binaryName) if segment == "" { return "my-mcp" } return segment } func environmentPrefix(binaryName string) string { name := strings.ToUpper(strings.TrimSpace(binaryName)) if name == "" { return "MCP" } var builder strings.Builder lastUnderscore := false for _, r := range name { switch { case unicode.IsLetter(r) || unicode.IsDigit(r): builder.WriteRune(r) lastUnderscore = false default: if !lastUnderscore { builder.WriteRune('_') lastUnderscore = true } } } result := strings.Trim(builder.String(), "_") if result == "" { return "MCP" } return result } const gitignoreTemplate = `bin/ dist/ *.log ` const goModTemplate = `module {{.ModulePath}} go 1.25.0 ` const installTemplate = `#!/usr/bin/env bash set -euo pipefail BINARY_NAME="{{.BinaryName}}" MODULE_PATH="{{.ModulePath}}" DEFAULT_PROFILE="{{.DefaultProfile}}" PROFILE_ENV="{{.ProfileEnv}}" if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then C_RESET="$(printf '\033[0m')" C_BOLD="$(printf '\033[1m')" C_DIM="$(printf '\033[2m')" C_RED="$(printf '\033[31m')" C_GREEN="$(printf '\033[32m')" C_YELLOW="$(printf '\033[33m')" C_BLUE="$(printf '\033[34m')" C_MAGENTA="$(printf '\033[35m')" C_CYAN="$(printf '\033[36m')" else C_RESET="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" fi ui_line() { printf "%b%s%b\n" "$C_DIM" "------------------------------------------------------------" "$C_RESET" >&2 } ui_title() { printf "\n%b%s%b\n" "$C_BOLD$C_CYAN" "$1" "$C_RESET" >&2 } ui_info() { printf "%b[info]%b %s\n" "$C_BLUE" "$C_RESET" "$1" >&2 } ui_success() { printf "%b[ok]%b %s\n" "$C_GREEN" "$C_RESET" "$1" >&2 } ui_warn() { printf "%b[warn]%b %s\n" "$C_YELLOW" "$C_RESET" "$1" >&2 } ui_error() { printf "%b[error]%b %s\n" "$C_RED" "$C_RESET" "$1" >&2 } prompt() { local label="$1" local default_value="$2" local answer="" if [ -n "$default_value" ]; then printf "%b%s%b [%s]: " "$C_BOLD" "$label" "$C_RESET" "$default_value" >&2 else printf "%b%s%b: " "$C_BOLD" "$label" "$C_RESET" >&2 fi if [ -t 2 ] && [ -r /dev/tty ]; then if ! IFS= read -r answer < /dev/tty 2>/dev/null; then IFS= read -r answer || answer="" fi else IFS= read -r answer || answer="" fi if [ -z "$answer" ]; then printf "%s" "$default_value" return fi printf "%s" "$answer" } sanitize_server_name() { local raw="$1" local sanitized sanitized="$(printf "%s" "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g; s/--*/-/g; s/^-*//; s/-*$//')" if [ -z "$sanitized" ]; then sanitized="$BINARY_NAME" fi printf "%s" "$sanitized" } toml_escape() { printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } go_bin_dir() { local gobin gobin="$(go env GOBIN 2>/dev/null || true)" if [ -n "$gobin" ]; then printf "%s\n" "$gobin" return fi go env GOPATH 2>/dev/null | awk '{print $1 "/bin"}' } resolve_binary_path() { if command -v "$BINARY_NAME" >/dev/null 2>&1; then command -v "$BINARY_NAME" return fi if command -v go >/dev/null 2>&1; then local bin_dir bin_dir="$(go_bin_dir)" if [ -n "$bin_dir" ] && [ -x "$bin_dir/$BINARY_NAME" ]; then printf "%s\n" "$bin_dir/$BINARY_NAME" return fi fi printf "%s\n" "$HOME/.local/bin/$BINARY_NAME" } ensure_cli() { local cli_name="$1" if command -v "$cli_name" >/dev/null 2>&1; then return fi ui_error "Commande introuvable: $cli_name" exit 1 } install_binary() { if command -v "$BINARY_NAME" >/dev/null 2>&1; then ui_success "Binaire detecte: $(command -v "$BINARY_NAME")" local reinstall reinstall="$(prompt "Reinstaller via go install ? (y/N)" "N")" case "$reinstall" in y|Y|yes|YES) ;; *) return ;; esac fi if ! command -v go >/dev/null 2>&1; then ui_error "Go n'est pas installe. Installe Go ou choisis une configuration manuelle." exit 1 fi ui_info "Installation du binaire via go install..." go install "${MODULE_PATH}/cmd/${BINARY_NAME}@latest" local bin_dir bin_dir="$(go_bin_dir)" if [ -n "$bin_dir" ]; then ui_success "Binaire installe dans $bin_dir" ui_info "Ajoute ce dossier au PATH si necessaire." fi } run_setup_wizard() { install_binary local profile profile="$(prompt "Profil a configurer (${PROFILE_ENV})" "$DEFAULT_PROFILE")" local binary_path binary_path="$(resolve_binary_path)" ui_info "Lancement de $BINARY_NAME setup" if [ -r /dev/tty ] && [ -w /dev/tty ]; then env "${PROFILE_ENV}=${profile}" "$binary_path" setup < /dev/tty > /dev/tty else env "${PROFILE_ENV}=${profile}" "$binary_path" setup fi ui_success "Setup termine pour le profil \"$profile\"." } collect_server_inputs() { local default_name default_name="$(sanitize_server_name "$BINARY_NAME")" SERVER_NAME="$(prompt "Nom du serveur MCP" "$default_name")" SERVER_NAME="$(sanitize_server_name "$SERVER_NAME")" PROFILE_VALUE="$(prompt "Valeur de ${PROFILE_ENV}" "$DEFAULT_PROFILE")" local default_command default_command="$(resolve_binary_path)" COMMAND_PATH="$(prompt "Chemin du binaire serveur MCP" "$default_command")" } choose_scope() { local selected while true; do ui_title "Scope de configuration" printf " 1) global (user)\n" >&2 printf " 2) project (projet courant)\n" >&2 selected="$(prompt "Choix" "1")" case "$selected" in 1) printf "global" return ;; 2) printf "project" return ;; *) ui_warn "Choix invalide: $selected" ;; esac done } apply_claude_mcp() { ensure_cli "claude" collect_server_inputs local scope_choice scope_choice="$(choose_scope)" local claude_scope if [ "$scope_choice" = "global" ]; then claude_scope="user" else claude_scope="project" fi ui_info "Application de la configuration Claude ($claude_scope)..." claude mcp remove --scope "$claude_scope" "$SERVER_NAME" >/dev/null 2>&1 || true claude mcp add \ --transport stdio \ --scope "$claude_scope" \ -e "${PROFILE_ENV}=${PROFILE_VALUE}" \ "$SERVER_NAME" -- "$COMMAND_PATH" mcp ui_success "Serveur \"$SERVER_NAME\" configure dans Claude ($claude_scope)." } rewrite_codex_project_config() { local project_dir="$1" local config_file="$project_dir/.codex/config.toml" local section_prefix="[mcp_servers.${SERVER_NAME}" mkdir -p "$project_dir/.codex" touch "$config_file" local tmp_file tmp_file="$(mktemp)" awk -v prefix="$section_prefix" ' function is_target(line, p, next_char) { if (index(line, p) != 1) { return 0 } next_char = substr(line, length(p) + 1, 1) return next_char == "]" || next_char == "." } /^\[.*\]$/ { if (is_target($0, prefix)) { skip = 1 next } if (skip == 1) { skip = 0 } } { if (skip != 1) { print $0 } } ' "$config_file" > "$tmp_file" mv "$tmp_file" "$config_file" { printf "\n[mcp_servers.%s]\n" "$SERVER_NAME" printf "command = \"%s\"\n" "$(toml_escape "$COMMAND_PATH")" printf "args = [\"mcp\"]\n\n" printf "[mcp_servers.%s.env]\n" "$SERVER_NAME" printf "%s = \"%s\"\n" "$PROFILE_ENV" "$(toml_escape "$PROFILE_VALUE")" } >> "$config_file" } apply_codex_mcp() { ensure_cli "codex" collect_server_inputs local scope_choice scope_choice="$(choose_scope)" if [ "$scope_choice" = "global" ]; then ui_info "Application via codex mcp add (scope global)..." codex mcp remove "$SERVER_NAME" >/dev/null 2>&1 || true codex mcp add \ "$SERVER_NAME" \ --env "${PROFILE_ENV}=${PROFILE_VALUE}" \ -- "$COMMAND_PATH" mcp ui_success "Serveur \"$SERVER_NAME\" configure dans le scope global Codex." return fi local default_project_dir default_project_dir="$(pwd)" local project_dir project_dir="$(prompt "Dossier projet cible pour .codex/config.toml" "$default_project_dir")" rewrite_codex_project_config "$project_dir" ui_success "Configuration projet ecrite dans $project_dir/.codex/config.toml" } print_mcp_json() { collect_server_inputs cat <&2 printf "%bFramework module:%b %s\n" "$C_DIM" "$C_RESET" "$MODULE_PATH" >&2 ui_line printf "Choisis une action:\n" >&2 printf " 1) Installer/mettre a jour le binaire + setup\n" >&2 printf " 2) Configurer Claude Code (apply direct)\n" >&2 printf " 3) Configurer Codex (apply direct)\n" >&2 printf " 4) Generer JSON MCP manuel\n" >&2 printf " 5) Quitter\n" >&2 } main() { while true; do print_header local choice choice="$(prompt "Choix" "1")" printf "\n" >&2 case "$choice" in 1) run_setup_wizard return ;; 2) apply_claude_mcp return ;; 3) apply_codex_mcp return ;; 4) ui_info "JSON MCP genere sur stdout." print_mcp_json return ;; 5) ui_warn "Annule." return ;; *) ui_warn "Choix invalide: $choice" ;; esac done } main "$@" ` const mainTemplate = `package main import ( "context" "log" "os" "{{.ModulePath}}/internal/app" ) var version = "dev" func main() { if err := app.Run(context.Background(), os.Args[1:], version); err != nil { log.Fatal(err) } } ` const appTemplate = `package app import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "gitea.lclr.dev/AI/mcp-framework/bootstrap" "gitea.lclr.dev/AI/mcp-framework/cli" "gitea.lclr.dev/AI/mcp-framework/config" "gitea.lclr.dev/AI/mcp-framework/manifest" "gitea.lclr.dev/AI/mcp-framework/secretstore" "gitea.lclr.dev/AI/mcp-framework/update" ) type Profile struct { BaseURL string } type Runtime struct { ConfigStore config.Store[Profile] Manifest manifest.File BinaryName string Description string Version string DefaultProfile string ProfileEnv string TokenEnv string SecretName string } func Run(ctx context.Context, args []string, version string) error { runtime, err := NewRuntime(version) if err != nil { return err } return runtime.Run(ctx, args) } func NewRuntime(version string) (Runtime, error) { manifestFile, _, err := manifest.LoadDefault(".") if err != nil { return Runtime{}, err } bootstrapInfo := manifestFile.BootstrapInfo() scaffoldInfo := manifestFile.ScaffoldInfo() binaryName := firstNonEmpty(bootstrapInfo.BinaryName, "{{.BinaryName}}") description := firstNonEmpty(bootstrapInfo.Description, "{{.Description}}") defaultProfile := firstNonEmpty(scaffoldInfo.DefaultProfile, "{{.DefaultProfile}}") profileEnv := "{{.ProfileEnv}}" tokenEnv := "{{.TokenEnv}}" if len(scaffoldInfo.KnownEnvironmentVariables) > 0 { profileEnv = firstNonEmpty(scaffoldInfo.KnownEnvironmentVariables[0], profileEnv) } if len(scaffoldInfo.KnownEnvironmentVariables) > 2 { tokenEnv = firstNonEmpty(scaffoldInfo.KnownEnvironmentVariables[2], tokenEnv) } return Runtime{ ConfigStore: config.NewStore[Profile](binaryName), Manifest: manifestFile, BinaryName: binaryName, Description: description, Version: firstNonEmpty(strings.TrimSpace(version), "dev"), DefaultProfile: defaultProfile, ProfileEnv: profileEnv, TokenEnv: tokenEnv, SecretName: binaryName + "-api-token", }, nil } func (r Runtime) Run(ctx context.Context, args []string) error { return bootstrap.Run(ctx, bootstrap.Options{ BinaryName: r.BinaryName, Description: r.Description, Version: r.Version, Args: args, Hooks: bootstrap.Hooks{ Setup: r.runSetup, MCP: r.runMCP, ConfigShow: r.runConfigShow, ConfigTest: r.runConfigTest, Update: r.runUpdate, }, }) } func (r Runtime) runSetup(_ context.Context, inv bootstrap.Invocation) error { stdin, ok := inv.Stdin.(*os.File) if !ok || stdin == nil { stdin = os.Stdin } stdout := inv.Stdout if stdout == nil { stdout = os.Stdout } cfg, _, err := r.ConfigStore.LoadDefault() if err != nil { return err } profileName := r.resolveProfileName(cfg.CurrentProfile) profile := cfg.Profiles[profileName] storedToken, _ := r.readToken() result, err := cli.RunSetup(cli.SetupOptions{ Stdin: stdin, Stdout: stdout, Fields: []cli.SetupField{ { Name: "base_url", Label: "Base URL", Type: cli.SetupFieldURL, Required: true, Default: profile.BaseURL, }, { Name: "api_token", Label: "API token", Type: cli.SetupFieldSecret, Required: true, ExistingSecret: storedToken, }, }, }) if err != nil { return err } baseURLValue, _ := result.Get("base_url") tokenValue, _ := result.Get("api_token") profile.BaseURL = strings.TrimSpace(baseURLValue.String) cfg.CurrentProfile = profileName cfg.Profiles[profileName] = profile if _, err := r.ConfigStore.SaveDefault(cfg); err != nil { return err } if !tokenValue.KeptStoredSecret { store, err := r.openSecretStore() if err != nil { return err } if err := store.SetSecret(r.SecretName, "API token", tokenValue.String); err != nil { if errors.Is(err, secretstore.ErrReadOnly) { fmt.Fprintf(stdout, "Secret store en lecture seule, exporte %s pour fournir le token.\n", r.TokenEnv) } else { return err } } } _, err = fmt.Fprintf(stdout, "Configuration sauvegardée pour le profil %q.\n", profileName) return err } func (r Runtime) runMCP(_ context.Context, inv bootstrap.Invocation) error { stdout := inv.Stdout if stdout == nil { stdout = os.Stdout } cfg, _, err := r.ConfigStore.LoadDefault() if err != nil { return err } profileName := r.resolveProfileName(cfg.CurrentProfile) profile, ok := cfg.Profiles[profileName] if !ok { return fmt.Errorf("profil %q absent, lance %s setup", profileName, r.BinaryName) } token, err := r.readToken() if err != nil { if errors.Is(err, secretstore.ErrNotFound) { return fmt.Errorf("secret %q introuvable, lance %s setup", r.SecretName, r.BinaryName) } return err } fmt.Fprintf(stdout, "MCP prêt sur %s (profil %s).\n", profile.BaseURL, profileName) fmt.Fprintf(stdout, "Token chargé (%d caractères).\n", len(strings.TrimSpace(token))) fmt.Fprintln(stdout, "Ajoute ici ta logique métier MCP.") return nil } func (r Runtime) runConfigShow(_ context.Context, inv bootstrap.Invocation) error { stdout := inv.Stdout if stdout == nil { stdout = os.Stdout } cfg, path, err := r.ConfigStore.LoadDefault() if err != nil { return err } payload, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("encode config JSON: %w", err) } if _, err := fmt.Fprintf(stdout, "Config: %s\n", path); err != nil { return err } _, err = fmt.Fprintf(stdout, "%s\n", payload) return err } func (r Runtime) runConfigTest(ctx context.Context, inv bootstrap.Invocation) error { stdout := inv.Stdout if stdout == nil { stdout = os.Stdout } report := cli.RunDoctor(ctx, cli.DoctorOptions{ ConfigCheck: cli.NewConfigCheck(r.ConfigStore), SecretStoreCheck: cli.SecretStoreAvailabilityCheck(r.openSecretStore), RequiredSecrets: []cli.DoctorSecret{ {Name: r.SecretName, Label: "API token"}, }, SecretStoreFactory: r.openSecretStore, ManifestDir: ".", }) if err := cli.RenderDoctorReport(stdout, report); err != nil { return err } if report.HasFailures() { return errors.New("doctor checks failed") } return nil } func (r Runtime) runUpdate(ctx context.Context, inv bootstrap.Invocation) error { stdout := inv.Stdout if stdout == nil { stdout = os.Stdout } return update.Run(ctx, update.Options{ CurrentVersion: r.Version, BinaryName: r.BinaryName, ReleaseSource: r.Manifest.Update.ReleaseSource(), Stdout: stdout, }) } func (r Runtime) openSecretStore() (secretstore.Store, error) { return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{ ServiceName: r.BinaryName, LookupEnv: func(name string) (string, bool) { if name == r.SecretName { return os.LookupEnv(r.TokenEnv) } return os.LookupEnv(name) }, }) } func (r Runtime) readToken() (string, error) { store, err := r.openSecretStore() if err != nil { return "", err } return store.GetSecret(r.SecretName) } func (r Runtime) resolveProfileName(currentProfile string) string { resolved := cli.ResolveProfileName("", os.Getenv(r.ProfileEnv), currentProfile) if strings.TrimSpace(resolved) != "" { return resolved } return r.DefaultProfile } func firstNonEmpty(values ...string) string { for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed != "" { return trimmed } } return "" } ` const manifestTemplate = `binary_name = "{{.BinaryName}}" docs_url = "{{.DocsURL}}" [update] source_name = "Release endpoint" driver = "{{.ReleaseDriver}}" repository = "{{.ReleaseRepository}}" base_url = "{{.ReleaseBaseURL}}" asset_name_template = "{binary}-{os}-{arch}{ext}" checksum_asset_name = "{asset}.sha256" checksum_required = false token_header = "Authorization" token_prefix = "token" token_env_names = ["{{.ReleaseTokenEnv}}"] [environment] known = [{{- range $index, $value := .KnownEnvironmentVariables}}{{if $index}}, {{end}}"{{$value}}"{{- end}}] [secret_store] backend_policy = "{{.SecretStorePolicy}}" [profiles] default = "{{.DefaultProfile}}" known = [{{- range $index, $value := .Profiles}}{{if $index}}, {{end}}"{{$value}}"{{- end}}] [bootstrap] description = "{{.Description}}" ` const readmeTemplate = `# {{.BinaryName}} Binaire MCP généré depuis ` + "`mcp-framework`" + `. ## Arborescence générée ` + "```text" + ` . ├── cmd/ │ └── {{.BinaryName}}/ │ └── main.go ├── internal/ │ └── app/ │ └── app.go ├── .gitignore ├── go.mod ├── install.sh ├── mcp.toml └── README.md ` + "```" + ` ## Démarrage rapide 1. Installer les dépendances : ` + "```bash" + ` go mod tidy ` + "```" + ` 2. Vérifier l’aide CLI bootstrap : ` + "```bash" + ` go run ./cmd/{{.BinaryName}} help ` + "```" + ` 3. Initialiser la configuration locale : ` + "```bash" + ` go run ./cmd/{{.BinaryName}} setup ` + "```" + ` 4. Lancer le flux MCP (placeholder) : ` + "```bash" + ` go run ./cmd/{{.BinaryName}} mcp ` + "```" + ` 5. Vérifier la configuration et le manifeste : ` + "```bash" + ` go run ./cmd/{{.BinaryName}} config test ` + "```" + ` 6. Publier un install wizard consommable via ` + "`curl | bash`" + ` : ` + "```bash" + ` curl -fsSL https://///raw/branch/main/install.sh | bash ` + "```" + ` Le wizard permet ensuite d'appliquer directement la configuration MCP pour Claude Code ou Codex (scope global/projet), ou de générer un JSON manuel. ## Points à adapter - Remplacer les valeurs de ` + "`mcp.toml`" + ` (forge, repository, URL docs). - Adapter l'URL ` + "`curl .../install.sh`" + ` à votre forge/répertoire. - Compléter la logique métier dans ` + "`internal/app/app.go`" + ` (` + "`runMCP`" + `). - Ajuster les variables d’environnement connues si besoin. `