first commit

This commit is contained in:
2026-06-14 20:31:10 +08:00
parent c33b143176
commit 1ed3f576fa
51 changed files with 3362 additions and 810 deletions

View File

@@ -7,68 +7,80 @@ import (
"sproutclaw-web/internal/models"
)
type mcpConfig struct {
MCPServers map[string]any `json:"mcpServers"`
MCPServersDisabled []string `json:"mcpServersDisabled,omitempty"`
ExcludeTools []string `json:"excludeTools,omitempty"`
}
// 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 {
cfg := loadMCPConfig(mcpConfigPath)
raw := loadRawMCP(mcpConfigPath)
cache := loadMCPCache(mcpCachePath)
disabledSet := map[string]bool{}
for _, s := range cfg.MCPServersDisabled {
disabledSet[s] = true
}
excludeSet := map[string]bool{}
for _, t := range cfg.ExcludeTools {
for _, t := range toStringSlice(raw["excludeTools"]) {
excludeSet[t] = true
}
var servers []models.MCPServer
for name := range cfg.MCPServers {
srv := models.MCPServer{
Name: name,
Disabled: disabledSet[name],
}
// add tools from cache
if toolNames, ok := cache[name]; ok {
for _, t := range toolNames {
srv.Tools = append(srv.Tools, models.MCPTool{
Name: t,
Disabled: excludeSet[name+"/"+t] || excludeSet[t],
})
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 or disables a server.
// 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 {
cfg := loadMCPConfig(mcpConfigPath)
set := map[string]bool{}
for _, s := range cfg.MCPServersDisabled {
set[s] = true
}
raw := loadRawMCP(mcpConfigPath)
servers := asMap(raw["mcpServers"])
disabled := asMap(raw["mcpServersDisabled"])
if enable {
delete(set, serverName)
if def, ok := disabled[serverName]; ok {
servers[serverName] = def
delete(disabled, serverName)
}
} else {
set[serverName] = true
if def, ok := servers[serverName]; ok {
disabled[serverName] = def
delete(servers, serverName)
}
}
cfg.MCPServersDisabled = keys(set)
return saveMCPConfig(mcpConfigPath, cfg)
raw["mcpServers"] = servers
setOrDeleteMap(raw, "mcpServersDisabled", disabled)
return saveRawMCP(mcpConfigPath, raw)
}
// ToggleMCPTool enables or disables a specific tool on a server.
// 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 {
cfg := loadMCPConfig(mcpConfigPath)
raw := loadRawMCP(mcpConfigPath)
key := serverName + "/" + toolName
set := map[string]bool{}
for _, t := range cfg.ExcludeTools {
for _, t := range toStringSlice(raw["excludeTools"]) {
set[t] = true
}
if enable {
@@ -76,31 +88,68 @@ func ToggleMCPTool(mcpConfigPath, serverName, toolName string, enable bool) erro
} else {
set[key] = true
}
cfg.ExcludeTools = keys(set)
return saveMCPConfig(mcpConfigPath, cfg)
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 loadMCPConfig(path string) mcpConfig {
func loadRawMCP(path string) map[string]any {
data, err := os.ReadFile(path)
if err != nil {
return mcpConfig{MCPServers: map[string]any{}}
return map[string]any{}
}
var cfg mcpConfig
_ = json.Unmarshal(data, &cfg)
if cfg.MCPServers == nil {
cfg.MCPServers = map[string]any{}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil || raw == nil {
return map[string]any{}
}
return cfg
return raw
}
func saveMCPConfig(path string, cfg mcpConfig) error {
data, err := json.MarshalIndent(cfg, "", " ")
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 {
@@ -123,11 +172,3 @@ func loadMCPCache(path string) map[string][]string {
}
return result
}
func keys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}