Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ type ConfigMcp struct {
// true -> global_mcp_config_file (user-global)
Global bool `yaml:"global,omitempty" json:"global,omitempty" jsonschema:"description=When true the MCP is configured in the agent's global config file instead of the project-scoped one"`
Merge *bool `yaml:"merge,omitempty" json:"merge,omitempty" jsonschema:"description=Merge server entry into existing file rather than overwriting it (default: true when omitted)"`
// Prune opts this entry's target file in for `gaal sync --prune`.
// Default false — without this gate, --prune was destructive even
// against agent config files holding entries the user added by hand
// outside of gaal. See #142.
Prune bool `yaml:"prune,omitempty" json:"prune,omitempty" jsonschema:"description=Allow gaal sync --prune to remove orphan entries from this MCP target file (default: false)"`
Inline *ConfigMcpItem `yaml:"inline,omitempty" json:"inline,omitempty" jsonschema:"description=Inline server definition (mutually exclusive with source)" validate:"omitempty"`
}

Expand Down
23 changes: 18 additions & 5 deletions internal/mcp/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,23 +305,36 @@ func mergeIntoTarget(target, name string, entry serverEntry) error {
return nil
}

// Prune removes mcpServers entries from each managed target file whose names
// are no longer declared in the config for that target. Entries added manually
// outside of gaal are also removed — callers should only use --prune on files
// they intend to manage exclusively with gaal.
// Prune removes mcpServers entries from each managed target file whose
// names are no longer declared in the config for that target.
//
// Per #142, this is gated by an opt-in flag: a target is only pruned
// when at least one ConfigMcp entry pointing at it sets `prune: true`.
// Without that gate `--prune` could silently wipe MCP entries the user
// had added by hand to a shared file (e.g. claude-code's ~/.claude.json,
// codex's ~/.codex/config.toml).
func (m *Manager) Prune(ctx context.Context) error {
slog.DebugContext(ctx, "pruning orphan mcp entries")

// Build expected name set per target path.
// Build expected name set + per-target prune-opt-in.
keepPerTarget := make(map[string]map[string]struct{})
pruneTarget := make(map[string]bool)
for _, mc := range m.resolvedMCPs() {
if keepPerTarget[mc.Target] == nil {
keepPerTarget[mc.Target] = make(map[string]struct{})
}
keepPerTarget[mc.Target][mc.Name] = struct{}{}
if mc.Prune {
pruneTarget[mc.Target] = true
}
}

for target, keep := range keepPerTarget {
if !pruneTarget[target] {
slog.Debug("mcp prune: target not opted in (set prune: true on at least one entry)",
"target", target)
continue
}
codec := codecFor(target)
servers, err := codec.ReadServers(target)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions internal/mcp/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,9 +718,11 @@ func TestMCPPrune_RemovesOrphanEntry(t *testing.T) {
initial := `{"mcpServers":{"kept":{"command":"node"},"orphan":{"command":"python"}}}`
os.WriteFile(target, []byte(initial), 0o644)

// Config only declares "kept".
// Config only declares "kept" — and opts the target in to pruning
// per #142 (without prune: true on at least one entry, the target
// is not eligible for orphan removal even when --prune is passed).
cfg := []config.ConfigMcp{
{Name: "kept", Target: target, Inline: &config.ConfigMcpItem{Command: "node"}},
{Name: "kept", Target: target, Prune: true, Inline: &config.ConfigMcpItem{Command: "node"}},
}
m := NewManager(cfg, "", "")
if err := m.Prune(context.Background()); err != nil {
Expand Down
Loading