email-mcp/internal/cli/wire.go
2026-04-14 15:54:17 +02:00

162 lines
4.6 KiB
Go

package cli
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
"email-mcp/internal/imapclient"
"email-mcp/internal/mcpserver"
"email-mcp/internal/secretstore"
)
type runtimeFactories struct {
newPrompter func(io.Reader, io.Writer) ConfigPrompter
newConfigStore func() profileConfigStore
openSecretStore func() (secretStore, error)
newMailService func() mcpserver.MailService
newRunner func(secretstore.Credential, mcpserver.MailService, io.Reader, io.Writer, io.Writer) MCPRunner
loadManifest manifestLoader
resolveExecutable executableResolver
}
func BuildApp(version string) *App {
return buildApp(os.Stdin, os.Stdout, os.Stderr, version, runtimeFactories{})
}
func buildApp(stdin io.Reader, stdout io.Writer, stderr io.Writer, version string, factories runtimeFactories) *App {
factories = factories.withDefaults()
return NewAppWithDependencies(
factories.newPrompter(stdin, stderr),
factories.newConfigStore(),
factories.openSecretStore,
factories.newMailService,
factories.newRunner,
factories.loadManifest,
factories.resolveExecutable,
stdin,
stdout,
stderr,
version,
)
}
func (f runtimeFactories) withDefaults() runtimeFactories {
if f.newPrompter == nil {
f.newPrompter = func(input io.Reader, output io.Writer) ConfigPrompter {
return NewInteractiveConfigPrompter(input, output)
}
}
if f.newConfigStore == nil {
f.newConfigStore = func() profileConfigStore {
return frameworkconfig.NewStore[ProfileConfig]("email-mcp")
}
}
if f.loadManifest == nil {
f.loadManifest = frameworkmanifest.LoadDefault
}
if f.resolveExecutable == nil {
f.resolveExecutable = os.Executable
}
if f.openSecretStore == nil {
f.openSecretStore = func() (secretStore, error) {
policy, err := resolveSecretStorePolicy(f.loadManifest, f.resolveExecutable)
if err != nil {
return nil, err
}
return frameworksecretstore.Open(frameworksecretstore.Options{
ServiceName: "email-mcp",
BackendPolicy: policy,
LookupEnv: func(name string) (string, bool) {
trimmedName := strings.TrimSpace(name)
if strings.HasPrefix(trimmedName, "imap-password/") {
return os.LookupEnv(passwordEnv)
}
return os.LookupEnv(trimmedName)
},
})
}
}
if f.newMailService == nil {
f.newMailService = func() mcpserver.MailService {
return imapclient.NewService(imapclient.NewDefaultBackend())
}
}
if f.newRunner == nil {
f.newRunner = func(cred secretstore.Credential, mail mcpserver.MailService, input io.Reader, output io.Writer, errOut io.Writer) MCPRunner {
return mcpserver.NewRunner(mcpserver.New(staticCredentialStore{credential: cred}, mail), input, output, errOut)
}
}
return f
}
type staticCredentialStore struct {
credential secretstore.Credential
}
func (s staticCredentialStore) Save(_ context.Context, _ string, _ secretstore.Credential) error {
return nil
}
func (s staticCredentialStore) Load(_ context.Context, _ string) (secretstore.Credential, error) {
return s.credential, nil
}
func resolveSecretStorePolicy(loadManifest manifestLoader, resolveExecutable executableResolver) (frameworksecretstore.BackendPolicy, error) {
if loadManifest == nil {
return frameworksecretstore.BackendAuto, nil
}
searchDirs := []string{"."}
if resolveExecutable != nil {
executablePath, err := resolveExecutable()
if err == nil {
searchDirs = append([]string{filepath.Dir(executablePath)}, searchDirs...)
}
}
seen := map[string]struct{}{}
for _, dir := range searchDirs {
trimmedDir := strings.TrimSpace(dir)
if trimmedDir == "" {
continue
}
if _, ok := seen[trimmedDir]; ok {
continue
}
seen[trimmedDir] = struct{}{}
file, _, err := loadManifest(trimmedDir)
if err != nil {
continue
}
return parseSecretBackendPolicy(file.SecretStore.BackendPolicy)
}
return frameworksecretstore.BackendAuto, nil
}
func parseSecretBackendPolicy(raw string) (frameworksecretstore.BackendPolicy, error) {
switch strings.TrimSpace(raw) {
case "", string(frameworksecretstore.BackendAuto):
return frameworksecretstore.BackendAuto, nil
case string(frameworksecretstore.BackendKWalletOnly):
return frameworksecretstore.BackendKWalletOnly, nil
case string(frameworksecretstore.BackendKeyringAny):
return frameworksecretstore.BackendKeyringAny, nil
case string(frameworksecretstore.BackendEnvOnly):
return frameworksecretstore.BackendEnvOnly, nil
default:
return "", fmt.Errorf("invalid secret backend policy %q", strings.TrimSpace(raw))
}
}