Skip to content

Commit a64df3a

Browse files
authored
Merge pull request #23 from agentsdance/load
feat(agent): add generic MCP management API and UI
2 parents dd37f6d + 44b7a1c commit a64df3a

11 files changed

Lines changed: 662 additions & 204 deletions

File tree

internal/agent/agent.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ type Agent interface {
2626
InstallRemixIcon() error
2727
// RemoveRemixIcon removes Remix Icon MCP from the config
2828
RemoveRemixIcon() error
29+
// HasMCP checks if a specific MCP server is configured
30+
HasMCP(name string) (bool, error)
31+
// InstallMCP adds a specific MCP server to the config
32+
InstallMCP(name string, mcpConfig map[string]interface{}) error
33+
// RemoveMCP removes a specific MCP server from the config
34+
RemoveMCP(name string) error
35+
// ListMCPs returns configured MCP servers
36+
ListMCPs() (map[string]map[string]interface{}, error)
2937
// SupportsSkills returns true if the agent supports skills
3038
SupportsSkills() bool
3139
// HasSkill checks if a skill is installed

internal/agent/claude.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67

@@ -143,6 +144,56 @@ func (a *ClaudeAgent) RemoveRemixIcon() error {
143144
return config.WriteConfig(a.configPath, cfg)
144145
}
145146

147+
func (a *ClaudeAgent) HasMCP(name string) (bool, error) {
148+
cfg, err := config.ReadConfig(a.configPath)
149+
if err != nil {
150+
if os.IsNotExist(err) {
151+
return false, nil
152+
}
153+
return false, err
154+
}
155+
return config.HasMCP(cfg, name), nil
156+
}
157+
158+
func (a *ClaudeAgent) InstallMCP(name string, mcpConfig map[string]interface{}) error {
159+
if mcpConfig == nil {
160+
return fmt.Errorf("missing mcp config")
161+
}
162+
cfg, err := config.ReadConfig(a.configPath)
163+
if err != nil {
164+
if os.IsNotExist(err) {
165+
cfg = make(map[string]interface{})
166+
} else {
167+
return err
168+
}
169+
}
170+
config.AddMCP(cfg, name, cloneMCPConfig(mcpConfig))
171+
return config.WriteConfig(a.configPath, cfg)
172+
}
173+
174+
func (a *ClaudeAgent) RemoveMCP(name string) error {
175+
cfg, err := config.ReadConfig(a.configPath)
176+
if err != nil {
177+
if os.IsNotExist(err) {
178+
return nil
179+
}
180+
return err
181+
}
182+
config.RemoveMCP(cfg, name)
183+
return config.WriteConfig(a.configPath, cfg)
184+
}
185+
186+
func (a *ClaudeAgent) ListMCPs() (map[string]map[string]interface{}, error) {
187+
cfg, err := config.ReadConfig(a.configPath)
188+
if err != nil {
189+
if os.IsNotExist(err) {
190+
return map[string]map[string]interface{}{}, nil
191+
}
192+
return nil, err
193+
}
194+
return config.GetMCPServers(cfg), nil
195+
}
196+
146197
func (a *ClaudeAgent) SupportsSkills() bool {
147198
return true
148199
}

internal/agent/codex.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67

@@ -164,6 +165,69 @@ func (a *CodexAgent) RemoveRemixIcon() error {
164165
return config.WriteTOMLConfig(a.configPath, cfg)
165166
}
166167

168+
func (a *CodexAgent) HasMCP(name string) (bool, error) {
169+
cfg, err := config.ReadTOMLConfig(a.configPath)
170+
if err != nil {
171+
if os.IsNotExist(err) {
172+
return false, nil
173+
}
174+
return false, err
175+
}
176+
return hasCodexMCP(cfg, name), nil
177+
}
178+
179+
func (a *CodexAgent) InstallMCP(name string, mcpConfig map[string]interface{}) error {
180+
if mcpConfig == nil {
181+
return fmt.Errorf("missing mcp config")
182+
}
183+
cfg, err := config.ReadTOMLConfig(a.configPath)
184+
if err != nil {
185+
if os.IsNotExist(err) {
186+
cfg = make(map[string]interface{})
187+
} else {
188+
return err
189+
}
190+
}
191+
addCodexMCP(cfg, name, normalizeArgsToStrings(mcpConfig))
192+
return config.WriteTOMLConfig(a.configPath, cfg)
193+
}
194+
195+
func (a *CodexAgent) RemoveMCP(name string) error {
196+
cfg, err := config.ReadTOMLConfig(a.configPath)
197+
if err != nil {
198+
if os.IsNotExist(err) {
199+
return nil
200+
}
201+
return err
202+
}
203+
removeCodexMCP(cfg, name)
204+
return config.WriteTOMLConfig(a.configPath, cfg)
205+
}
206+
207+
func (a *CodexAgent) ListMCPs() (map[string]map[string]interface{}, error) {
208+
cfg, err := config.ReadTOMLConfig(a.configPath)
209+
if err != nil {
210+
if os.IsNotExist(err) {
211+
return map[string]map[string]interface{}{}, nil
212+
}
213+
return nil, err
214+
}
215+
result := map[string]map[string]interface{}{}
216+
mcpServers, ok := cfg[codexMCPKey].(map[string]interface{})
217+
if !ok {
218+
return result, nil
219+
}
220+
for name, raw := range mcpServers {
221+
if raw == nil {
222+
continue
223+
}
224+
if serverCfg, ok := raw.(map[string]interface{}); ok {
225+
result[name] = serverCfg
226+
}
227+
}
228+
return result, nil
229+
}
230+
167231
func (a *CodexAgent) SupportsSkills() bool {
168232
return true
169233
}

internal/agent/cursor.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ func (a *CursorAgent) RemoveRemixIcon() error {
7171
return a.RemoveMCP("remix-icon")
7272
}
7373

74+
func (a *CursorAgent) ListMCPs() (map[string]map[string]interface{}, error) {
75+
cfg, err := config.ReadConfig(a.configPath)
76+
if err != nil {
77+
if os.IsNotExist(err) {
78+
return map[string]map[string]interface{}{}, nil
79+
}
80+
return nil, err
81+
}
82+
return config.GetMCPServers(cfg), nil
83+
}
84+
7485
// HasMCP checks if a specific MCP server is configured
7586
func (a *CursorAgent) HasMCP(name string) (bool, error) {
7687
cfg, err := config.ReadConfig(a.configPath)

internal/agent/gemini.go

Lines changed: 159 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package agent
22

33
import (
4+
"encoding/json"
5+
"fmt"
46
"os"
57
"path/filepath"
68

@@ -34,14 +36,7 @@ func (a *GeminiAgent) Exists() bool {
3436
}
3537

3638
func (a *GeminiAgent) HasPlaywright() (bool, error) {
37-
cfg, err := config.ReadConfig(a.configPath)
38-
if err != nil {
39-
if os.IsNotExist(err) {
40-
return false, nil
41-
}
42-
return false, err
43-
}
44-
return config.HasPlaywrightMCP(cfg), nil
39+
return a.HasMCP("playwright")
4540
}
4641

4742
func (a *GeminiAgent) InstallPlaywright() error {
@@ -74,14 +69,7 @@ func (a *GeminiAgent) RemovePlaywright() error {
7469
}
7570

7671
func (a *GeminiAgent) HasContext7() (bool, error) {
77-
cfg, err := config.ReadConfig(a.configPath)
78-
if err != nil {
79-
if os.IsNotExist(err) {
80-
return false, nil
81-
}
82-
return false, err
83-
}
84-
return config.HasContext7MCP(cfg), nil
72+
return a.HasMCP("context7")
8573
}
8674

8775
func (a *GeminiAgent) InstallContext7() error {
@@ -113,14 +101,7 @@ func (a *GeminiAgent) RemoveContext7() error {
113101
}
114102

115103
func (a *GeminiAgent) HasRemixIcon() (bool, error) {
116-
cfg, err := config.ReadConfig(a.configPath)
117-
if err != nil {
118-
if os.IsNotExist(err) {
119-
return false, nil
120-
}
121-
return false, err
122-
}
123-
return config.HasRemixIconMCP(cfg), nil
104+
return a.HasMCP("remix-icon")
124105
}
125106

126107
func (a *GeminiAgent) InstallRemixIcon() error {
@@ -151,6 +132,160 @@ func (a *GeminiAgent) RemoveRemixIcon() error {
151132
return config.WriteConfig(a.configPath, cfg)
152133
}
153134

135+
func (a *GeminiAgent) HasMCP(name string) (bool, error) {
136+
cfg, err := config.ReadConfig(a.configPath)
137+
if err != nil {
138+
if os.IsNotExist(err) {
139+
cfg = map[string]interface{}{}
140+
} else {
141+
return false, err
142+
}
143+
}
144+
if config.HasMCP(cfg, name) {
145+
return true, nil
146+
}
147+
extensions, err := a.listExtensionMCPs()
148+
if err != nil {
149+
return false, err
150+
}
151+
_, ok := extensions[name]
152+
return ok, nil
153+
}
154+
155+
func (a *GeminiAgent) InstallMCP(name string, mcpConfig map[string]interface{}) error {
156+
if mcpConfig == nil {
157+
return fmt.Errorf("missing mcp config")
158+
}
159+
cfg, err := config.ReadConfig(a.configPath)
160+
if err != nil {
161+
if os.IsNotExist(err) {
162+
cfg = make(map[string]interface{})
163+
if err := os.MkdirAll(filepath.Dir(a.configPath), 0755); err != nil {
164+
return err
165+
}
166+
} else {
167+
return err
168+
}
169+
}
170+
config.AddMCP(cfg, name, cloneMCPConfig(mcpConfig))
171+
return config.WriteConfig(a.configPath, cfg)
172+
}
173+
174+
func (a *GeminiAgent) RemoveMCP(name string) error {
175+
cfg, err := config.ReadConfig(a.configPath)
176+
if err != nil {
177+
if os.IsNotExist(err) {
178+
cfg = map[string]interface{}{}
179+
} else {
180+
return err
181+
}
182+
}
183+
if !config.HasMCP(cfg, name) {
184+
extensions, extErr := a.listExtensionMCPs()
185+
if extErr == nil {
186+
if _, ok := extensions[name]; ok {
187+
return fmt.Errorf("mcp %s is managed by a Gemini extension", name)
188+
}
189+
}
190+
return nil
191+
}
192+
config.RemoveMCP(cfg, name)
193+
return config.WriteConfig(a.configPath, cfg)
194+
}
195+
196+
func (a *GeminiAgent) ListMCPs() (map[string]map[string]interface{}, error) {
197+
cfg, err := config.ReadConfig(a.configPath)
198+
if err != nil {
199+
if os.IsNotExist(err) {
200+
cfg = map[string]interface{}{}
201+
} else {
202+
return nil, err
203+
}
204+
}
205+
result := config.GetMCPServers(cfg)
206+
extensions, err := a.listExtensionMCPs()
207+
if err != nil {
208+
return result, nil
209+
}
210+
for name, cfg := range extensions {
211+
if _, exists := result[name]; exists {
212+
continue
213+
}
214+
result[name] = cfg
215+
}
216+
return result, nil
217+
}
218+
219+
func (a *GeminiAgent) listExtensionMCPs() (map[string]map[string]interface{}, error) {
220+
extensionsDir := filepath.Join(filepath.Dir(a.configPath), "extensions")
221+
entries, err := os.ReadDir(extensionsDir)
222+
if err != nil {
223+
if os.IsNotExist(err) {
224+
return map[string]map[string]interface{}{}, nil
225+
}
226+
return nil, err
227+
}
228+
229+
enabled, _ := readGeminiExtensionEnablement(filepath.Join(extensionsDir, "extension-enablement.json"))
230+
result := make(map[string]map[string]interface{})
231+
for _, entry := range entries {
232+
if !entry.IsDir() {
233+
continue
234+
}
235+
name := entry.Name()
236+
if enabled != nil {
237+
if _, ok := enabled[name]; !ok {
238+
continue
239+
}
240+
}
241+
configPath := filepath.Join(extensionsDir, name, "gemini-extension.json")
242+
extCfg, err := readGeminiExtensionConfig(configPath)
243+
if err != nil {
244+
continue
245+
}
246+
for serverName, serverCfg := range extCfg {
247+
if serverCfg == nil {
248+
continue
249+
}
250+
result[serverName] = serverCfg
251+
}
252+
}
253+
return result, nil
254+
}
255+
256+
func readGeminiExtensionEnablement(path string) (map[string]struct{}, error) {
257+
data, err := os.ReadFile(path)
258+
if err != nil {
259+
return nil, err
260+
}
261+
var raw map[string]interface{}
262+
if err := json.Unmarshal(data, &raw); err != nil {
263+
return nil, err
264+
}
265+
result := make(map[string]struct{}, len(raw))
266+
for key := range raw {
267+
result[key] = struct{}{}
268+
}
269+
return result, nil
270+
}
271+
272+
func readGeminiExtensionConfig(path string) (map[string]map[string]interface{}, error) {
273+
data, err := os.ReadFile(path)
274+
if err != nil {
275+
return nil, err
276+
}
277+
var payload struct {
278+
MCPServers map[string]map[string]interface{} `json:"mcpServers"`
279+
}
280+
if err := json.Unmarshal(data, &payload); err != nil {
281+
return nil, err
282+
}
283+
if payload.MCPServers == nil {
284+
return map[string]map[string]interface{}{}, nil
285+
}
286+
return payload.MCPServers, nil
287+
}
288+
154289
func (a *GeminiAgent) SupportsSkills() bool {
155290
return false
156291
}

0 commit comments

Comments
 (0)