# mcp-framework Bibliothèque Go pour construire des binaires MCP avec : - résolution de profils CLI - stockage JSON de configuration dans `os.UserConfigDir()` - stockage de secrets dans le wallet natif selon l'OS - lecture d'un manifeste `mcp.toml` à la racine du projet - pipeline d'auto-update via endpoint de release configurable Le framework est volontairement petit. Il fournit des briques réutilisables, pas une application MCP complète. ## Installation ```bash go get gitea.lclr.dev/AI/mcp-framework ``` ## Packages - `cli` : helpers pour résoudre un profil, valider une URL et demander des valeurs à l'utilisateur. - `config` : lecture/écriture atomique d'une config JSON versionnée dans `os.UserConfigDir()`. - `manifest` : lecture de `mcp.toml` à la racine du projet et conversion vers `update.ReleaseSource`. - `secretstore` : lecture/écriture de secrets dans le wallet natif. - `update` : téléchargement et remplacement du binaire courant depuis un endpoint de release. ## Utilisation type Le flux typique côté application est : 1. Résoudre le profil actif avec `cli`. 2. Charger la config versionnée avec `config`. 3. Lire les secrets avec `secretstore`. 4. Charger `mcp.toml` avec `manifest`. 5. Exécuter l'auto-update avec `update` si nécessaire. ## Manifeste `mcp.toml` Le package `manifest` cherche automatiquement `mcp.toml` dans le répertoire courant puis remonte les répertoires parents jusqu'à trouver le fichier. Exemple minimal : ```toml [update] source_name = "Gitea releases" base_url = "https://gitea.example.com" latest_release_url = "https://gitea.example.com/api/v1/repos/org/repo/releases/latest" token_header = "Authorization" token_env_names = ["GITEA_TOKEN"] ``` Champs supportés dans `[update]` : - `source_name` : nom humain de la source de release, utilisé dans certains messages d'erreur. - `base_url` : base de la forge ou du service de release. - `latest_release_url` : URL complète qui retourne la release la plus récente. - `token_header` : header HTTP à utiliser pour l'authentification. - `token_env_names` : liste de variables d'environnement candidates pour retrouver le token. Exemple de chargement : ```go file, path, err := manifest.LoadDefault(".") if err != nil { return err } fmt.Printf("manifest loaded from %s\n", path) source := file.Update.ReleaseSource() ``` ## Config JSON Le package `config` stocke une structure générique par profil dans un JSON privé pour l'utilisateur courant. Exemple : ```go type Profile struct { BaseURL string `json:"base_url"` APIKey string `json:"api_key"` } store := config.NewStore[Profile]("my-mcp") cfg, path, err := store.LoadDefault() if err != nil { return err } profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile) profile := cfg.Profiles[profileName] profile.BaseURL = "https://api.example.com" cfg.CurrentProfile = profileName cfg.Profiles[profileName] = profile _, err = store.SaveDefault(cfg) if err != nil { return err } fmt.Printf("config saved to %s\n", path) ``` Notes : - le fichier est créé avec des permissions `0600` - le répertoire parent est forcé en `0700` - l'écriture est atomique via un fichier temporaire puis `rename` - si le fichier n'existe pas, `Load` et `LoadDefault` retournent une config vide par défaut - `NewStoreWithOptions` permet de définir une version cible, des migrations JSON (`from -> to`) et une validation explicite après chargement - une config plus récente que la version supportée, ou sans chemin de migration complet, retourne une erreur explicite Exemple de store versionné : ```go store := config.NewStoreWithOptions[Profile]("my-mcp", config.Options[Profile]{ Version: 2, Migrations: map[int]config.Migration{ 1: func(doc map[string]json.RawMessage) error { return nil }, }, Validator: func(cfg config.FileConfig[Profile]) []config.ValidationIssue { if cfg.CurrentProfile == "" { return []config.ValidationIssue{{ Path: "current_profile", Message: "must not be empty", }} } return nil }, }) ``` ## Secrets Le package `secretstore` s'appuie sur le wallet natif du système : - macOS : Keychain - Linux : Secret Service ou KWallet selon l'environnement - Windows : Credential Manager Exemple : ```go store, err := secretstore.Open(secretstore.Options{ ServiceName: "my-mcp", }) if err != nil { return err } if err := store.SetSecret("api-token", "My MCP API token", token); err != nil { return err } token, err = store.GetSecret("api-token") switch { case err == nil: // secret found case errors.Is(err, secretstore.ErrNotFound): // first run default: return err } ``` ## Helpers CLI `cli` fournit des helpers simples pour les assistants interactifs : ```go 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 } ``` ## Auto-Update Le package `update` ne déduit pas la forge ni l'authentification. L'application cliente fournit l'URL de release, le header d'auth éventuel et, si besoin, les variables d'environnement à consulter. Le format attendu pour la réponse `latest release` est actuellement : ```json { "tag_name": "v1.2.3", "assets": { "links": [ { "name": "my-mcp-linux-amd64", "url": "https://example.com/downloads/my-mcp-linux-amd64" } ] } } ``` Exemple : ```go file, _, err := manifest.LoadDefault(".") if err != nil { return err } err = update.Run(ctx, update.Options{ CurrentVersion: version, BinaryName: "my-mcp", ReleaseSource: file.Update.ReleaseSource(), Stdout: os.Stdout, }) if err != nil { return err } ``` Contraintes actuelles : - le `latest_release_url` doit être renseigné explicitement - les assets supportés sont `darwin/amd64`, `darwin/arm64`, `linux/amd64` et `windows/amd64` - le remplacement du binaire n'est pas supporté sur Windows - le nom de l'asset est dérivé de `BinaryName`, `GOOS` et `GOARCH` ## Exemple Minimal ```go type Profile struct { BaseURL string `json:"base_url"` } func run(ctx context.Context, flagProfile string) error { cfgStore := config.NewStore[Profile]("my-mcp") cfg, _, err := cfgStore.LoadDefault() if err != nil { return err } profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile) profile := cfg.Profiles[profileName] manifestFile, _, err := manifest.LoadDefault(".") if err != nil { return err } err = update.Run(ctx, update.Options{ CurrentVersion: version, BinaryName: "my-mcp", ReleaseSource: manifestFile.Update.ReleaseSource(), }) if err != nil { return err } _ = profile return nil } ``` ## Limites Actuelles - le manifeste gère uniquement la section `[update]` - le framework ne fournit pas encore d'interface unique de bootstrap - l'auto-update reste volontairement simple et ne supporte pas encore de scripts externes