2026-04-13 13:33:48 +00:00
# mcp-framework
2026-04-13 13:57:35 +00:00
Bibliothèque Go pour construire des binaires MCP avec :
2026-04-13 13:33:48 +00:00
2026-04-13 13:57:35 +00:00
- résolution de profils CLI
2026-04-13 13:33:48 +00:00
- stockage JSON de configuration dans `os.UserConfigDir()`
- stockage de secrets dans le wallet natif selon l'OS
2026-04-13 13:57:35 +00:00
- lecture d'un manifeste `mcp.toml` à la racine du projet
2026-04-13 13:46:28 +00:00
- pipeline d'auto-update via endpoint de release configurable
2026-04-13 13:57:35 +00:00
Le framework est volontairement petit. Il fournit des briques réutilisables,
pas une application MCP complète.
2026-04-13 13:53:53 +00:00
## Installation
```bash
go get gitea.lclr.dev/AI/mcp-framework
```
2026-04-14 13:59:18 +00:00
## CLI de scaffold
Pour initialiser un projet MCP depuis un dossier vide, sans écrire de runner Go :
```bash
go install gitea.lclr.dev/AI/mcp-framework/cmd/mcp-framework@latest
mcp-framework scaffold init \
--target ./my-mcp \
--module example.com/my-mcp \
--binary my-mcp \
--profiles dev,prod
```
Puis dans le projet généré :
```bash
cd my-mcp
go mod tidy
go run ./cmd/my-mcp help
```
2026-04-13 13:53:53 +00:00
## Packages
2026-04-14 08:52:36 +00:00
- `bootstrap` : couche CLI optionnelle avec sous-commandes communes (`setup`, `mcp` , `config show|test` , `update` , `version` ) et hooks métier explicites.
2026-04-13 18:53:36 +00:00
- `cli` : helpers pour résoudre un profil, valider une URL, demander des valeurs à l'utilisateur et exécuter un `doctor` .
2026-04-13 13:57:35 +00:00
- `config` : lecture/écriture atomique d'une config JSON versionnée dans `os.UserConfigDir()` .
2026-04-14 10:30:56 +00:00
- `manifest` : lecture de `mcp.toml` à la racine du projet, conversion vers `update.ReleaseSource` et exposition de métadonnées pour `bootstrap` /scaffolding.
2026-04-15 08:13:45 +00:00
- `scaffold` : génération d'un squelette de projet MCP (arborescence, `main.go` , `mcp.toml` , `install.sh` wizard, wiring de base et README de démarrage).
2026-04-14 14:40:50 +00:00
- `secretstore` : lecture/écriture de secrets dans le wallet natif, avec helper runtime `OpenFromManifest` .
2026-04-13 13:57:35 +00:00
- `update` : téléchargement et remplacement du binaire courant depuis un endpoint de release.
2026-04-13 13:53:53 +00:00
2026-04-13 13:57:35 +00:00
## Utilisation type
2026-04-13 13:33:48 +00:00
2026-04-13 13:57:35 +00:00
Le flux typique côté application est :
2026-04-13 13:33:48 +00:00
2026-04-14 06:24:24 +00:00
1. Déclarer les sous-commandes communes via `bootstrap` (optionnel).
2. Résoudre le profil actif avec `cli` .
3. Charger la config versionnée avec `config` .
4. Lire les secrets avec `secretstore` .
5. Charger `mcp.toml` avec `manifest` .
6. Exécuter l'auto-update avec `update` si nécessaire.
7. Exécuter `doctor` pour diagnostiquer la configuration locale et brancher des checks métier.
## Bootstrap CLI
Le package `bootstrap` reste optionnel : une application peut l'utiliser pour
uniformiser le parsing CLI et l'aide, sans imposer de runtime monolithique.
Exemple minimal :
```go
func main() {
err := bootstrap.Run(context.Background(), bootstrap.Options{
2026-04-14 15:51:28 +00:00
BinaryName: "my-mcp",
Description: "Client MCP",
Version: version,
EnableDoctorAlias: true, // expose `doctor` comme alias de `config test`
AliasDescriptions: map[string]string{
"doctor": "Diagnostiquer la configuration locale.",
},
2026-04-14 06:24:24 +00:00
Hooks: bootstrap.Hooks{
Setup: func(ctx context.Context, inv bootstrap.Invocation) error {
return runSetup(ctx, inv.Args)
},
MCP: func(ctx context.Context, inv bootstrap.Invocation) error {
return runMCP(ctx, inv.Args)
},
2026-04-14 08:52:36 +00:00
ConfigShow: func(ctx context.Context, inv bootstrap.Invocation) error {
return runConfigShow(ctx, inv.Args)
},
ConfigTest: func(ctx context.Context, inv bootstrap.Invocation) error {
return runConfigTest(ctx, inv.Args)
2026-04-14 06:24:24 +00:00
},
Update: func(ctx context.Context, inv bootstrap.Invocation) error {
return runUpdate(ctx, inv.Args)
},
},
})
if err != nil {
log.Fatal(err)
}
}
```
Si `Hooks.Version` n'est pas fourni, la sous-commande `version` affiche
automatiquement `Options.Version` .
2026-04-13 13:52:00 +00:00
2026-04-14 08:52:36 +00:00
Sans arguments, `bootstrap.Run` affiche l'aide globale (pas de lancement implicite de `mcp` ).
La commande `config` impose une sous-commande (`show`, `test` , et optionnellement `delete` ).
2026-04-14 15:51:28 +00:00
Les alias déclarés (ou `EnableDoctorAlias` ) sont affichés dans l'aide globale.
2026-04-14 08:52:36 +00:00
2026-04-13 13:53:53 +00:00
## Manifeste `mcp.toml`
2026-04-13 13:57:35 +00:00
Le package `manifest` cherche automatiquement `mcp.toml` dans le répertoire
courant puis remonte les répertoires parents jusqu'à trouver le fichier.
2026-04-13 13:53:53 +00:00
Exemple minimal :
2026-04-13 13:52:00 +00:00
```toml
2026-04-14 10:30:56 +00:00
binary_name = "my-mcp"
docs_url = "https://docs.example.com/my-mcp"
2026-04-13 13:52:00 +00:00
[update]
source_name = "Gitea releases"
2026-04-14 12:11:43 +00:00
driver = "gitea"
repository = "org/repo"
2026-04-13 13:52:00 +00:00
base_url = "https://gitea.example.com"
2026-04-14 12:11:43 +00:00
asset_name_template = "{binary}-{os}-{arch}{ext}"
checksum_asset_name = "{asset}.sha256"
2026-04-15 10:20:06 +00:00
checksum_required = true
signature_asset_name = "{asset}.sig"
signature_required = false
signature_public_key_env_names = ["MY_MCP_RELEASE_ED25519_PUBLIC_KEY"]
2026-04-13 13:52:00 +00:00
token_header = "Authorization"
2026-04-14 12:11:43 +00:00
token_prefix = "token"
2026-04-13 13:52:00 +00:00
token_env_names = ["GITEA_TOKEN"]
2026-04-14 10:30:56 +00:00
[environment]
known = ["MY_MCP_PROFILE", "MY_MCP_URL", "MY_MCP_TOKEN"]
[secret_store]
backend_policy = "auto"
[profiles]
default = "prod"
known = ["dev", "staging", "prod"]
[bootstrap]
description = "Client MCP interne"
2026-04-13 13:52:00 +00:00
```
2026-04-13 13:53:53 +00:00
2026-04-14 10:30:56 +00:00
Champs supportés :
- `binary_name` : nom du binaire (utilisable par le bootstrap/scaffolding).
- `docs_url` : URL de documentation projet.
- `[update]` : source de release consommée par `update` .
2026-04-13 13:53:53 +00:00
2026-04-13 13:57:35 +00:00
- `source_name` : nom humain de la source de release, utilisé dans certains messages d'erreur.
2026-04-14 12:11:43 +00:00
- `driver` : driver de forge (`gitea`, `gitlab` , `github` ) pour déduire automatiquement l'endpoint latest.
- `repository` : dépôt cible (`org/repo` ou `group/subgroup/repo` ).
2026-04-13 13:53:53 +00:00
- `base_url` : base de la forge ou du service de release.
2026-04-14 12:11:43 +00:00
- `latest_release_url` : URL complète qui retourne la release la plus récente (prioritaire sur le driver).
- `asset_name_template` : template de nom d'asset (`{binary}`, `{os}` , `{arch}` , `{ext}` ).
- `checksum_asset_name` : nom d'asset checksum, avec placeholder optionnel `{asset}` .
- `checksum_required` : si `true` , l'update échoue quand l'asset checksum est absent.
2026-04-15 10:20:06 +00:00
- `signature_asset_name` : nom d'asset signature Ed25519 (détachée), avec placeholder optionnel `{asset}` .
- `signature_required` : si `true` , l'update échoue si la signature ou la clé publique manquent, ou si la signature est invalide.
- `signature_public_key` : clé publique Ed25519 (hex ou base64) utilisée pour vérifier la signature.
- `signature_public_key_env_names` : variables d'environnement candidates contenant la clé publique Ed25519.
2026-04-13 13:57:35 +00:00
- `token_header` : header HTTP à utiliser pour l'authentification.
2026-04-14 12:11:43 +00:00
- `token_prefix` : préfixe appliqué devant le token (`Bearer`, `token` , ...).
2026-04-13 13:53:53 +00:00
- `token_env_names` : liste de variables d'environnement candidates pour retrouver le token.
2026-04-14 10:30:56 +00:00
- `[environment].known` : variables d'environnement connues du projet.
- `[secret_store].backend_policy` : politique de secret store (`auto`, `kwallet-only` , `keyring-any` , `env-only` ).
- `[profiles].default` : profil recommandé par défaut.
- `[profiles].known` : profils connus du projet.
- `[bootstrap].description` : description CLI utilisée par le bootstrap.
Toutes ces sections (hors `[update]` selon les besoins de l'application) sont optionnelles.
2026-04-13 13:53:53 +00:00
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()
2026-04-14 10:30:56 +00:00
bootstrapInfo := file.BootstrapInfo()
scaffoldInfo := file.ScaffoldInfo()
_ = bootstrapInfo
_ = scaffoldInfo
2026-04-13 13:53:53 +00:00
```
2026-04-14 13:39:21 +00:00
## Scaffolding
Le package `scaffold` génère un point de départ cohérent pour un nouveau binaire MCP :
- arborescence recommandée (`cmd/< binary > /main.go`, `internal/app/app.go` , `mcp.toml` )
2026-04-15 08:33:13 +00:00
- script `install.sh` prêt à publier (`curl .../install.sh | bash`) avec wizard TUI (setup, apply Claude/Codex, JSON MCP)
2026-04-14 13:39:21 +00:00
- wiring initial `bootstrap + config + secretstore + update`
- `README.md` de démarrage
Exemple :
```go
result, err := scaffold.Generate(scaffold.Options{
TargetDir: "./my-mcp",
ModulePath: "gitea.lclr.dev/AI/my-mcp",
BinaryName: "my-mcp",
Description: "Client MCP interne",
DefaultProfile: "prod",
Profiles: []string{"dev", "prod"},
})
if err != nil {
return err
}
fmt.Printf("Scaffold generated in %s (%d files)\n", result.Root, len(result.Files))
```
2026-04-13 13:53:53 +00:00
## Config JSON
2026-04-13 13:57:35 +00:00
Le package `config` stocke une structure générique par profil dans un JSON privé
2026-04-13 13:53:53 +00:00
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 :
2026-04-13 13:57:35 +00:00
- 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
2026-04-13 15:28:47 +00:00
- `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
},
})
```
2026-04-13 13:53:53 +00:00
## Secrets
2026-04-13 14:47:22 +00:00
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 :
2026-04-13 13:53:53 +00:00
- macOS : Keychain
- Linux : Secret Service ou KWallet selon l'environnement
- Windows : Credential Manager
2026-04-14 14:40:50 +00:00
Ouverture recommandée depuis la policy du manifeste runtime (`mcp.toml` résolu
près de l'exécutable) :
```go
store, err := secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
ServiceName: "my-mcp",
LookupEnv: os.LookupEnv,
})
if err != nil {
return err
}
```
Exemple bas niveau :
2026-04-13 13:53:53 +00:00
```go
store, err := secretstore.Open(secretstore.Options{
2026-04-13 14:47:22 +00:00
ServiceName: "my-mcp",
BackendPolicy: secretstore.BackendAuto,
2026-04-13 13:53:53 +00:00
})
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
}
```
2026-04-13 14:47:22 +00:00
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` .
2026-04-13 13:57:35 +00:00
## Helpers CLI
2026-04-13 13:53:53 +00:00
`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
}
```
2026-04-14 07:17:45 +00:00
Pour décrire un setup complet sans réécrire la boucle interactive :
```go
result, err := cli.RunSetup(cli.SetupOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Fields: []cli.SetupField{
{
Name: "base_url",
Label: "Base URL",
Type: cli.SetupFieldURL,
Required: true,
},
{
Name: "api_token",
Label: "API token",
Type: cli.SetupFieldSecret,
Required: true,
ExistingSecret: storedToken, // conserve la valeur existante si l'utilisateur laisse vide
},
{
Name: "enabled",
Label: "Enable integration",
Type: cli.SetupFieldBool,
Default: "true",
},
{
Name: "scopes",
Label: "Scopes",
Type: cli.SetupFieldList,
Default: "read,write",
Normalize: func(value string) string { return strings.TrimSpace(strings.ToLower(value)) },
},
},
})
if err != nil {
return err
}
baseURL, _ := result.Get("base_url")
apiToken, _ := result.Get("api_token")
enabled, _ := result.Get("enabled")
scopes, _ := result.Get("scopes")
if apiToken.KeptStoredSecret {
fmt.Println("Stored token kept.")
}
```
Chaque champ peut déclarer ses propres hooks de validation (`Validate`, `ValidateBool` , `ValidateList` ).
Les validations sont appliquées de manière cohérente en TTY et en stdin non interactif.
2026-04-14 06:55:01 +00:00
Pour standardiser la résolution `flag > env > config > secret` avec provenance :
```go
2026-04-14 14:48:26 +00:00
store, err := secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
ServiceName: "my-mcp",
})
if err != nil {
return err
2026-04-14 06:55:01 +00:00
}
2026-04-14 14:48:26 +00:00
lookup := cli.ResolveLookup(cli.ResolveLookupOptions{
Flag: cli.MapLookup(flagValues),
Env: cli.EnvLookup(os.LookupEnv),
Config: cli.ConfigMap(configValues),
Secret: cli.SecretStore(store), // ErrNotFound => valeur absente, pas une erreur
})
2026-04-14 06:55:01 +00:00
resolution, err := cli.ResolveFields(cli.ResolveOptions{
Fields: []cli.FieldSpec{
{Name: "base_url", Required: true, FlagKey: "base-url", EnvKey: "MY_MCP_BASE_URL"},
{Name: "api_token", Required: true, EnvKey: "MY_MCP_API_TOKEN", SecretKey: "my-mcp-api-token"},
{Name: "timeout", DefaultValue: "30s"},
},
Lookup: lookup,
})
if err != nil {
return err
}
if err := cli.RenderResolutionProvenance(os.Stdout, resolution); err != nil {
return err
}
```
`ResolveOptions.Order` permet de changer la priorité globale si nécessaire, et
`FieldSpec.Sources` permet de définir un ordre spécifique pour un champ.
2026-04-13 18:53:36 +00:00
Le package fournit aussi un socle réutilisable pour une commande `doctor` .
L'application cliente choisit comment exposer la commande (`flag`, `cobra` , etc.),
mais peut réutiliser les checks communs et ajouter ses propres hooks :
```go
report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
ConfigCheck: cli.NewConfigCheck(store),
SecretStoreCheck: cli.SecretStoreAvailabilityCheck(func() (secretstore.Store, error) {
2026-04-14 14:40:50 +00:00
return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
2026-04-13 18:53:36 +00:00
ServiceName: "my-mcp",
})
}),
RequiredSecrets: []cli.DoctorSecret{
{Name: "api-token", Label: "API token"},
},
SecretStoreFactory: func() (secretstore.Store, error) {
2026-04-14 14:40:50 +00:00
return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
2026-04-13 18:53:36 +00:00
ServiceName: "my-mcp",
})
},
ManifestDir: ".",
ConnectivityCheck: func(context.Context) cli.DoctorResult {
if err := pingBackend(); err != nil {
return cli.DoctorResult{
Name: "connectivity",
Status: cli.DoctorStatusFail,
Summary: "backend is unreachable",
Detail: err.Error(),
}
}
return cli.DoctorResult{
Name: "connectivity",
Status: cli.DoctorStatusOK,
Summary: "backend is reachable",
}
},
})
if err := cli.RenderDoctorReport(os.Stdout, report); err != nil {
return err
}
if report.HasFailures() {
os.Exit(1)
}
```
2026-04-14 14:53:26 +00:00
Pour éviter des checks custom répétitifs sur les profils, `doctor` expose aussi
un helper basé sur `FieldSpec` :
```go
report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
ExtraChecks: []cli.DoctorCheck{
cli.RequiredResolvedFieldsCheck(cli.ResolveOptions{
Fields: []cli.FieldSpec{
{Name: "base_url", Required: true, EnvKey: "MY_MCP_BASE_URL"},
{
Name: "api_token",
Required: true,
EnvKey: "MY_MCP_API_TOKEN",
SecretKey: "my-mcp-api-token",
Sources: []cli.ValueSource{cli.SourceEnv, cli.SourceSecret},
},
},
Lookup: cli.ResolveLookup(cli.ResolveLookupOptions{
Env: cli.EnvLookup(os.LookupEnv),
Config: cli.ConfigMap(configValues),
Secret: cli.SecretStore(secretStore), // provenance explicite: env ou secret
}),
}),
},
})
```
En cas d'échec de résolution, tu peux aussi réutiliser le formatteur
`cli.FormatResolveFieldsError(err)` dans un check custom pour garder des messages homogènes.
2026-04-13 13:53:53 +00:00
## Auto-Update
2026-04-14 12:11:43 +00:00
Le package `update` supporte les drivers `gitea` , `gitlab` et `github` .
Si `latest_release_url` est vide, l'URL latest est déduite depuis
`driver + repository (+ base_url)` .
Le parseur de release supporte :
- format `assets.links` (Gitea/GitLab)
- format `assets[]` avec `browser_download_url` (GitHub et Gitea API)
2026-04-13 13:53:53 +00:00
2026-04-13 13:57:35 +00:00
Le format attendu pour la réponse `latest release` est actuellement :
2026-04-13 13:53:53 +00:00
```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
}
```
2026-04-14 12:11:43 +00:00
Comportement :
2026-04-13 13:53:53 +00:00
2026-04-14 12:11:43 +00:00
- le nom de l'asset est configurable (`asset_name_template`) et supporte tout couple `GOOS/GOARCH`
- si un asset `<asset>.sha256` (ou `checksum_asset_name` ) existe, le binaire téléchargé est vérifié avant remplacement
- un hook `ValidateDownloaded` permet d'ajouter une validation custom (signature, scan, etc.)
- sur Windows, le remplacement in-place n'est pas fait par défaut ; fournir `Options.ReplaceExecutable` pour une stratégie dédiée
2026-04-13 13:53:53 +00:00
## 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
- l'auto-update reste volontairement simple et ne supporte pas encore de scripts externes