feat: upgrade mcp framework generation
This commit is contained in:
parent
be33b467a6
commit
be2b7e631b
17 changed files with 456 additions and 138 deletions
10
Makefile
10
Makefile
|
|
@ -14,7 +14,7 @@ endif
|
||||||
|
|
||||||
OUTPUT := $(BUILD_DIR)/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(EXT)
|
OUTPUT := $(BUILD_DIR)/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(EXT)
|
||||||
|
|
||||||
.PHONY: build test
|
.PHONY: build test generate generate-check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@mkdir -p $(BUILD_DIR) $(GOCACHE)
|
@mkdir -p $(BUILD_DIR) $(GOCACHE)
|
||||||
|
|
@ -23,3 +23,11 @@ build:
|
||||||
test:
|
test:
|
||||||
@mkdir -p $(GOCACHE)
|
@mkdir -p $(GOCACHE)
|
||||||
GOCACHE=$(GOCACHE) go test ./...
|
GOCACHE=$(GOCACHE) go test ./...
|
||||||
|
|
||||||
|
generate:
|
||||||
|
@mkdir -p $(GOCACHE)
|
||||||
|
GOCACHE=$(GOCACHE) go run forge.lclr.dev/AI/mcp-framework/cmd/mcp-framework generate
|
||||||
|
|
||||||
|
generate-check:
|
||||||
|
@mkdir -p $(GOCACHE)
|
||||||
|
GOCACHE=$(GOCACHE) go run forge.lclr.dev/AI/mcp-framework/cmd/mcp-framework generate --check
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -8,6 +8,7 @@ Le binaire s’appuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
|
||||||
- le stockage JSON de configuration dans `os.UserConfigDir()`
|
- le stockage JSON de configuration dans `os.UserConfigDir()`
|
||||||
- le stockage du mot de passe dans le wallet natif de l’OS
|
- le stockage du mot de passe dans le wallet natif de l’OS
|
||||||
- le manifeste `mcp.toml`
|
- le manifeste `mcp.toml`
|
||||||
|
- les helpers Go générés depuis `mcp.toml` (`mcpgen/`)
|
||||||
- l’auto-update via `email-mcp update`
|
- l’auto-update via `email-mcp update`
|
||||||
|
|
||||||
## Commandes
|
## Commandes
|
||||||
|
|
@ -44,7 +45,7 @@ Le profil actif est résolu dans cet ordre :
|
||||||
4. `[profiles].default` dans `mcp.toml`
|
4. `[profiles].default` dans `mcp.toml`
|
||||||
5. `default`
|
5. `default`
|
||||||
|
|
||||||
Les credentials IMAP sont résolus ensuite via le résolveur multi-sources du framework (RC3) :
|
Les credentials IMAP sont résolus ensuite via les champs `[[config.fields]]` du manifeste et les helpers générés par le framework :
|
||||||
|
|
||||||
1. `host` : `EMAIL_MCP_HOST` puis `config.json`
|
1. `host` : `EMAIL_MCP_HOST` puis `config.json`
|
||||||
2. `username` : `EMAIL_MCP_USERNAME` puis `config.json`
|
2. `username` : `EMAIL_MCP_USERNAME` puis `config.json`
|
||||||
|
|
@ -221,3 +222,10 @@ Pour lancer les tests :
|
||||||
```sh
|
```sh
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pour régénérer la glue framework après une modification de `mcp.toml` :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make generate
|
||||||
|
make generate-check
|
||||||
|
```
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module email-mcp
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.4.2
|
forge.lclr.dev/AI/mcp-framework v1.9.0
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||||
github.com/emersion/go-message v0.18.2
|
github.com/emersion/go-message v0.18.2
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.4.2 h1:VRJsnQWnxD0Kzdm/XiasYefe4gYHCt376i44WkkQsas=
|
forge.lclr.dev/AI/mcp-framework v1.9.0 h1:8i2CHQlQo/mRG1BE2UArHptAa/HC7AOhZBIqz8md8Vk=
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.4.2/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
|
forge.lclr.dev/AI/mcp-framework v1.9.0/go.mod h1:2xzmFEHGLQzT5PORq35j10pRhsOm0CDwivUZTHvxgh4=
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
||||||
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,24 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
frameworkbootstrap "gitea.lclr.dev/AI/mcp-framework/bootstrap"
|
"email-mcp/mcpgen"
|
||||||
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
frameworkbootstrap "forge.lclr.dev/AI/mcp-framework/bootstrap"
|
||||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
||||||
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
|
frameworkmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
frameworkupdate "gitea.lclr.dev/AI/mcp-framework/update"
|
frameworksecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
|
frameworkupdate "forge.lclr.dev/AI/mcp-framework/update"
|
||||||
|
|
||||||
"email-mcp/internal/mcpserver"
|
"email-mcp/internal/mcpserver"
|
||||||
"email-mcp/internal/secretstore"
|
"email-mcp/internal/secretstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
binaryName = "email-mcp"
|
binaryName = mcpgen.BinaryName
|
||||||
defaultProfileEnv = "EMAIL_MCP_PROFILE"
|
defaultProfileEnv = "EMAIL_MCP_PROFILE"
|
||||||
hostEnv = "EMAIL_MCP_HOST"
|
hostEnv = "EMAIL_MCP_HOST"
|
||||||
usernameEnv = "EMAIL_MCP_USERNAME"
|
usernameEnv = "EMAIL_MCP_USERNAME"
|
||||||
passwordEnv = "EMAIL_MCP_PASSWORD"
|
passwordEnv = "EMAIL_MCP_PASSWORD"
|
||||||
binaryDescription = "Local MCP server to read an IMAP mailbox."
|
|
||||||
fallbackProfile = "default"
|
fallbackProfile = "default"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -264,7 +264,7 @@ func (a *App) runConfigShow(ctx context.Context, args []string) error {
|
||||||
return mapAppError(err)
|
return mapAppError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolution, err := resolveCredentialFields(profile, secrets, credentialFieldSpecs(profileName))
|
resolution, err := resolveCredentialFields(profile, secrets, mcpgen.ResolveFieldSpecs(profileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var missingErr *frameworkcli.MissingRequiredValuesError
|
var missingErr *frameworkcli.MissingRequiredValuesError
|
||||||
if !errors.As(err, &missingErr) {
|
if !errors.As(err, &missingErr) {
|
||||||
|
|
@ -393,18 +393,13 @@ func (a *App) runUpdate(ctx context.Context, args []string) error {
|
||||||
return fmt.Errorf("resolve executable path: %w", err)
|
return fmt.Errorf("resolve executable path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestFile, err := a.loadManifestForExecutable(executablePath)
|
options, err := mcpgen.UpdateOptionsFrom(filepath.Dir(executablePath), a.version, a.stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
options.ExecutablePath = executablePath
|
||||||
|
|
||||||
return frameworkupdate.Run(ctx, frameworkupdate.Options{
|
return frameworkupdate.Run(ctx, options)
|
||||||
CurrentVersion: a.version,
|
|
||||||
ExecutablePath: executablePath,
|
|
||||||
BinaryName: a.runtimeMetadata().BinaryName,
|
|
||||||
ReleaseSource: manifestFile.Update.ReleaseSource(),
|
|
||||||
Stdout: a.stdout,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) loadManifestForExecutable(executablePath string) (frameworkmanifest.File, error) {
|
func (a *App) loadManifestForExecutable(executablePath string) (frameworkmanifest.File, error) {
|
||||||
|
|
@ -445,7 +440,7 @@ func (a *App) loadCredential(profileFlag string) (secretstore.Credential, error)
|
||||||
return secretstore.Credential{}, err
|
return secretstore.Credential{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resolution, err := resolveCredentialFields(profile, secrets, credentialFieldSpecs(profileName))
|
resolution, err := resolveCredentialFields(profile, secrets, mcpgen.ResolveFieldSpecs(profileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var missingErr *frameworkcli.MissingRequiredValuesError
|
var missingErr *frameworkcli.MissingRequiredValuesError
|
||||||
if errors.As(err, &missingErr) {
|
if errors.As(err, &missingErr) {
|
||||||
|
|
@ -470,68 +465,24 @@ func (a *App) loadCredential(profileFlag string) (secretstore.Credential, error)
|
||||||
return cred, nil
|
return cred, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func credentialFieldSpecs(profileName string) []frameworkcli.FieldSpec {
|
func profileFieldSpecs(profileName string) []frameworkcli.FieldSpec {
|
||||||
return []frameworkcli.FieldSpec{
|
specs := mcpgen.ResolveFieldSpecs(profileName)
|
||||||
{
|
profileSpecs := make([]frameworkcli.FieldSpec, 0, len(specs))
|
||||||
Name: "host",
|
for _, spec := range specs {
|
||||||
Required: true,
|
if spec.Name == "host" || spec.Name == "username" {
|
||||||
Sources: []frameworkcli.ValueSource{
|
profileSpecs = append(profileSpecs, spec)
|
||||||
frameworkcli.SourceEnv,
|
|
||||||
frameworkcli.SourceConfig,
|
|
||||||
},
|
|
||||||
EnvKey: hostEnv,
|
|
||||||
ConfigKey: "host",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "username",
|
|
||||||
Required: true,
|
|
||||||
Sources: []frameworkcli.ValueSource{
|
|
||||||
frameworkcli.SourceEnv,
|
|
||||||
frameworkcli.SourceConfig,
|
|
||||||
},
|
|
||||||
EnvKey: usernameEnv,
|
|
||||||
ConfigKey: "username",
|
|
||||||
},
|
|
||||||
passwordFieldSpec(profileName),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return profileSpecs
|
||||||
|
}
|
||||||
|
|
||||||
func profileFieldSpecs() []frameworkcli.FieldSpec {
|
func passwordOnlyFieldSpecs(profileName string) []frameworkcli.FieldSpec {
|
||||||
return []frameworkcli.FieldSpec{
|
for _, spec := range mcpgen.ResolveFieldSpecs(profileName) {
|
||||||
{
|
if spec.Name == "password" {
|
||||||
Name: "host",
|
return []frameworkcli.FieldSpec{spec}
|
||||||
Required: true,
|
|
||||||
Sources: []frameworkcli.ValueSource{
|
|
||||||
frameworkcli.SourceEnv,
|
|
||||||
frameworkcli.SourceConfig,
|
|
||||||
},
|
|
||||||
EnvKey: hostEnv,
|
|
||||||
ConfigKey: "host",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "username",
|
|
||||||
Required: true,
|
|
||||||
Sources: []frameworkcli.ValueSource{
|
|
||||||
frameworkcli.SourceEnv,
|
|
||||||
frameworkcli.SourceConfig,
|
|
||||||
},
|
|
||||||
EnvKey: usernameEnv,
|
|
||||||
ConfigKey: "username",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
func passwordFieldSpec(profileName string) frameworkcli.FieldSpec {
|
|
||||||
return frameworkcli.FieldSpec{
|
|
||||||
Name: "password",
|
|
||||||
Required: true,
|
|
||||||
Sources: []frameworkcli.ValueSource{
|
|
||||||
frameworkcli.SourceEnv,
|
|
||||||
frameworkcli.SourceSecret,
|
|
||||||
},
|
|
||||||
EnvKey: passwordEnv,
|
|
||||||
SecretKey: passwordSecretName(profileName),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveCredentialFields(profile ProfileConfig, store secretStore, fields []frameworkcli.FieldSpec) (frameworkcli.Resolution, error) {
|
func resolveCredentialFields(profile ProfileConfig, store secretStore, fields []frameworkcli.FieldSpec) (frameworkcli.Resolution, error) {
|
||||||
|
|
@ -586,6 +537,11 @@ func loadStoredPassword(store secretStore, profileName string) (string, bool, er
|
||||||
}
|
}
|
||||||
|
|
||||||
func passwordSecretName(profileName string) string {
|
func passwordSecretName(profileName string) string {
|
||||||
|
for _, spec := range mcpgen.ResolveFieldSpecs(profileName) {
|
||||||
|
if spec.Name == "password" && strings.TrimSpace(spec.SecretKey) != "" {
|
||||||
|
return spec.SecretKey
|
||||||
|
}
|
||||||
|
}
|
||||||
return "imap-password/" + strings.TrimSpace(profileName)
|
return "imap-password/" + strings.TrimSpace(profileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,8 +589,8 @@ type runtimeMetadata struct {
|
||||||
|
|
||||||
func (a *App) runtimeMetadata() runtimeMetadata {
|
func (a *App) runtimeMetadata() runtimeMetadata {
|
||||||
metadata := runtimeMetadata{
|
metadata := runtimeMetadata{
|
||||||
BinaryName: binaryName,
|
BinaryName: mcpgen.BinaryName,
|
||||||
Description: binaryDescription,
|
Description: mcpgen.DefaultDescription,
|
||||||
DefaultProfile: fallbackProfile,
|
DefaultProfile: fallbackProfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
|
frameworksecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
|
|
||||||
"email-mcp/internal/imapclient"
|
"email-mcp/internal/imapclient"
|
||||||
"email-mcp/internal/mcpserver"
|
"email-mcp/internal/mcpserver"
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
"email-mcp/mcpgen"
|
||||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
||||||
frameworkupdate "gitea.lclr.dev/AI/mcp-framework/update"
|
frameworkmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
frameworkupdate "forge.lclr.dev/AI/mcp-framework/update"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) runDoctor(ctx context.Context, args []string) error {
|
func (a *App) runDoctor(ctx context.Context, args []string) error {
|
||||||
|
|
@ -33,7 +34,7 @@ func (a *App) runDoctor(ctx context.Context, args []string) error {
|
||||||
|
|
||||||
metadata := a.runtimeMetadata()
|
metadata := a.runtimeMetadata()
|
||||||
report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{
|
report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{
|
||||||
ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](binaryName)),
|
ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)),
|
||||||
SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore),
|
SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore),
|
||||||
ManifestDir: a.doctorManifestDir(),
|
ManifestDir: a.doctorManifestDir(),
|
||||||
ManifestValidator: func(file frameworkmanifest.File, _ string) []string {
|
ManifestValidator: func(file frameworkmanifest.File, _ string) []string {
|
||||||
|
|
@ -73,7 +74,7 @@ func (a *App) doctorRequiredProfileFieldsCheck(profileFlag string) frameworkcli.
|
||||||
)
|
)
|
||||||
|
|
||||||
check := frameworkcli.RequiredResolvedFieldsCheck(frameworkcli.ResolveOptions{
|
check := frameworkcli.RequiredResolvedFieldsCheck(frameworkcli.ResolveOptions{
|
||||||
Fields: profileFieldSpecs(),
|
Fields: profileFieldSpecs(a.resolveDoctorProfileName(profileFlag)),
|
||||||
Lookup: frameworkcli.ResolveLookup(frameworkcli.ResolveLookupOptions{
|
Lookup: frameworkcli.ResolveLookup(frameworkcli.ResolveLookupOptions{
|
||||||
Env: frameworkcli.EnvLookup(os.LookupEnv),
|
Env: frameworkcli.EnvLookup(os.LookupEnv),
|
||||||
Config: func(key string) (string, bool, error) {
|
Config: func(key string) (string, bool, error) {
|
||||||
|
|
@ -123,7 +124,7 @@ func (a *App) doctorPasswordCheck(profileFlag string) frameworkcli.DoctorCheck {
|
||||||
resolution, err := resolveCredentialFields(
|
resolution, err := resolveCredentialFields(
|
||||||
ProfileConfig{},
|
ProfileConfig{},
|
||||||
store,
|
store,
|
||||||
[]frameworkcli.FieldSpec{passwordFieldSpec(profileName)},
|
passwordOnlyFieldSpecs(profileName),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var missingErr *frameworkcli.MissingRequiredValuesError
|
var missingErr *frameworkcli.MissingRequiredValuesError
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
"email-mcp/mcpgen"
|
||||||
|
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
|
||||||
|
|
||||||
"email-mcp/internal/secretstore"
|
"email-mcp/internal/secretstore"
|
||||||
)
|
)
|
||||||
|
|
@ -76,32 +77,20 @@ func (p *InteractiveConfigPrompter) promptCredentialWithSetupEngine(existing sec
|
||||||
password = existing.Password
|
password = existing.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields := mcpgen.SetupFields(map[string]string{"password": password})
|
||||||
|
for i := range fields {
|
||||||
|
switch fields[i].Name {
|
||||||
|
case "host":
|
||||||
|
fields[i].Default = existing.Host
|
||||||
|
case "username":
|
||||||
|
fields[i].Default = existing.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, err := frameworkcli.RunSetup(frameworkcli.SetupOptions{
|
result, err := frameworkcli.RunSetup(frameworkcli.SetupOptions{
|
||||||
Stdin: p.stdinFile,
|
Stdin: p.stdinFile,
|
||||||
Stdout: p.output,
|
Stdout: p.output,
|
||||||
Fields: []frameworkcli.SetupField{
|
Fields: fields,
|
||||||
{
|
|
||||||
Name: "host",
|
|
||||||
Label: "IMAP host",
|
|
||||||
Type: frameworkcli.SetupFieldString,
|
|
||||||
Required: true,
|
|
||||||
Default: existing.Host,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "username",
|
|
||||||
Label: "Username",
|
|
||||||
Type: frameworkcli.SetupFieldString,
|
|
||||||
Required: true,
|
|
||||||
Default: existing.Username,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "password",
|
|
||||||
Label: "Password",
|
|
||||||
Type: frameworkcli.SetupFieldSecret,
|
|
||||||
Required: true,
|
|
||||||
ExistingSecret: password,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return secretstore.Credential{}, err
|
return secretstore.Credential{}, err
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
"email-mcp/mcpgen"
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
||||||
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
|
frameworksecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
|
|
||||||
"email-mcp/internal/imapclient"
|
"email-mcp/internal/imapclient"
|
||||||
"email-mcp/internal/mcpserver"
|
"email-mcp/internal/mcpserver"
|
||||||
|
|
@ -48,6 +48,7 @@ func buildApp(stdin io.Reader, stdout io.Writer, stderr io.Writer, version strin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f runtimeFactories) withDefaults() runtimeFactories {
|
func (f runtimeFactories) withDefaults() runtimeFactories {
|
||||||
|
useGeneratedManifest := f.loadManifest == nil
|
||||||
if f.newPrompter == nil {
|
if f.newPrompter == nil {
|
||||||
f.newPrompter = func(input io.Reader, output io.Writer) ConfigPrompter {
|
f.newPrompter = func(input io.Reader, output io.Writer) ConfigPrompter {
|
||||||
return NewInteractiveConfigPrompter(input, output)
|
return NewInteractiveConfigPrompter(input, output)
|
||||||
|
|
@ -55,28 +56,29 @@ func (f runtimeFactories) withDefaults() runtimeFactories {
|
||||||
}
|
}
|
||||||
if f.newConfigStore == nil {
|
if f.newConfigStore == nil {
|
||||||
f.newConfigStore = func() profileConfigStore {
|
f.newConfigStore = func() profileConfigStore {
|
||||||
return frameworkconfig.NewStore[ProfileConfig]("email-mcp")
|
return frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if f.loadManifest == nil {
|
if f.loadManifest == nil {
|
||||||
f.loadManifest = frameworkmanifest.LoadDefault
|
f.loadManifest = mcpgen.LoadManifest
|
||||||
}
|
}
|
||||||
if f.resolveExecutable == nil {
|
if f.resolveExecutable == nil {
|
||||||
f.resolveExecutable = os.Executable
|
f.resolveExecutable = os.Executable
|
||||||
}
|
}
|
||||||
if f.openSecretStore == nil {
|
if f.openSecretStore == nil {
|
||||||
f.openSecretStore = func() (secretStore, error) {
|
f.openSecretStore = func() (secretStore, error) {
|
||||||
|
if useGeneratedManifest {
|
||||||
|
return mcpgen.OpenSecretStore(mcpgen.SecretStoreOptions{
|
||||||
|
ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable),
|
||||||
|
LookupEnv: profilePasswordLookupEnv,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return frameworksecretstore.OpenFromManifest(frameworksecretstore.OpenFromManifestOptions{
|
return frameworksecretstore.OpenFromManifest(frameworksecretstore.OpenFromManifestOptions{
|
||||||
ServiceName: "email-mcp",
|
ServiceName: mcpgen.BinaryName,
|
||||||
ManifestLoader: frameworksecretstore.ManifestLoader(f.loadManifest),
|
ManifestLoader: frameworksecretstore.ManifestLoader(f.loadManifest),
|
||||||
ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable),
|
ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable),
|
||||||
LookupEnv: func(name string) (string, bool) {
|
LookupEnv: profilePasswordLookupEnv,
|
||||||
trimmedName := strings.TrimSpace(name)
|
|
||||||
if strings.HasPrefix(trimmedName, "imap-password/") {
|
|
||||||
return os.LookupEnv(passwordEnv)
|
|
||||||
}
|
|
||||||
return os.LookupEnv(trimmedName)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +96,14 @@ func (f runtimeFactories) withDefaults() runtimeFactories {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func profilePasswordLookupEnv(name string) (string, bool) {
|
||||||
|
trimmedName := strings.TrimSpace(name)
|
||||||
|
if strings.HasPrefix(trimmedName, "imap-password/") {
|
||||||
|
return os.LookupEnv(passwordEnv)
|
||||||
|
}
|
||||||
|
return os.LookupEnv(trimmedName)
|
||||||
|
}
|
||||||
|
|
||||||
type staticCredentialStore struct {
|
type staticCredentialStore struct {
|
||||||
credential secretstore.Credential
|
credential secretstore.Credential
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildAppReturnsConfiguredApp(t *testing.T) {
|
func TestBuildAppReturnsConfiguredApp(t *testing.T) {
|
||||||
|
|
|
||||||
27
mcp.toml
27
mcp.toml
|
|
@ -23,3 +23,30 @@ known = ["default"]
|
||||||
|
|
||||||
[bootstrap]
|
[bootstrap]
|
||||||
description = "Local MCP server to read an IMAP mailbox."
|
description = "Local MCP server to read an IMAP mailbox."
|
||||||
|
|
||||||
|
[[config.fields]]
|
||||||
|
name = "host"
|
||||||
|
env = "EMAIL_MCP_HOST"
|
||||||
|
config_key = "host"
|
||||||
|
type = "string"
|
||||||
|
label = "IMAP host"
|
||||||
|
required = true
|
||||||
|
sources = ["env", "config"]
|
||||||
|
|
||||||
|
[[config.fields]]
|
||||||
|
name = "username"
|
||||||
|
env = "EMAIL_MCP_USERNAME"
|
||||||
|
config_key = "username"
|
||||||
|
type = "string"
|
||||||
|
label = "Username"
|
||||||
|
required = true
|
||||||
|
sources = ["env", "config"]
|
||||||
|
|
||||||
|
[[config.fields]]
|
||||||
|
name = "password"
|
||||||
|
env = "EMAIL_MCP_PASSWORD"
|
||||||
|
secret_key_template = "imap-password/{profile}"
|
||||||
|
type = "secret"
|
||||||
|
label = "Password"
|
||||||
|
required = true
|
||||||
|
sources = ["env", "secret"]
|
||||||
|
|
|
||||||
63
mcpgen/config.go
Normal file
63
mcpgen/config.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Code generated by mcp-framework generate. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fwcli "forge.lclr.dev/AI/mcp-framework/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigFlags struct {
|
||||||
|
values map[string]*string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddConfigFlags(fs *flag.FlagSet) ConfigFlags {
|
||||||
|
if fs == nil {
|
||||||
|
fs = flag.CommandLine
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := ConfigFlags{
|
||||||
|
values: make(map[string]*string),
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFlagValues(flags ConfigFlags) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
for name, value := range flags.values {
|
||||||
|
if value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if trimmed := strings.TrimSpace(*value); trimmed != "" {
|
||||||
|
values[name] = trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveFieldSpecs(profile string) []fwcli.FieldSpec {
|
||||||
|
return []fwcli.FieldSpec{
|
||||||
|
{Name: "host", Required: true, DefaultValue: "", Sources: []fwcli.ValueSource{fwcli.SourceEnv, fwcli.SourceConfig}, FlagKey: "", EnvKey: "EMAIL_MCP_HOST", ConfigKey: "host", SecretKey: replaceProfile("", profile)},
|
||||||
|
{Name: "username", Required: true, DefaultValue: "", Sources: []fwcli.ValueSource{fwcli.SourceEnv, fwcli.SourceConfig}, FlagKey: "", EnvKey: "EMAIL_MCP_USERNAME", ConfigKey: "username", SecretKey: replaceProfile("", profile)},
|
||||||
|
{Name: "password", Required: true, DefaultValue: "", Sources: []fwcli.ValueSource{fwcli.SourceEnv, fwcli.SourceSecret}, FlagKey: "", EnvKey: "EMAIL_MCP_PASSWORD", ConfigKey: "", SecretKey: replaceProfile("imap-password/{profile}", profile)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupFields(existing map[string]string) []fwcli.SetupField {
|
||||||
|
if existing == nil {
|
||||||
|
existing = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []fwcli.SetupField{
|
||||||
|
{Name: "host", Label: "IMAP host", Type: fwcli.SetupFieldString, Required: true, Default: "", ExistingSecret: existing["host"]},
|
||||||
|
{Name: "username", Label: "Username", Type: fwcli.SetupFieldString, Required: true, Default: "", ExistingSecret: existing["username"]},
|
||||||
|
{Name: "password", Label: "Password", Type: fwcli.SetupFieldSecret, Required: true, Default: "", ExistingSecret: existing["password"]},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceProfile(value, profile string) string {
|
||||||
|
return strings.ReplaceAll(value, "{profile}", strings.TrimSpace(profile))
|
||||||
|
}
|
||||||
68
mcpgen/generated_test.go
Normal file
68
mcpgen/generated_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneratedManifestFallsBackToEmbeddedRootManifest(t *testing.T) {
|
||||||
|
previousDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Getwd returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("Chdir temp dir returned error: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.Chdir(previousDir); err != nil {
|
||||||
|
t.Fatalf("restore working directory: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
manifestFile, source, err := LoadManifest(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadManifest returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if source != fwmanifest.EmbeddedSource {
|
||||||
|
t.Fatalf("source = %q, want %q", source, fwmanifest.EmbeddedSource)
|
||||||
|
}
|
||||||
|
if manifestFile.BinaryName != "email-mcp" {
|
||||||
|
t.Fatalf("BinaryName = %q, want email-mcp", manifestFile.BinaryName)
|
||||||
|
}
|
||||||
|
if len(manifestFile.Config.Fields) != 3 {
|
||||||
|
t.Fatalf("config fields = %d, want 3", len(manifestFile.Config.Fields))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratedConfigHelpersExposeIMAPResolutionFields(t *testing.T) {
|
||||||
|
specs := ResolveFieldSpecs("work")
|
||||||
|
if len(specs) != 3 {
|
||||||
|
t.Fatalf("ResolveFieldSpecs returned %d fields, want 3", len(specs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if specs[0].Name != "host" || specs[0].EnvKey != "EMAIL_MCP_HOST" || specs[0].ConfigKey != "host" {
|
||||||
|
t.Fatalf("host spec = %+v", specs[0])
|
||||||
|
}
|
||||||
|
if specs[1].Name != "username" || specs[1].EnvKey != "EMAIL_MCP_USERNAME" || specs[1].ConfigKey != "username" {
|
||||||
|
t.Fatalf("username spec = %+v", specs[1])
|
||||||
|
}
|
||||||
|
if specs[2].Name != "password" || specs[2].EnvKey != "EMAIL_MCP_PASSWORD" || specs[2].SecretKey != "imap-password/work" {
|
||||||
|
t.Fatalf("password spec = %+v", specs[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratedManifestPrefersRootFileWhenPresent(t *testing.T) {
|
||||||
|
manifestFile, source, err := LoadManifest(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadManifest returned error: %v", err)
|
||||||
|
}
|
||||||
|
if source == fwmanifest.EmbeddedSource {
|
||||||
|
t.Fatalf("source = %q, want root manifest path", source)
|
||||||
|
}
|
||||||
|
if manifestFile.BinaryName != "email-mcp" {
|
||||||
|
t.Fatalf("BinaryName = %q, want email-mcp", manifestFile.BinaryName)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mcpgen/manifest.go
Normal file
11
mcpgen/manifest.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Code generated by mcp-framework generate. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
|
||||||
|
const embeddedManifest = "binary_name = \"email-mcp\"\ndocs_url = \"https://gitea.lclr.dev/AI/email-mcp\"\n\n[update]\nsource_name = \"email-mcp releases\"\ndriver = \"gitea\"\nrepository = \"AI/email-mcp\"\nbase_url = \"https://gitea.lclr.dev\"\nasset_name_template = \"{binary}-{os}-{arch}{ext}\"\nchecksum_asset_name = \"{asset}.sha256\"\nchecksum_required = true\ntoken_env_names = [\"GITEA_TOKEN\"]\n\n[environment]\nknown = [\"EMAIL_MCP_PROFILE\", \"EMAIL_MCP_HOST\", \"EMAIL_MCP_USERNAME\", \"EMAIL_MCP_PASSWORD\"]\n\n[secret_store]\nbackend_policy = \"auto\"\n\n[profiles]\ndefault = \"default\"\nknown = [\"default\"]\n\n[bootstrap]\ndescription = \"Local MCP server to read an IMAP mailbox.\"\n\n[[config.fields]]\nname = \"host\"\nenv = \"EMAIL_MCP_HOST\"\nconfig_key = \"host\"\ntype = \"string\"\nlabel = \"IMAP host\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"username\"\nenv = \"EMAIL_MCP_USERNAME\"\nconfig_key = \"username\"\ntype = \"string\"\nlabel = \"Username\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"password\"\nenv = \"EMAIL_MCP_PASSWORD\"\nsecret_key_template = \"imap-password/{profile}\"\ntype = \"secret\"\nlabel = \"Password\"\nrequired = true\nsources = [\"env\", \"secret\"]\n"
|
||||||
|
|
||||||
|
func LoadManifest(startDir string) (fwmanifest.File, string, error) {
|
||||||
|
return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest)
|
||||||
|
}
|
||||||
27
mcpgen/metadata.go
Normal file
27
mcpgen/metadata.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Code generated by mcp-framework generate. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
|
||||||
|
const BinaryName = "email-mcp"
|
||||||
|
const DefaultDescription = "Local MCP server to read an IMAP mailbox."
|
||||||
|
const DocsURL = "https://gitea.lclr.dev/AI/email-mcp"
|
||||||
|
|
||||||
|
func BootstrapInfo(startDir string) (fwmanifest.BootstrapMetadata, string, error) {
|
||||||
|
manifestFile, source, err := LoadManifest(startDir)
|
||||||
|
if err != nil {
|
||||||
|
return fwmanifest.BootstrapMetadata{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifestFile.BootstrapInfo(), source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScaffoldInfo(startDir string) (fwmanifest.ScaffoldMetadata, string, error) {
|
||||||
|
manifestFile, source, err := LoadManifest(startDir)
|
||||||
|
if err != nil {
|
||||||
|
return fwmanifest.ScaffoldMetadata{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifestFile.ScaffoldInfo(), source, nil
|
||||||
|
}
|
||||||
91
mcpgen/secretstore.go
Normal file
91
mcpgen/secretstore.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Code generated by mcp-framework generate. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fwsecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SecretStoreOptions struct {
|
||||||
|
ServiceName string
|
||||||
|
LookupEnv func(string) (string, bool)
|
||||||
|
KWalletAppID string
|
||||||
|
KWalletFolder string
|
||||||
|
BitwardenCommand string
|
||||||
|
BitwardenDebug bool
|
||||||
|
DisableBitwardenCache bool
|
||||||
|
Shell string
|
||||||
|
ExecutableResolver fwsecretstore.ExecutableResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenSecretStore(options SecretStoreOptions) (fwsecretstore.Store, error) {
|
||||||
|
return fwsecretstore.OpenFromManifest(secretStoreOpenOptions(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DescribeSecretRuntime(options SecretStoreOptions) (fwsecretstore.RuntimeDescription, error) {
|
||||||
|
return fwsecretstore.DescribeRuntime(secretStoreDescribeOptions(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func PreflightSecretStore(options SecretStoreOptions) (fwsecretstore.PreflightReport, error) {
|
||||||
|
return fwsecretstore.PreflightFromManifest(secretStoreDescribeOptions(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretStoreOpenOptions(options SecretStoreOptions) fwsecretstore.OpenFromManifestOptions {
|
||||||
|
return fwsecretstore.OpenFromManifestOptions{
|
||||||
|
ServiceName: secretStoreServiceName(options),
|
||||||
|
LookupEnv: options.LookupEnv,
|
||||||
|
KWalletAppID: options.KWalletAppID,
|
||||||
|
KWalletFolder: options.KWalletFolder,
|
||||||
|
BitwardenCommand: options.BitwardenCommand,
|
||||||
|
BitwardenDebug: options.BitwardenDebug,
|
||||||
|
DisableBitwardenCache: options.DisableBitwardenCache,
|
||||||
|
Shell: options.Shell,
|
||||||
|
ManifestLoader: LoadManifest,
|
||||||
|
ExecutableResolver: options.ExecutableResolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretStoreDescribeOptions(options SecretStoreOptions) fwsecretstore.DescribeRuntimeOptions {
|
||||||
|
return fwsecretstore.DescribeRuntimeOptions{
|
||||||
|
ServiceName: secretStoreServiceName(options),
|
||||||
|
LookupEnv: options.LookupEnv,
|
||||||
|
KWalletAppID: options.KWalletAppID,
|
||||||
|
KWalletFolder: options.KWalletFolder,
|
||||||
|
BitwardenCommand: options.BitwardenCommand,
|
||||||
|
BitwardenDebug: options.BitwardenDebug,
|
||||||
|
DisableBitwardenCache: options.DisableBitwardenCache,
|
||||||
|
Shell: options.Shell,
|
||||||
|
ManifestLoader: LoadManifest,
|
||||||
|
ExecutableResolver: options.ExecutableResolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretStoreServiceName(options SecretStoreOptions) string {
|
||||||
|
serviceName := strings.TrimSpace(options.ServiceName)
|
||||||
|
if serviceName != "" {
|
||||||
|
return serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
startDir := "."
|
||||||
|
executableResolver := options.ExecutableResolver
|
||||||
|
if executableResolver == nil {
|
||||||
|
executableResolver = os.Executable
|
||||||
|
}
|
||||||
|
if executablePath, err := executableResolver(); err == nil {
|
||||||
|
if dir := strings.TrimSpace(filepath.Dir(strings.TrimSpace(executablePath))); dir != "" {
|
||||||
|
startDir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifestFile, _, err := LoadManifest(startDir); err == nil {
|
||||||
|
if binaryName := strings.TrimSpace(manifestFile.BinaryName); binaryName != "" {
|
||||||
|
return binaryName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BinaryName
|
||||||
|
}
|
||||||
59
mcpgen/update.go
Normal file
59
mcpgen/update.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Code generated by mcp-framework generate. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mcpgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fwupdate "forge.lclr.dev/AI/mcp-framework/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateOptions(version string, stdout io.Writer) (fwupdate.Options, error) {
|
||||||
|
return UpdateOptionsFrom(".", version, stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (fwupdate.Options, error) {
|
||||||
|
manifestFile, _, err := LoadManifest(startDir)
|
||||||
|
if err != nil {
|
||||||
|
return fwupdate.Options{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryName := strings.TrimSpace(manifestFile.BinaryName)
|
||||||
|
if binaryName == "" {
|
||||||
|
binaryName = BinaryName
|
||||||
|
}
|
||||||
|
|
||||||
|
return fwupdate.Options{
|
||||||
|
CurrentVersion: version,
|
||||||
|
Stdout: stdout,
|
||||||
|
BinaryName: binaryName,
|
||||||
|
ReleaseSource: manifestFile.Update.ReleaseSource(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error {
|
||||||
|
return RunUpdateFrom(ctx, args, ".", version, stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunUpdateFrom(ctx context.Context, args []string, startDir string, version string, stdout io.Writer) error {
|
||||||
|
fs := flag.NewFlagSet("update", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() != 0 {
|
||||||
|
return fmt.Errorf("update does not accept positional arguments: %s", strings.Join(fs.Args(), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := UpdateOptionsFrom(startDir, version, stdout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fwupdate.Run(ctx, options)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue