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
This commit is contained in:
Snider 2026-02-05 22:07:24 +00:00
parent 23d9c4da19
commit 00c011bd39
8 changed files with 65 additions and 19 deletions

View file

@ -9,6 +9,7 @@ interface Config {
notificationsEnabled: boolean; notificationsEnabled: boolean;
notificationSound: boolean; notificationSound: boolean;
workspaceDir: string; workspaceDir: string;
marketplaceMcpRoot: string;
theme: string; theme: string;
autoSeedContext: boolean; autoSeedContext: boolean;
workHours?: { workHours?: {
@ -161,6 +162,13 @@ interface Config {
<input type="text" class="form-input" [(ngModel)]="config.workspaceDir" <input type="text" class="form-input" [(ngModel)]="config.workspaceDir"
placeholder="Leave empty for default"> placeholder="Leave empty for default">
</div> </div>
<div class="form-group">
<label class="form-label">Marketplace MCP Root</label>
<input type="text" class="form-input" [(ngModel)]="config.marketplaceMcpRoot"
placeholder="Path to core-agent (optional)">
<p class="section-description">Override the marketplace MCP root. Leave empty to auto-detect.</p>
</div>
</section> </section>
</div> </div>
</div> </div>
@ -308,6 +316,7 @@ export class SettingsComponent implements OnInit {
notificationsEnabled: true, notificationsEnabled: true,
notificationSound: true, notificationSound: true,
workspaceDir: '', workspaceDir: '',
marketplaceMcpRoot: '',
theme: 'dark', theme: 'dark',
autoSeedContext: true, autoSeedContext: true,
workHours: { workHours: {

View file

@ -37,7 +37,7 @@ func main() {
} }
// Initialize core services // Initialize core services
notifyService := bugseti.NewNotifyService() notifyService := bugseti.NewNotifyService(configService)
statsService := bugseti.NewStatsService(configService) statsService := bugseti.NewStatsService(configService)
fetcherService := bugseti.NewFetcherService(configService, notifyService) fetcherService := bugseti.NewFetcherService(configService, notifyService)
queueService := bugseti.NewQueueService(configService) queueService := bugseti.NewQueueService(configService)

View file

@ -37,6 +37,8 @@ type Config struct {
// Workspace // Workspace
WorkspaceDir string `json:"workspaceDir,omitempty"` WorkspaceDir string `json:"workspaceDir,omitempty"`
DataDir string `json:"dataDir,omitempty"` DataDir string `json:"dataDir,omitempty"`
// Marketplace MCP
MarketplaceMCPRoot string `json:"marketplaceMcpRoot,omitempty"`
// Onboarding // Onboarding
Onboarded bool `json:"onboarded"` Onboarded bool `json:"onboarded"`
@ -96,6 +98,7 @@ func NewConfigService() *ConfigService {
MaxConcurrentIssues: 1, MaxConcurrentIssues: 1,
AutoSeedContext: true, AutoSeedContext: true,
DataDir: bugsetiDir, DataDir: bugsetiDir,
MarketplaceMCPRoot: "",
UpdateChannel: "stable", UpdateChannel: "stable",
AutoUpdate: false, AutoUpdate: false,
UpdateCheckInterval: 6, // Check every 6 hours UpdateCheckInterval: 6, // Check every 6 hours
@ -181,6 +184,13 @@ func (c *ConfigService) GetConfig() Config {
return *c.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. // SetConfig updates the configuration and saves it.
func (c *ConfigService) SetConfig(config Config) error { func (c *ConfigService) SetConfig(config Config) error {
c.mu.Lock() c.mu.Lock()

View file

@ -26,19 +26,32 @@ type EthicsGuard struct {
} }
var ( var (
ethicsGuardOnce sync.Once ethicsGuardMu sync.Mutex
ethicsGuard *EthicsGuard ethicsGuard *EthicsGuard
ethicsGuardRoot string
) )
func getEthicsGuard(ctx context.Context) *EthicsGuard { func getEthicsGuard(ctx context.Context) *EthicsGuard {
ethicsGuardOnce.Do(func() { return getEthicsGuardWithRoot(ctx, "")
guard := loadEthicsGuard(ctx) }
if guard == nil {
guard = &EthicsGuard{}
}
ethicsGuard = guard
})
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 { if ethicsGuard == nil {
return &EthicsGuard{} 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 { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
ctx, cancel := context.WithTimeout(ctx, 2*time.Second) ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel() defer cancel()
client, err := newMarketplaceClient(ctx) client, err := newMarketplaceClient(ctx, rootHint)
if err != nil { if err != nil {
return &EthicsGuard{} return &EthicsGuard{}
} }

View file

@ -60,12 +60,12 @@ type mcpMarketplaceClient struct {
client *client.Client client *client.Client
} }
func newMarketplaceClient(ctx context.Context) (marketplaceClient, error) { func newMarketplaceClient(ctx context.Context, rootHint string) (marketplaceClient, error) {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
command, args, err := resolveMarketplaceCommand() command, args, err := resolveMarketplaceCommand(rootHint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -180,12 +180,17 @@ func toolResultMessage(result *mcp.CallToolResult) string {
return "unknown error" 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 != "" { if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" {
args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS")) args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS"))
return command, args, nil 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 != "" { if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" {
path := filepath.Join(root, "mcp") path := filepath.Join(root, "mcp")
return "go", []string{"run", path}, nil return "go", []string{"run", path}, nil

View file

@ -14,13 +14,15 @@ import (
type NotifyService struct { type NotifyService struct {
enabled bool enabled bool
sound bool sound bool
config *ConfigService
} }
// NewNotifyService creates a new NotifyService. // NewNotifyService creates a new NotifyService.
func NewNotifyService() *NotifyService { func NewNotifyService(config *ConfigService) *NotifyService {
return &NotifyService{ return &NotifyService{
enabled: true, enabled: true,
sound: true, sound: true,
config: config,
} }
} }
@ -45,7 +47,7 @@ func (n *NotifyService) Notify(title, message string) error {
return nil return nil
} }
guard := getEthicsGuard(context.Background()) guard := getEthicsGuardWithRoot(context.Background(), n.getMarketplaceRoot())
safeTitle := guard.SanitizeNotification(title) safeTitle := guard.SanitizeNotification(title)
safeMessage := guard.SanitizeNotification(message) safeMessage := guard.SanitizeNotification(message)
@ -72,6 +74,13 @@ func (n *NotifyService) Notify(title, message string) error {
return err 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. // NotifyIssue sends a notification about a new issue.
func (n *NotifyService) NotifyIssue(issue *Issue) error { func (n *NotifyService) NotifyIssue(issue *Issue) error {
title := "New Issue Available" title := "New Issue Available"

View file

@ -48,7 +48,7 @@ func (s *SeederService) SeedIssue(issue *Issue) (*IssueContext, error) {
if err != nil { if err != nil {
log.Printf("Seed skill failed, using fallback: %v", err) log.Printf("Seed skill failed, using fallback: %v", err)
// Fallback to basic context preparation // Fallback to basic context preparation
guard := getEthicsGuard(context.Background()) guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
ctx = s.prepareBasicContext(issue, guard) 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) mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second)
defer mcpCancel() defer mcpCancel()
marketplace, err := newMarketplaceClient(mcpCtx) marketplace, err := newMarketplaceClient(mcpCtx, s.config.GetMarketplaceMCPRoot())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -67,7 +67,7 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) {
return nil, fmt.Errorf("work directory not specified") return nil, fmt.Errorf("work directory not specified")
} }
guard := getEthicsGuard(context.Background()) guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
issueTitle := guard.SanitizeTitle(issue.Title) issueTitle := guard.SanitizeTitle(issue.Title)
// Step 1: Ensure we have a fork // Step 1: Ensure we have a fork