No description
Find a file
2026-04-13 17:28:47 +02:00
.gitea/workflows ci: add tag release workflow 2026-04-13 16:07:54 +02:00
cli feat: add reusable mcp framework 2026-04-13 15:33:48 +02:00
config feat: add versioned config migrations 2026-04-13 17:28:47 +02:00
manifest feat: add toml manifest loader for mcp projects 2026-04-13 15:52:00 +02:00
secretstore feat: add reusable mcp framework 2026-04-13 15:33:48 +02:00
update refactor: decouple update package from forge-specific gitlab config 2026-04-13 15:46:28 +02:00
.gitignore update .gitignore 2026-04-13 15:47:20 +02:00
go.mod feat: add toml manifest loader for mcp projects 2026-04-13 15:52:00 +02:00
go.sum feat: add toml manifest loader for mcp projects 2026-04-13 15:52:00 +02:00
README.md feat: add versioned config migrations 2026-04-13 17:28:47 +02:00

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 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 :

[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, 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é :

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 :

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 :

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_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

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