175 lines
4.4 KiB
Go
175 lines
4.4 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
|
|
"sproutclaw-web/internal/models"
|
|
)
|
|
|
|
// mcp.json is shared with pi-mcp-adapter, which only reads the top-level
|
|
// "mcpServers", "imports" and "settings" keys. To disable a server we move its
|
|
// full definition into the adapter-ignored "mcpServersDisabled" object, so the
|
|
// adapter never starts it; enabling moves it back. All operations work on the
|
|
// raw JSON map so unrecognized keys (settings, imports, ...) are preserved.
|
|
|
|
// ReadMCPServers loads mcp.json and returns the list of servers with their tools.
|
|
// Servers under "mcpServers" are reported enabled; those parked under
|
|
// "mcpServersDisabled" are reported disabled.
|
|
func ReadMCPServers(mcpConfigPath, mcpCachePath string) []models.MCPServer {
|
|
raw := loadRawMCP(mcpConfigPath)
|
|
cache := loadMCPCache(mcpCachePath)
|
|
|
|
excludeSet := map[string]bool{}
|
|
for _, t := range toStringSlice(raw["excludeTools"]) {
|
|
excludeSet[t] = true
|
|
}
|
|
|
|
var servers []models.MCPServer
|
|
appendServer := func(name string, enabled bool) {
|
|
srv := models.MCPServer{Name: name, Configured: true, Enabled: enabled}
|
|
for _, t := range cache[name] {
|
|
toolEnabled := !(excludeSet[name+"/"+t] || excludeSet[t])
|
|
srv.Tools = append(srv.Tools, models.MCPTool{Server: name, Name: t, Enabled: toolEnabled})
|
|
srv.ToolCount++
|
|
if toolEnabled {
|
|
srv.EnabledToolCount++
|
|
}
|
|
}
|
|
servers = append(servers, srv)
|
|
}
|
|
|
|
for name := range asMap(raw["mcpServers"]) {
|
|
appendServer(name, true)
|
|
}
|
|
for name := range asMap(raw["mcpServersDisabled"]) {
|
|
appendServer(name, false)
|
|
}
|
|
return servers
|
|
}
|
|
|
|
// ToggleMCPServer enables/disables a server by moving its full definition between
|
|
// "mcpServers" (adapter reads it -> server can start) and "mcpServersDisabled"
|
|
// (adapter ignores it -> server never starts).
|
|
func ToggleMCPServer(mcpConfigPath, serverName string, enable bool) error {
|
|
raw := loadRawMCP(mcpConfigPath)
|
|
servers := asMap(raw["mcpServers"])
|
|
disabled := asMap(raw["mcpServersDisabled"])
|
|
|
|
if enable {
|
|
if def, ok := disabled[serverName]; ok {
|
|
servers[serverName] = def
|
|
delete(disabled, serverName)
|
|
}
|
|
} else {
|
|
if def, ok := servers[serverName]; ok {
|
|
disabled[serverName] = def
|
|
delete(servers, serverName)
|
|
}
|
|
}
|
|
|
|
raw["mcpServers"] = servers
|
|
setOrDeleteMap(raw, "mcpServersDisabled", disabled)
|
|
return saveRawMCP(mcpConfigPath, raw)
|
|
}
|
|
|
|
// ToggleMCPTool enables or disables a specific tool via the top-level
|
|
// "excludeTools" list (server/tool keys).
|
|
func ToggleMCPTool(mcpConfigPath, serverName, toolName string, enable bool) error {
|
|
raw := loadRawMCP(mcpConfigPath)
|
|
key := serverName + "/" + toolName
|
|
|
|
set := map[string]bool{}
|
|
for _, t := range toStringSlice(raw["excludeTools"]) {
|
|
set[t] = true
|
|
}
|
|
if enable {
|
|
delete(set, key)
|
|
} else {
|
|
set[key] = true
|
|
}
|
|
|
|
list := make([]any, 0, len(set))
|
|
for k := range set {
|
|
list = append(list, k)
|
|
}
|
|
if len(list) > 0 {
|
|
raw["excludeTools"] = list
|
|
} else {
|
|
delete(raw, "excludeTools")
|
|
}
|
|
return saveRawMCP(mcpConfigPath, raw)
|
|
}
|
|
|
|
func loadRawMCP(path string) map[string]any {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return map[string]any{}
|
|
}
|
|
var raw map[string]any
|
|
if err := json.Unmarshal(data, &raw); err != nil || raw == nil {
|
|
return map[string]any{}
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func saveRawMCP(path string, raw map[string]any) error {
|
|
data, err := json.MarshalIndent(raw, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, append(data, '\n'), 0o644)
|
|
}
|
|
|
|
func asMap(v any) map[string]any {
|
|
if m, ok := v.(map[string]any); ok {
|
|
return m
|
|
}
|
|
return map[string]any{}
|
|
}
|
|
|
|
func setOrDeleteMap(raw map[string]any, key string, m map[string]any) {
|
|
if len(m) > 0 {
|
|
raw[key] = m
|
|
} else {
|
|
delete(raw, key)
|
|
}
|
|
}
|
|
|
|
func toStringSlice(v any) []string {
|
|
arr, ok := v.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(arr))
|
|
for _, e := range arr {
|
|
if s, ok := e.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func loadMCPCache(path string) map[string][]string {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
// cache format: { "serverName": { "tools": [{ "name": "..." }] } }
|
|
var raw map[string]struct {
|
|
Tools []struct {
|
|
Name string `json:"name"`
|
|
} `json:"tools"`
|
|
}
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return nil
|
|
}
|
|
result := map[string][]string{}
|
|
for srv, info := range raw {
|
|
for _, t := range info.Tools {
|
|
result[srv] = append(result[srv], t.Name)
|
|
}
|
|
}
|
|
return result
|
|
}
|