diff --git a/go.mod b/go.mod index ce1a67e..e7c08c1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module email-mcp go 1.25.0 require ( - forge.lclr.dev/AI/mcp-framework v1.10.0 + forge.lclr.dev/AI/mcp-framework v1.12.0 github.com/emersion/go-imap/v2 v2.0.0-beta.8 github.com/emersion/go-message v0.18.2 github.com/godbus/dbus/v5 v5.2.2 diff --git a/go.sum b/go.sum index 94ff5ae..3130662 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ forge.lclr.dev/AI/mcp-framework v1.9.0 h1:8i2CHQlQo/mRG1BE2UArHptAa/HC7AOhZBIqz8 forge.lclr.dev/AI/mcp-framework v1.9.0/go.mod h1:2xzmFEHGLQzT5PORq35j10pRhsOm0CDwivUZTHvxgh4= forge.lclr.dev/AI/mcp-framework v1.10.0 h1:RrTy7K/hSruaVS9Z/oaRpkLs2U5WGs4H3tox7PiErak= forge.lclr.dev/AI/mcp-framework v1.10.0/go.mod h1:2xzmFEHGLQzT5PORq35j10pRhsOm0CDwivUZTHvxgh4= +forge.lclr.dev/AI/mcp-framework v1.12.0 h1:pu1cfWcL62BF+f7DBe4IbkigHLcK6YOJ3vEBz1495AY= +forge.lclr.dev/AI/mcp-framework v1.12.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/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= diff --git a/internal/cli/app.go b/internal/cli/app.go index 38d535e..c92c564 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -137,6 +137,7 @@ func (a *App) runBootstrap(ctx context.Context, args []string) error { Setup: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { return a.runConfig(ctx, frameworkbootstrap.CommandSetup, inv.Args) }, + Login: frameworkbootstrap.BitwardenLoginHandler(metadata.BinaryName), MCP: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { return a.runMCP(ctx, inv.Args) }, diff --git a/internal/cli/app_test.go b/internal/cli/app_test.go index 1b7d380..73cd943 100644 --- a/internal/cli/app_test.go +++ b/internal/cli/app_test.go @@ -966,7 +966,7 @@ base_url = "https://gitea.lclr.dev" "[OK] profile: required profile values are resolved", "[OK] password: stored password is present", "[OK] connectivity: IMAP server is reachable", - "Summary: 6 ok, 0 warning(s), 0 failure(s), 6 total", + "Summary: 5 ok, 0 warning(s), 0 failure(s), 5 total", } { if !strings.Contains(text, needle) { t.Fatalf("output = %q, want substring %q", text, needle) @@ -996,16 +996,6 @@ func TestAppRunDoctorReturnsErrorWhenChecksFail(t *testing.T) { t.Fatalf("Save returned error: %v", err) } - manifestDir := t.TempDir() - if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte(` -[update] -driver = "gitea" -repository = "AI/email-mcp" -base_url = "https://gitea.lclr.dev" -`), 0o600); err != nil { - t.Fatalf("WriteFile returned error: %v", err) - } - output := &bytes.Buffer{} app := NewAppWithDependencies( nil, @@ -1014,7 +1004,7 @@ base_url = "https://gitea.lclr.dev" func() mcpserver.MailService { return &doctorMailServiceStub{} }, nil, nil, - func() (string, error) { return filepath.Join(manifestDir, "email-mcp"), nil }, + nil, nil, output, &bytes.Buffer{}, @@ -1057,16 +1047,6 @@ func TestAppRunDoctorAcceptsPasswordFromEnvironment(t *testing.T) { t.Fatalf("Save returned error: %v", err) } - manifestDir := t.TempDir() - if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte(` -[update] -driver = "gitea" -repository = "AI/email-mcp" -base_url = "https://gitea.lclr.dev" -`), 0o600); err != nil { - t.Fatalf("WriteFile returned error: %v", err) - } - output := &bytes.Buffer{} app := NewAppWithDependencies( nil, @@ -1075,7 +1055,7 @@ base_url = "https://gitea.lclr.dev" func() mcpserver.MailService { return &doctorMailServiceStub{} }, nil, nil, - func() (string, error) { return filepath.Join(manifestDir, "email-mcp"), nil }, + nil, nil, output, &bytes.Buffer{}, @@ -1090,73 +1070,6 @@ base_url = "https://gitea.lclr.dev" } } -func TestAppRunDoctorFailsWhenManifestUpdateConfigIsInvalid(t *testing.T) { - tempHome := t.TempDir() - t.Setenv("XDG_CONFIG_HOME", tempHome) - t.Setenv("HOME", tempHome) - t.Setenv(passwordEnv, "env-secret") - - store := frameworkconfig.NewStore[ProfileConfig](binaryName) - configPath, err := store.ConfigPath() - if err != nil { - t.Fatalf("ConfigPath returned error: %v", err) - } - if err := store.Save(configPath, frameworkconfig.FileConfig[ProfileConfig]{ - Version: frameworkconfig.CurrentVersion, - CurrentProfile: "work", - Profiles: map[string]ProfileConfig{ - "work": { - Host: "imap.example.com", - Username: "alice", - }, - }, - }); err != nil { - t.Fatalf("Save returned error: %v", err) - } - - manifestDir := t.TempDir() - if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte(` -[update] -driver = "gitea" -base_url = "https://gitea.lclr.dev" -`), 0o600); err != nil { - t.Fatalf("WriteFile returned error: %v", err) - } - - mail := &doctorMailServiceStub{ - listMailboxes: []imapclient.Mailbox{{Name: "INBOX"}}, - } - output := &bytes.Buffer{} - app := NewAppWithDependencies( - nil, - store, - func() (secretStore, error) { return &secretStoreStub{}, nil }, - func() mcpserver.MailService { return mail }, - nil, - nil, - func() (string, error) { return filepath.Join(manifestDir, "email-mcp"), nil }, - nil, - output, - &bytes.Buffer{}, - "dev", - ) - - err = app.Run([]string{"doctor"}) - if err == nil { - t.Fatal("expected doctor to fail with invalid manifest update config") - } - if !strings.Contains(err.Error(), "doctor checks failed") { - t.Fatalf("unexpected error: %v", err) - } - - text := output.String() - if !strings.Contains(text, "[FAIL] manifest: manifest validation failed") { - t.Fatalf("unexpected output: %q", text) - } - if !strings.Contains(text, "requires repository") { - t.Fatalf("unexpected output: %q", text) - } -} func TestAppRunReturnsClearErrorsWhenDependenciesMissing(t *testing.T) { app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, nil, &bytes.Buffer{}, "dev") diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index e3fef54..2e8b794 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -5,16 +5,11 @@ import ( "errors" "fmt" "os" - "path/filepath" - "runtime" - "strings" "time" "email-mcp/mcpgen" frameworkcli "forge.lclr.dev/AI/mcp-framework/cli" frameworkconfig "forge.lclr.dev/AI/mcp-framework/config" - 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 { @@ -32,14 +27,9 @@ func (a *App) runDoctor(ctx context.Context, args []string) error { return fmt.Errorf("mail service is not configured") } - metadata := a.runtimeMetadata() report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{ - ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)), - SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore), - ManifestDir: a.doctorManifestDir(), - ManifestValidator: func(file frameworkmanifest.File, _ string) []string { - return validateManifestUpdate(file, metadata.BinaryName) - }, + ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)), + SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore), ConnectivityCheck: a.doctorConnectivityCheck(profileFlag), ExtraChecks: []frameworkcli.DoctorCheck{ a.doctorRequiredProfileFieldsCheck(profileFlag), @@ -55,18 +45,6 @@ func (a *App) runDoctor(ctx context.Context, args []string) error { return nil } -func (a *App) doctorManifestDir() string { - if a.resolveExecutable == nil { - return "." - } - - executablePath, err := a.resolveExecutable() - if err != nil { - return "." - } - return filepath.Dir(executablePath) -} - func (a *App) doctorRequiredProfileFieldsCheck(profileFlag string) frameworkcli.DoctorCheck { var ( profileValues map[string]string @@ -213,25 +191,3 @@ func (a *App) resolveDoctorProfileName(profileFlag string) string { return a.resolveProfileName(profileFlag, cfg.CurrentProfile) } -func validateManifestUpdate(file frameworkmanifest.File, runtimeBinaryName string) []string { - source := file.Update.ReleaseSource() - issues := make([]string, 0, 2) - - if _, err := frameworkupdate.ResolveLatestReleaseURL("", source); err != nil { - issues = append(issues, err.Error()) - } - - binary := strings.TrimSpace(runtimeBinaryName) - if binary == "" { - binary = strings.TrimSpace(file.BinaryName) - } - if binary == "" { - binary = binaryName - } - - if _, err := frameworkupdate.AssetNameWithTemplate(binary, runtime.GOOS, runtime.GOARCH, source.AssetNameTemplate); err != nil { - issues = append(issues, err.Error()) - } - - return issues -}