# 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 ## Secrets Le package `secretstore` supporte plusieurs politiques de backend : - `auto` : comportement par défaut, utilise un backend keyring disponible et peut retomber sur l'environnement si `LookupEnv` est fourni - `kwallet-only` : impose KWallet et retourne une erreur explicite si KWallet n'est pas disponible - `keyring-any` : impose l'utilisation d'un backend keyring disponible - `env-only` : lecture seule depuis les variables d'environnement Backends keyring typiques : - macOS : Keychain - Linux : Secret Service ou KWallet selon l'environnement - Windows : Credential Manager Exemple : ```go store, err := secretstore.Open(secretstore.Options{ ServiceName: "my-mcp", BackendPolicy: secretstore.BackendAuto, }) 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 } ``` Pour imposer KWallet sur Linux : ```go store, err := secretstore.Open(secretstore.Options{ ServiceName: "email-mcp", BackendPolicy: secretstore.BackendKWalletOnly, }) ``` Pour stocker un secret structuré en JSON : ```go type Credentials struct { Host string `json:"host"` Username string `json:"username"` Password string `json:"password"` } err = secretstore.SetJSON(store, "imap-credentials", "IMAP credentials", Credentials{ Host: "imap.example.com", Username: "alice", Password: token, }) if err != nil { return err } creds, err := secretstore.GetJSON[Credentials](store, "imap-credentials") if err != nil { return err } ``` En mode `env-only`, `GetSecret("API_TOKEN")` lit la variable d'environnement `API_TOKEN`. Les opérations d'écriture et de suppression retournent `secretstore.ErrReadOnly`. ## 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