From 00c011bd3982fb7ef31ebcc2e040964d03308c0d Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 5 Feb 2026 22:07:24 +0000 Subject: [PATCH] feat(bugseti): add marketplace MCP root - add MarketplaceMCPRoot config and UI setting\n- prefer config root before env or auto-discovery\n- thread config root into ethics guard usage --- .../src/app/settings/settings.component.ts | 9 +++++ cmd/bugseti/main.go | 2 +- internal/bugseti/config.go | 10 ++++++ internal/bugseti/ethics_guard.go | 33 +++++++++++++------ internal/bugseti/mcp_marketplace.go | 11 +++++-- internal/bugseti/notify.go | 13 ++++++-- internal/bugseti/seeder.go | 4 +-- internal/bugseti/submit.go | 2 +- 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/cmd/bugseti/frontend/src/app/settings/settings.component.ts b/cmd/bugseti/frontend/src/app/settings/settings.component.ts index f144af15..7447d3fa 100644 --- a/cmd/bugseti/frontend/src/app/settings/settings.component.ts +++ b/cmd/bugseti/frontend/src/app/settings/settings.component.ts @@ -9,6 +9,7 @@ interface Config { notificationsEnabled: boolean; notificationSound: boolean; workspaceDir: string; + marketplaceMcpRoot: string; theme: string; autoSeedContext: boolean; workHours?: { @@ -161,6 +162,13 @@ interface Config { + +
+ + +

Override the marketplace MCP root. Leave empty to auto-detect.

+
@@ -308,6 +316,7 @@ export class SettingsComponent implements OnInit { notificationsEnabled: true, notificationSound: true, workspaceDir: '', + marketplaceMcpRoot: '', theme: 'dark', autoSeedContext: true, workHours: { diff --git a/cmd/bugseti/main.go b/cmd/bugseti/main.go index 4e23dbab..a7c2493e 100644 --- a/cmd/bugseti/main.go +++ b/cmd/bugseti/main.go @@ -37,7 +37,7 @@ func main() { } // Initialize core services - notifyService := bugseti.NewNotifyService() + notifyService := bugseti.NewNotifyService(configService) statsService := bugseti.NewStatsService(configService) fetcherService := bugseti.NewFetcherService(configService, notifyService) queueService := bugseti.NewQueueService(configService) diff --git a/internal/bugseti/config.go b/internal/bugseti/config.go index f5c9b301..3a8af7b5 100644 --- a/internal/bugseti/config.go +++ b/internal/bugseti/config.go @@ -37,6 +37,8 @@ type Config struct { // Workspace WorkspaceDir string `json:"workspaceDir,omitempty"` DataDir string `json:"dataDir,omitempty"` + // Marketplace MCP + MarketplaceMCPRoot string `json:"marketplaceMcpRoot,omitempty"` // Onboarding Onboarded bool `json:"onboarded"` @@ -96,6 +98,7 @@ func NewConfigService() *ConfigService { MaxConcurrentIssues: 1, AutoSeedContext: true, DataDir: bugsetiDir, + MarketplaceMCPRoot: "", UpdateChannel: "stable", AutoUpdate: false, UpdateCheckInterval: 6, // Check every 6 hours @@ -181,6 +184,13 @@ func (c *ConfigService) GetConfig() Config { return *c.config } +// GetMarketplaceMCPRoot returns the configured marketplace MCP root path. +func (c *ConfigService) GetMarketplaceMCPRoot() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.config.MarketplaceMCPRoot +} + // SetConfig updates the configuration and saves it. func (c *ConfigService) SetConfig(config Config) error { c.mu.Lock() diff --git a/internal/bugseti/ethics_guard.go b/internal/bugseti/ethics_guard.go index 3a0a3a44..8a267a7a 100644 --- a/internal/bugseti/ethics_guard.go +++ b/internal/bugseti/ethics_guard.go @@ -26,19 +26,32 @@ type EthicsGuard struct { } var ( - ethicsGuardOnce sync.Once + ethicsGuardMu sync.Mutex ethicsGuard *EthicsGuard + ethicsGuardRoot string ) func getEthicsGuard(ctx context.Context) *EthicsGuard { - ethicsGuardOnce.Do(func() { - guard := loadEthicsGuard(ctx) - if guard == nil { - guard = &EthicsGuard{} - } - ethicsGuard = guard - }) + return getEthicsGuardWithRoot(ctx, "") +} +func getEthicsGuardWithRoot(ctx context.Context, rootHint string) *EthicsGuard { + rootHint = strings.TrimSpace(rootHint) + + ethicsGuardMu.Lock() + defer ethicsGuardMu.Unlock() + + if ethicsGuard != nil && ethicsGuardRoot == rootHint { + return ethicsGuard + } + + guard := loadEthicsGuard(ctx, rootHint) + if guard == nil { + guard = &EthicsGuard{} + } + + ethicsGuard = guard + ethicsGuardRoot = rootHint if ethicsGuard == nil { return &EthicsGuard{} } @@ -67,14 +80,14 @@ func guardFromMarketplace(ctx context.Context, client marketplaceClient) *Ethics } } -func loadEthicsGuard(ctx context.Context) *EthicsGuard { +func loadEthicsGuard(ctx context.Context, rootHint string) *EthicsGuard { if ctx == nil { ctx = context.Background() } ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() - client, err := newMarketplaceClient(ctx) + client, err := newMarketplaceClient(ctx, rootHint) if err != nil { return &EthicsGuard{} } diff --git a/internal/bugseti/mcp_marketplace.go b/internal/bugseti/mcp_marketplace.go index 7364f1d1..9f379dfd 100644 --- a/internal/bugseti/mcp_marketplace.go +++ b/internal/bugseti/mcp_marketplace.go @@ -60,12 +60,12 @@ type mcpMarketplaceClient struct { client *client.Client } -func newMarketplaceClient(ctx context.Context) (marketplaceClient, error) { +func newMarketplaceClient(ctx context.Context, rootHint string) (marketplaceClient, error) { if ctx == nil { ctx = context.Background() } - command, args, err := resolveMarketplaceCommand() + command, args, err := resolveMarketplaceCommand(rootHint) if err != nil { return nil, err } @@ -180,12 +180,17 @@ func toolResultMessage(result *mcp.CallToolResult) string { return "unknown error" } -func resolveMarketplaceCommand() (string, []string, error) { +func resolveMarketplaceCommand(rootHint string) (string, []string, error) { if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" { args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS")) return command, args, nil } + if root := strings.TrimSpace(rootHint); root != "" { + path := filepath.Join(root, "mcp") + return "go", []string{"run", path}, nil + } + if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" { path := filepath.Join(root, "mcp") return "go", []string{"run", path}, nil diff --git a/internal/bugseti/notify.go b/internal/bugseti/notify.go index 097704d4..c467c1b2 100644 --- a/internal/bugseti/notify.go +++ b/internal/bugseti/notify.go @@ -14,13 +14,15 @@ import ( type NotifyService struct { enabled bool sound bool + config *ConfigService } // NewNotifyService creates a new NotifyService. -func NewNotifyService() *NotifyService { +func NewNotifyService(config *ConfigService) *NotifyService { return &NotifyService{ enabled: true, sound: true, + config: config, } } @@ -45,7 +47,7 @@ func (n *NotifyService) Notify(title, message string) error { return nil } - guard := getEthicsGuard(context.Background()) + guard := getEthicsGuardWithRoot(context.Background(), n.getMarketplaceRoot()) safeTitle := guard.SanitizeNotification(title) safeMessage := guard.SanitizeNotification(message) @@ -72,6 +74,13 @@ func (n *NotifyService) Notify(title, message string) error { return err } +func (n *NotifyService) getMarketplaceRoot() string { + if n == nil || n.config == nil { + return "" + } + return n.config.GetMarketplaceMCPRoot() +} + // NotifyIssue sends a notification about a new issue. func (n *NotifyService) NotifyIssue(issue *Issue) error { title := "New Issue Available" diff --git a/internal/bugseti/seeder.go b/internal/bugseti/seeder.go index 579ad53b..52f9a8b7 100644 --- a/internal/bugseti/seeder.go +++ b/internal/bugseti/seeder.go @@ -48,7 +48,7 @@ func (s *SeederService) SeedIssue(issue *Issue) (*IssueContext, error) { if err != nil { log.Printf("Seed skill failed, using fallback: %v", err) // Fallback to basic context preparation - guard := getEthicsGuard(context.Background()) + guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot()) ctx = s.prepareBasicContext(issue, guard) } @@ -95,7 +95,7 @@ func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContex mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second) defer mcpCancel() - marketplace, err := newMarketplaceClient(mcpCtx) + marketplace, err := newMarketplaceClient(mcpCtx, s.config.GetMarketplaceMCPRoot()) if err != nil { return nil, err } diff --git a/internal/bugseti/submit.go b/internal/bugseti/submit.go index a61c8282..fb15234e 100644 --- a/internal/bugseti/submit.go +++ b/internal/bugseti/submit.go @@ -67,7 +67,7 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) { return nil, fmt.Errorf("work directory not specified") } - guard := getEthicsGuard(context.Background()) + guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot()) issueTitle := guard.SanitizeTitle(issue.Title) // Step 1: Ensure we have a fork