mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 06:47:30 +08:00
217 lines
6.0 KiB
Go
217 lines
6.0 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
cfgpkg "github.com/YspCoder/clawgo/pkg/config"
|
|
"github.com/YspCoder/clawgo/pkg/tools"
|
|
)
|
|
|
|
func (s *Server) handleWebUITools(w http.ResponseWriter, r *http.Request) {
|
|
if !s.checkAuth(r) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
toolsList := []map[string]interface{}{}
|
|
if s.onToolsCatalog != nil {
|
|
if items, ok := s.onToolsCatalog().([]map[string]interface{}); ok && items != nil {
|
|
toolsList = items
|
|
}
|
|
}
|
|
mcpItems := make([]map[string]interface{}, 0)
|
|
for _, item := range toolsList {
|
|
if strings.TrimSpace(fmt.Sprint(item["source"])) == "mcp" {
|
|
mcpItems = append(mcpItems, item)
|
|
}
|
|
}
|
|
serverChecks := []map[string]interface{}{}
|
|
if strings.TrimSpace(s.configPath) != "" {
|
|
if cfg, err := cfgpkg.LoadConfig(s.configPath); err == nil {
|
|
serverChecks = buildMCPServerChecks(cfg)
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"tools": toolsList,
|
|
"mcp_tools": mcpItems,
|
|
"mcp_server_checks": serverChecks,
|
|
})
|
|
}
|
|
|
|
func buildMCPServerChecks(cfg *cfgpkg.Config) []map[string]interface{} {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
names := make([]string, 0, len(cfg.Tools.MCP.Servers))
|
|
for name := range cfg.Tools.MCP.Servers {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
items := make([]map[string]interface{}, 0, len(names))
|
|
for _, name := range names {
|
|
server := cfg.Tools.MCP.Servers[name]
|
|
transport := strings.ToLower(strings.TrimSpace(server.Transport))
|
|
if transport == "" {
|
|
transport = "stdio"
|
|
}
|
|
command := strings.TrimSpace(server.Command)
|
|
status := "missing_command"
|
|
message := "command is empty"
|
|
resolved := ""
|
|
missingCommand := false
|
|
if !server.Enabled {
|
|
status = "disabled"
|
|
message = "server is disabled"
|
|
} else if transport != "stdio" {
|
|
status = "not_applicable"
|
|
message = "command check not required for non-stdio transport"
|
|
} else if command != "" {
|
|
if filepath.IsAbs(command) {
|
|
if info, err := os.Stat(command); err == nil && !info.IsDir() {
|
|
status = "ok"
|
|
message = "command found"
|
|
resolved = command
|
|
} else {
|
|
status = "missing_command"
|
|
message = fmt.Sprintf("command not found: %s", command)
|
|
missingCommand = true
|
|
}
|
|
} else if path, err := exec.LookPath(command); err == nil {
|
|
status = "ok"
|
|
message = "command found"
|
|
resolved = path
|
|
} else {
|
|
status = "missing_command"
|
|
message = fmt.Sprintf("command not found in PATH: %s", command)
|
|
missingCommand = true
|
|
}
|
|
}
|
|
installSpec := inferMCPInstallSpec(server)
|
|
items = append(items, map[string]interface{}{
|
|
"name": name,
|
|
"enabled": server.Enabled,
|
|
"transport": transport,
|
|
"status": status,
|
|
"message": message,
|
|
"command": command,
|
|
"resolved": resolved,
|
|
"package": installSpec.Package,
|
|
"installer": installSpec.Installer,
|
|
"installable": missingCommand && installSpec.AutoInstallSupported,
|
|
"missing_command": missingCommand,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
type mcpInstallSpec struct {
|
|
Installer string
|
|
Package string
|
|
AutoInstallSupported bool
|
|
}
|
|
|
|
func inferMCPInstallSpec(server cfgpkg.MCPServerConfig) mcpInstallSpec {
|
|
if pkgName := strings.TrimSpace(server.Package); pkgName != "" {
|
|
return mcpInstallSpec{Installer: "npm", Package: pkgName, AutoInstallSupported: true}
|
|
}
|
|
command := strings.TrimSpace(server.Command)
|
|
args := make([]string, 0, len(server.Args))
|
|
for _, arg := range server.Args {
|
|
if v := strings.TrimSpace(arg); v != "" {
|
|
args = append(args, v)
|
|
}
|
|
}
|
|
base := filepath.Base(command)
|
|
switch base {
|
|
case "npx":
|
|
return mcpInstallSpec{Installer: "npm", Package: firstNonFlagArg(args), AutoInstallSupported: firstNonFlagArg(args) != ""}
|
|
case "uvx":
|
|
pkgName := firstNonFlagArg(args)
|
|
return mcpInstallSpec{Installer: "uv", Package: pkgName, AutoInstallSupported: pkgName != ""}
|
|
case "bunx":
|
|
pkgName := firstNonFlagArg(args)
|
|
return mcpInstallSpec{Installer: "bun", Package: pkgName, AutoInstallSupported: pkgName != ""}
|
|
case "python", "python3":
|
|
if len(args) >= 2 && args[0] == "-m" {
|
|
return mcpInstallSpec{Installer: "pip", Package: strings.TrimSpace(args[1]), AutoInstallSupported: false}
|
|
}
|
|
}
|
|
return mcpInstallSpec{}
|
|
}
|
|
|
|
func firstNonFlagArg(args []string) string {
|
|
for _, arg := range args {
|
|
item := strings.TrimSpace(arg)
|
|
if item == "" || strings.HasPrefix(item, "-") {
|
|
continue
|
|
}
|
|
return item
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) handleWebUIMCPInstall(w http.ResponseWriter, r *http.Request) {
|
|
if !s.checkAuth(r) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var body struct {
|
|
Package string `json:"package"`
|
|
Installer string `json:"installer"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
pkgName := strings.TrimSpace(body.Package)
|
|
if pkgName == "" {
|
|
http.Error(w, "package required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
out, binName, binPath, err := ensureMCPPackageInstalledWithInstaller(r.Context(), pkgName, body.Installer)
|
|
if err != nil {
|
|
msg := err.Error()
|
|
if strings.TrimSpace(out) != "" {
|
|
msg = strings.TrimSpace(out) + "\n" + msg
|
|
}
|
|
http.Error(w, strings.TrimSpace(msg), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"ok": true,
|
|
"package": pkgName,
|
|
"output": out,
|
|
"bin_name": binName,
|
|
"bin_path": binPath,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleWebUIToolAllowlistGroups(w http.ResponseWriter, r *http.Request) {
|
|
if !s.checkAuth(r) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"ok": true,
|
|
"groups": tools.ToolAllowlistGroups(),
|
|
})
|
|
}
|