2026-04-10 08:17:38 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-13 16:01:28 +00:00
|
|
|
"context"
|
2026-04-10 08:17:38 +00:00
|
|
|
"io"
|
|
|
|
|
"os"
|
2026-04-14 10:48:36 +00:00
|
|
|
"strings"
|
2026-04-10 08:17:38 +00:00
|
|
|
|
2026-05-11 09:16:37 +00:00
|
|
|
"email-mcp/mcpgen"
|
|
|
|
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
|
|
|
|
frameworksecretstore "forge.lclr.dev/AI/mcp-framework/secretstore"
|
2026-04-13 16:01:28 +00:00
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
"email-mcp/internal/imapclient"
|
|
|
|
|
"email-mcp/internal/mcpserver"
|
2026-04-10 08:17:38 +00:00
|
|
|
"email-mcp/internal/secretstore"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
type runtimeFactories struct {
|
2026-04-13 16:01:28 +00:00
|
|
|
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
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:01:28 +00:00
|
|
|
func BuildApp(version string) *App {
|
|
|
|
|
return buildApp(os.Stdin, os.Stdout, os.Stderr, version, runtimeFactories{})
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:01:28 +00:00
|
|
|
func buildApp(stdin io.Reader, stdout io.Writer, stderr io.Writer, version string, factories runtimeFactories) *App {
|
2026-04-10 10:21:58 +00:00
|
|
|
factories = factories.withDefaults()
|
2026-04-10 08:17:38 +00:00
|
|
|
|
2026-04-13 16:01:28 +00:00
|
|
|
return NewAppWithDependencies(
|
|
|
|
|
factories.newPrompter(stdin, stderr),
|
|
|
|
|
factories.newConfigStore(),
|
|
|
|
|
factories.openSecretStore,
|
|
|
|
|
factories.newMailService,
|
|
|
|
|
factories.newRunner,
|
|
|
|
|
factories.loadManifest,
|
|
|
|
|
factories.resolveExecutable,
|
|
|
|
|
stdin,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
version,
|
|
|
|
|
)
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
func (f runtimeFactories) withDefaults() runtimeFactories {
|
2026-05-11 09:16:37 +00:00
|
|
|
useGeneratedManifest := f.loadManifest == nil
|
2026-04-10 10:21:58 +00:00
|
|
|
if f.newPrompter == nil {
|
2026-04-13 16:01:28 +00:00
|
|
|
f.newPrompter = func(input io.Reader, output io.Writer) ConfigPrompter {
|
|
|
|
|
return NewInteractiveConfigPrompter(input, output)
|
2026-04-10 10:21:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-13 16:01:28 +00:00
|
|
|
if f.newConfigStore == nil {
|
|
|
|
|
f.newConfigStore = func() profileConfigStore {
|
2026-05-11 09:16:37 +00:00
|
|
|
return frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)
|
2026-04-13 16:01:28 +00:00
|
|
|
}
|
2026-04-10 10:21:58 +00:00
|
|
|
}
|
2026-04-14 10:48:36 +00:00
|
|
|
if f.loadManifest == nil {
|
2026-05-11 09:16:37 +00:00
|
|
|
f.loadManifest = mcpgen.LoadManifest
|
2026-04-14 10:48:36 +00:00
|
|
|
}
|
|
|
|
|
if f.resolveExecutable == nil {
|
|
|
|
|
f.resolveExecutable = os.Executable
|
|
|
|
|
}
|
2026-04-13 16:01:28 +00:00
|
|
|
if f.openSecretStore == nil {
|
|
|
|
|
f.openSecretStore = func() (secretStore, error) {
|
2026-05-11 09:16:37 +00:00
|
|
|
if useGeneratedManifest {
|
|
|
|
|
return mcpgen.OpenSecretStore(mcpgen.SecretStoreOptions{
|
|
|
|
|
ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable),
|
|
|
|
|
LookupEnv: profilePasswordLookupEnv,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
return frameworksecretstore.OpenFromManifest(frameworksecretstore.OpenFromManifestOptions{
|
2026-05-11 09:16:37 +00:00
|
|
|
ServiceName: mcpgen.BinaryName,
|
2026-04-14 15:09:46 +00:00
|
|
|
ManifestLoader: frameworksecretstore.ManifestLoader(f.loadManifest),
|
|
|
|
|
ExecutableResolver: frameworksecretstore.ExecutableResolver(f.resolveExecutable),
|
2026-05-11 09:16:37 +00:00
|
|
|
LookupEnv: profilePasswordLookupEnv,
|
2026-04-13 16:01:28 +00:00
|
|
|
})
|
2026-04-10 10:21:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if f.newMailService == nil {
|
|
|
|
|
f.newMailService = func() mcpserver.MailService {
|
|
|
|
|
return imapclient.NewService(imapclient.NewDefaultBackend())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if f.newRunner == nil {
|
2026-04-13 16:01:28 +00:00
|
|
|
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)
|
2026-04-10 10:21:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return f
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
2026-04-13 16:01:28 +00:00
|
|
|
|
2026-05-11 09:16:37 +00:00
|
|
|
func profilePasswordLookupEnv(name string) (string, bool) {
|
|
|
|
|
trimmedName := strings.TrimSpace(name)
|
|
|
|
|
if strings.HasPrefix(trimmedName, "imap-password/") {
|
|
|
|
|
return os.LookupEnv(passwordEnv)
|
|
|
|
|
}
|
|
|
|
|
return os.LookupEnv(trimmedName)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:01:28 +00:00
|
|
|
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
|
|
|
|
|
}
|