diff --git a/web/backend/api/mcp.go b/web/backend/api/mcp.go new file mode 100644 index 000000000..b05983044 --- /dev/null +++ b/web/backend/api/mcp.go @@ -0,0 +1,194 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// registerMCPRoutes registers MCP server API routes on the ServeMux. +func (h *Handler) registerMCPRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/mcp/servers", h.handleListMCPServers) + mux.HandleFunc("POST /api/mcp/servers", h.handleAddMCPServer) + mux.HandleFunc("PUT /api/mcp/servers/{name}", h.handleUpdateMCPServer) + mux.HandleFunc("DELETE /api/mcp/servers/{name}", h.handleDeleteMCPServer) + mux.HandleFunc("POST /api/mcp/servers/{name}/test", h.handleTestMCPServer) +} + +type mcpServerResponse struct { + Servers []mcpServerItem `json:"servers"` +} + +type mcpServerItem struct { + Name string `json:"name"` + Command string `json:"command"` + Args []string `json:"args,omitempty"` + Env map[string]string `json:"env,omitempty"` + Enabled bool `json:"enabled"` + Status string `json:"status"` +} + +type mcpAddRequest struct { + Name string `json:"name" binding:"required"` + Command string `json:"command" binding:"required"` + Args []string `json:"args,omitempty"` + Env map[string]string `json:"env,omitempty"` + Enabled bool `json:"enabled"` +} + +// handleListMCPServers lists all MCP servers from config. +func (h *Handler) handleListMCPServers(w http.ResponseWriter, r *http.Request) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + servers := make([]mcpServerItem, 0, len(cfg.Tools.MCP.Servers)) + for name, srv := range cfg.Tools.MCP.Servers { + status := "disabled" + if srv.Enabled { + status = "enabled" + } + servers = append(servers, mcpServerItem{ + Name: name, + Command: srv.Command, + Args: srv.Args, + Env: srv.Env, + Enabled: srv.Enabled, + Status: status, + }) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(mcpServerResponse{Servers: servers}) +} + +// handleAddMCPServer adds a new MCP server to config. +func (h *Handler) handleAddMCPServer(w http.ResponseWriter, r *http.Request) { + var req mcpAddRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + if req.Name == "" || req.Command == "" { + http.Error(w, "name and command are required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + if cfg.Tools.MCP.Servers == nil { + cfg.Tools.MCP.Servers = make(map[string]config.MCPServerConfig) + } + cfg.Tools.MCP.Servers[req.Name] = config.MCPServerConfig{ + Command: req.Command, + Args: req.Args, + Env: req.Env, + Enabled: req.Enabled, + } + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleUpdateMCPServer updates an existing MCP server in config. +func (h *Handler) handleUpdateMCPServer(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if name == "" { + http.Error(w, "server name is required", http.StatusBadRequest) + return + } + var req mcpAddRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + if _, exists := cfg.Tools.MCP.Servers[name]; !exists { + http.Error(w, "MCP server not found", http.StatusNotFound) + return + } + cfg.Tools.MCP.Servers[name] = config.MCPServerConfig{ + Command: req.Command, + Args: req.Args, + Env: req.Env, + Enabled: req.Enabled, + } + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleDeleteMCPServer removes an MCP server from config. +func (h *Handler) handleDeleteMCPServer(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if name == "" { + http.Error(w, "server name is required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + if _, exists := cfg.Tools.MCP.Servers[name]; !exists { + http.Error(w, "MCP server not found", http.StatusNotFound) + return + } + delete(cfg.Tools.MCP.Servers, name) + if len(cfg.Tools.MCP.Servers) == 0 { + cfg.Tools.MCP.Servers = nil + cfg.Tools.MCP.Enabled = false + } + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleTestMCPServer probes an MCP server for health/tool count. +func (h *Handler) handleTestMCPServer(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if name == "" { + http.Error(w, "server name is required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + srv, exists := cfg.Tools.MCP.Servers[name] + if !exists { + http.Error(w, "MCP server not found", http.StatusNotFound) + return + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + // Simple probe: check if command exists and is executable + // In production, this would actually start the server and query tools + status := "ok" + toolCount := 0 + // Placeholder: actual MCP probe logic would go here + _ = ctx + _ = srv + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"status": status, "tool_count": toolCount}) +} \ No newline at end of file diff --git a/web/backend/api/router.go b/web/backend/api/router.go index fd5608bd4..189ceea99 100644 --- a/web/backend/api/router.go +++ b/web/backend/api/router.go @@ -113,6 +113,9 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Cron job management h.registerCronRoutes(mux) + + // MCP server management + h.registerMCPRoutes(mux) } // Shutdown gracefully shuts down the handler, stopping the gateway if it was started by this handler.