main
  1package bash
  2
  3import (
  4  "fmt"
  5  "os/exec"
  6  "path/filepath"
  7  "runtime"
  8
  9  "github.com/xlgmokha/mcp/pkg/mcp"
 10)
 11
 12type Server struct {
 13  workingDir string
 14}
 15
 16func New(workingDir string) *mcp.Server {
 17  if workingDir == "" {
 18    workingDir = "."
 19  }
 20
 21  absDir, err := filepath.Abs(workingDir)
 22  if err != nil {
 23    absDir = workingDir
 24  }
 25
 26  bash := &Server{
 27    workingDir: absDir,
 28  }
 29
 30  builder := mcp.NewServerBuilder("bash", "1.0.0")
 31
 32  builder.AddTool(mcp.NewTool("exec", "Execute a shell command with streaming output", map[string]interface{}{
 33    "type": "object",
 34    "properties": map[string]interface{}{
 35      "command": map[string]interface{}{
 36        "type":        "string",
 37        "description": "Shell command to execute",
 38      },
 39    },
 40    "required": []string{"command"},
 41  }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 42    return bash.handleExec(req)
 43  }))
 44
 45  bashBuiltins := []string{
 46    "alias", "bg", "bind", "break", "builtin", "caller", "cd", "command",
 47    "compgen", "complete", "compopt", "continue", "declare", "dirs", "disown",
 48    "echo", "enable", "eval", "exec", "exit", "export", "fc", "fg", "getopts",
 49    "hash", "help", "history", "jobs", "kill", "let", "local", "logout",
 50    "mapfile", "popd", "printf", "pushd", "pwd", "read", "readonly", "return",
 51    "set", "shift", "shopt", "source", "suspend", "test", "times", "trap",
 52    "type", "typeset", "ulimit", "umask", "unalias", "unset", "wait",
 53  }
 54
 55  coreutils := []string{
 56    "basename", "cat", "chgrp", "chmod", "chown", "cp", "cut", "date", "dd",
 57    "df", "dirname", "du", "echo", "env", "expr", "false", "find", "grep",
 58    "head", "hostname", "id", "kill", "ln", "ls", "mkdir", "mv", "ps", "pwd",
 59    "rm", "rmdir", "sed", "sleep", "sort", "tail", "tar", "touch", "tr",
 60    "true", "uname", "uniq", "wc", "which", "whoami", "xargs",
 61  }
 62
 63  for _, builtin := range bashBuiltins {
 64    builder.AddResource(mcp.NewResource(
 65      fmt.Sprintf("bash://builtin/%s", builtin),
 66      fmt.Sprintf("Bash builtin: %s", builtin),
 67      "text/plain",
 68      func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
 69        return mcp.ReadResourceResult{
 70          Contents: []mcp.Content{
 71            mcp.NewTextContent(fmt.Sprintf("Bash builtin command: %s", builtin)),
 72          },
 73        }, nil
 74      },
 75    ))
 76  }
 77
 78  for _, util := range coreutils {
 79    builder.AddResource(mcp.NewResource(
 80      fmt.Sprintf("bash://coreutil/%s", util),
 81      fmt.Sprintf("Coreutil: %s", util),
 82      "text/plain",
 83      func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
 84        return mcp.ReadResourceResult{
 85          Contents: []mcp.Content{
 86            mcp.NewTextContent(fmt.Sprintf("Core utility command: %s", util)),
 87          },
 88        }, nil
 89      },
 90    ))
 91  }
 92
 93  return builder.Build()
 94}
 95
 96func (s *Server) handleExec(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 97  command, ok := req.Arguments["command"].(string)
 98  if !ok {
 99    return mcp.NewToolError("command argument is required and must be a string"), nil
100  }
101
102  if command == "" {
103    return mcp.NewToolError("command cannot be empty"), nil
104  }
105
106  var cmd *exec.Cmd
107  if runtime.GOOS == "windows" {
108    cmd = exec.Command("cmd", "/C", command)
109  } else {
110    cmd = exec.Command("bash", "-c", command)
111  }
112
113  cmd.Dir = s.workingDir
114
115  output, err := cmd.CombinedOutput()
116  if err != nil {
117    exitCode := 1
118    if exitError, ok := err.(*exec.ExitError); ok {
119      exitCode = exitError.ExitCode()
120    }
121
122    result := fmt.Sprintf("Command failed with exit code %d:\n%s", exitCode, string(output))
123    return mcp.CallToolResult{
124      Content: []mcp.Content{mcp.NewTextContent(result)},
125      IsError: exitCode != 0,
126    }, nil
127  }
128
129  return mcp.NewToolResult(mcp.NewTextContent(string(output))), nil
130}