mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-27 21:47:30 +08:00
feat: expand mcp transports and skill execution
This commit is contained in:
@@ -617,13 +617,132 @@ func (s *Server) handleWebUITools(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tools": toolsList,
|
||||
"mcp_tools": mcpItems,
|
||||
"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)
|
||||
@@ -634,7 +753,8 @@ func (s *Server) handleWebUIMCPInstall(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Package string `json:"package"`
|
||||
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)
|
||||
@@ -645,7 +765,7 @@ func (s *Server) handleWebUIMCPInstall(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "package required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
out, binName, binPath, err := ensureMCPPackageInstalled(r.Context(), pkgName)
|
||||
out, binName, binPath, err := ensureMCPPackageInstalledWithInstaller(r.Context(), pkgName, body.Installer)
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
if strings.TrimSpace(out) != "" {
|
||||
@@ -1766,38 +1886,84 @@ func ensureClawHubReady(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
func ensureMCPPackageInstalled(ctx context.Context, pkgName string) (output string, binName string, binPath string, err error) {
|
||||
return ensureMCPPackageInstalledWithInstaller(ctx, pkgName, "npm")
|
||||
}
|
||||
|
||||
func ensureMCPPackageInstalledWithInstaller(ctx context.Context, pkgName, installer string) (output string, binName string, binPath string, err error) {
|
||||
pkgName = strings.TrimSpace(pkgName)
|
||||
if pkgName == "" {
|
||||
return "", "", "", fmt.Errorf("package empty")
|
||||
}
|
||||
installer = strings.ToLower(strings.TrimSpace(installer))
|
||||
if installer == "" {
|
||||
installer = "npm"
|
||||
}
|
||||
outs := make([]string, 0, 4)
|
||||
nodeOut, err := ensureNodeRuntime(ctx)
|
||||
if nodeOut != "" {
|
||||
outs = append(outs, nodeOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
installOut, err := runInstallCommand(ctx, "npm i -g "+shellEscapeArg(pkgName))
|
||||
if installOut != "" {
|
||||
outs = append(outs, installOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
binName, err = resolveNpmPackageBin(ctx, pkgName)
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
switch installer {
|
||||
case "npm":
|
||||
nodeOut, err := ensureNodeRuntime(ctx)
|
||||
if nodeOut != "" {
|
||||
outs = append(outs, nodeOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
installOut, err := runInstallCommand(ctx, "npm i -g "+shellEscapeArg(pkgName))
|
||||
if installOut != "" {
|
||||
outs = append(outs, installOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
binName, err = resolveNpmPackageBin(ctx, pkgName)
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
case "uv":
|
||||
if !commandExists("uv") {
|
||||
return "", "", "", fmt.Errorf("uv is not installed; install uv first to auto-install %s", pkgName)
|
||||
}
|
||||
installOut, err := runInstallCommand(ctx, "uv tool install "+shellEscapeArg(pkgName))
|
||||
if installOut != "" {
|
||||
outs = append(outs, installOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
binName = guessSimpleCommandName(pkgName)
|
||||
case "bun":
|
||||
if !commandExists("bun") {
|
||||
return "", "", "", fmt.Errorf("bun is not installed; install bun first to auto-install %s", pkgName)
|
||||
}
|
||||
installOut, err := runInstallCommand(ctx, "bun add -g "+shellEscapeArg(pkgName))
|
||||
if installOut != "" {
|
||||
outs = append(outs, installOut)
|
||||
}
|
||||
if err != nil {
|
||||
return strings.Join(outs, "\n"), "", "", err
|
||||
}
|
||||
binName = guessSimpleCommandName(pkgName)
|
||||
default:
|
||||
return "", "", "", fmt.Errorf("unsupported installer: %s", installer)
|
||||
}
|
||||
binPath = resolveInstalledBinary(ctx, binName)
|
||||
if strings.TrimSpace(binPath) == "" {
|
||||
return strings.Join(outs, "\n"), binName, "", fmt.Errorf("installed %s but binary %q not found in PATH", pkgName, binName)
|
||||
}
|
||||
outs = append(outs, fmt.Sprintf("installed %s", pkgName))
|
||||
outs = append(outs, fmt.Sprintf("installed %s via %s", pkgName, installer))
|
||||
outs = append(outs, fmt.Sprintf("resolved binary: %s", binPath))
|
||||
return strings.Join(outs, "\n"), binName, binPath, nil
|
||||
}
|
||||
|
||||
func guessSimpleCommandName(pkgName string) string {
|
||||
pkgName = strings.TrimSpace(pkgName)
|
||||
pkgName = strings.TrimPrefix(pkgName, "@")
|
||||
if idx := strings.LastIndex(pkgName, "/"); idx >= 0 {
|
||||
pkgName = pkgName[idx+1:]
|
||||
}
|
||||
return strings.TrimSpace(pkgName)
|
||||
}
|
||||
|
||||
func resolveNpmPackageBin(ctx context.Context, pkgName string) (string, error) {
|
||||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
Reference in New Issue
Block a user