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;
notificationSound: boolean;
workspaceDir: string;
marketplaceMcpRoot: string;
theme: string;
autoSeedContext: boolean;
workHours?: {
@ -161,6 +162,13 @@ interface Config {
<input type="text" class="form-input" [(ngModel)]="config.workspaceDir"
placeholder="Leave empty for default">
</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>
</div>
</div>
@ -308,6 +316,7 @@ export class SettingsComponent implements OnInit {
notificationsEnabled: true,
notificationSound: true,
workspaceDir: '',
marketplaceMcpRoot: '',
theme: 'dark',
autoSeedContext: true,
workHours: {

View file

@ -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)

View file

@ -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()

View file

@ -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)
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
})
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{}
}

View file

@ -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

View file

@ -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"

View file

@ -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
}

View file

@ -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