package cli import ( "context" "io" "os" "strings" "email-mcp/mcpgen" frameworkconfig "forge.lclr.dev/AI/mcp-framework/config" frameworksecretstore "forge.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 { useGeneratedManifest := f.loadManifest == nil 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](mcpgen.BinaryName) } } if f.loadManifest == nil { f.loadManifest = mcpgen.LoadManifest } if f.resolveExecutable == nil { f.resolveExecutable = os.Executable } if f.openSecretStore == nil { f.openSecretStore = func() (secretStore, error) { if useGeneratedManifest { return mcpgen.OpenSecretStore(mcpgen.SecretStoreOptions{ ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable), LookupEnv: profilePasswordLookupEnv, }) } return frameworksecretstore.OpenFromManifest(frameworksecretstore.OpenFromManifestOptions{ ServiceName: mcpgen.BinaryName, ManifestLoader: frameworksecretstore.ManifestLoader(f.loadManifest), ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable), LookupEnv: profilePasswordLookupEnv, }) } } 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 } 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 { 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 }