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}