155 lines
4.4 KiB
Go
155 lines
4.4 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,
|
|
})
|
|
}
|
|
}
|
|
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))
|
|
}
|
|
}
|