diff --git a/README.md b/README.md index 8ecb33e..e0e8f1c 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,39 @@ Bibliotheque Go pour construire des binaires MCP avec : - lecture d'un manifeste `mcp.toml` a la racine du projet - pipeline d'auto-update via endpoint de release configurable -Le package `update` ne deduit pas la forge ni l'authentification. -L'application cliente fournit l'URL de release, le header d'auth eventuel et, -si besoin, les variables d'environnement a consulter. +Le framework est volontairement petit. Il fournit des briques reutilisables, +pas une application MCP complete. -Packages exposes : +## Installation -- `cli` -- `config` -- `manifest` -- `secretstore` -- `update` +```bash +go get gitea.lclr.dev/AI/mcp-framework +``` -Exemple minimal de `mcp.toml` : +## Packages + +- `cli` : helpers pour resoudre un profil, valider une URL et demander des valeurs a l'utilisateur. +- `config` : lecture/ecriture atomique d'une config JSON versionnee dans `os.UserConfigDir()`. +- `manifest` : lecture de `mcp.toml` a la racine du projet et conversion vers `update.ReleaseSource`. +- `secretstore` : lecture/ecriture de secrets dans le wallet natif. +- `update` : telechargement et remplacement du binaire courant depuis un endpoint de release. + +## Usage Type + +Le flux typique cote application est : + +1. Resoudre le profil actif avec `cli`. +2. Charger la config versionnee avec `config`. +3. Lire les secrets avec `secretstore`. +4. Charger `mcp.toml` avec `manifest`. +5. Executer l'auto-update avec `update` si necessaire. + +## Manifeste `mcp.toml` + +Le package `manifest` cherche automatiquement `mcp.toml` dans le repertoire +courant puis remonte les repertoires parents jusqu'a trouver le fichier. + +Exemple minimal : ```toml [update] @@ -30,3 +50,210 @@ latest_release_url = "https://gitea.example.com/api/v1/repos/org/repo/releases/l token_header = "Authorization" token_env_names = ["GITEA_TOKEN"] ``` + +Champs supportes dans `[update]` : + +- `source_name` : nom humain de la source de release, utilise dans certains messages d'erreur. +- `base_url` : base de la forge ou du service de release. +- `latest_release_url` : URL complete qui retourne la release la plus recente. +- `token_header` : header HTTP a 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 generique par profil dans un JSON prive +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 cree avec des permissions `0600` +- le repertoire parent est force en `0700` +- l'ecriture est atomique via un fichier temporaire puis `rename` +- si le fichier n'existe pas, `Load` et `LoadDefault` retournent une config vide par defaut + +## Secrets + +Le package `secretstore` s'appuie sur le wallet natif du systeme : + +- 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 +} +``` + +## CLI Helpers + +`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 deduit pas la forge ni l'authentification. +L'application cliente fournit l'URL de release, le header d'auth eventuel et, +si besoin, les variables d'environnement a consulter. + +Le format attendu pour la reponse `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 etre renseigne explicitement +- les assets supportes sont `darwin/amd64`, `darwin/arm64`, `linux/amd64` et `windows/amd64` +- le remplacement du binaire n'est pas supporte sur Windows +- le nom de l'asset est derive 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 gere 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