| .gitea/workflows | ||
| cli | ||
| config | ||
| manifest | ||
| secretstore | ||
| update | ||
| .gitignore | ||
| AGENTS.md | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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
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 dansos.UserConfigDir().manifest: lecture demcp.tomlà la racine du projet et conversion versupdate.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 :
- Résoudre le profil actif avec
cli. - Charger la config versionnée avec
config. - Lire les secrets avec
secretstore. - Charger
mcp.tomlavecmanifest. - Exécuter l'auto-update avec
updatesi 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 :
[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 :
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 :
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,
LoadetLoadDefaultretournent une config vide par défaut NewStoreWithOptionspermet 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é :
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 supporte plusieurs politiques de backend :
auto: comportement par défaut, utilise un backend keyring disponible et peut retomber sur l'environnement siLookupEnvest fournikwallet-only: impose KWallet et retourne une erreur explicite si KWallet n'est pas disponiblekeyring-any: impose l'utilisation d'un backend keyring disponibleenv-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 :
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 :
store, err := secretstore.Open(secretstore.Options{
ServiceName: "email-mcp",
BackendPolicy: secretstore.BackendKWalletOnly,
})
Pour stocker un secret structuré en JSON :
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 :
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 :
{
"tag_name": "v1.2.3",
"assets": {
"links": [
{
"name": "my-mcp-linux-amd64",
"url": "https://example.com/downloads/my-mcp-linux-amd64"
}
]
}
}
Exemple :
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_urldoit être renseigné explicitement - les assets supportés sont
darwin/amd64,darwin/arm64,linux/amd64etwindows/amd64 - le remplacement du binaire n'est pas supporté sur Windows
- le nom de l'asset est dérivé de
BinaryName,GOOSetGOARCH
Exemple Minimal
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