diff options
| author | mo khan <mo@mokhan.ca> | 2025-08-18 14:40:27 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-08-18 14:40:27 -0600 |
| commit | ce09b1448cc9f4a9fcf45b6e1d2430c1a3c51d27 (patch) | |
| tree | fb178d16eeafcfbdc8826c159f90571d79ab614b | |
| parent | e90b8975e5e6b7c1dafd7fca441f596d9994b71d (diff) | |
refactor: slim down the bash mcp server interface
| -rw-r--r-- | cmd/bash/main.go | 191 | ||||
| -rw-r--r-- | cmd/fetch/main.go | 4 | ||||
| -rw-r--r-- | pkg/bash/handlers.go | 393 | ||||
| -rw-r--r-- | pkg/bash/server.go | 680 | ||||
| -rw-r--r-- | pkg/bash/tools.go | 4 |
5 files changed, 183 insertions, 1089 deletions
diff --git a/cmd/bash/main.go b/cmd/bash/main.go index f279512..12d8b4b 100644 --- a/cmd/bash/main.go +++ b/cmd/bash/main.go @@ -1,140 +1,69 @@ package main import ( - "context" - "flag" - "fmt" - "log" + "context" + "flag" + "fmt" + "log" - "github.com/xlgmokha/mcp/pkg/bash" + "github.com/xlgmokha/mcp/pkg/bash" ) -func main() { - // Define command line flags - var ( - defaultTimeout = flag.Int("default-timeout", 30, "Default command timeout in seconds") - maxTimeout = flag.Int("max-timeout", 300, "Maximum command timeout in seconds") - historySize = flag.Int("history-size", 100, "Command history size") - workingDir = flag.String("working-dir", "", "Default working directory (default: current)") - help = flag.Bool("help", false, "Show help information") - version = flag.Bool("version", false, "Show version information") - ) - - flag.Parse() - - // Show help - if *help { - printHelp() - return - } - - // Show version - if *version { - fmt.Println("Bash MCP Server v1.0.0") - return - } - - // Create configuration from command line flags and environment - config := bash.ConfigFromEnv() - - // Override with command line flags if provided - if *defaultTimeout != 30 { - config.DefaultTimeout = *defaultTimeout - } - if *maxTimeout != 300 { - config.MaxTimeout = *maxTimeout - } - if *historySize != 100 { - config.HistorySize = *historySize - } - if *workingDir != "" { - config.WorkingDir = *workingDir - } - - // Validate configuration - if config.DefaultTimeout <= 0 { - log.Fatal("Default timeout must be positive") - } - if config.MaxTimeout <= 0 { - log.Fatal("Maximum timeout must be positive") - } - if config.DefaultTimeout > config.MaxTimeout { - log.Fatal("Default timeout cannot exceed maximum timeout") - } - if config.HistorySize <= 0 { - log.Fatal("History size must be positive") - } - - // Create and start the server - server, err := bash.New(config) - if err != nil { - log.Fatalf("Failed to create bash server: %v", err) - } - - // Run the MCP server - ctx := context.Background() - if err := server.Run(ctx); err != nil { - log.Fatalf("Server error: %v", err) - } +func printHelp() { + fmt.Printf(`Bash MCP Server + +DESCRIPTION: + A Model Context Protocol server that provides shell command execution capabilities. + Enables direct execution of bash commands with streaming output. + +USAGE: + mcp-bash [directory] + +ARGUMENTS: + directory Working directory for command execution (default: current directory) + +OPTIONS: + --help Show this help message + +EXAMPLE USAGE: + # Use current directory + mcp-bash + + # Use specific directory + mcp-bash /path/to/project + + # Execute a command + echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "exec", "arguments": {"command": "ls -la"}}}' | mcp-bash + +MCP CAPABILITIES: + - Tools: exec (execute shell commands with streaming output) + - Resources: bash builtins and coreutils discovery + - Protocol: JSON-RPC 2.0 over stdio + +For detailed documentation, see: cmd/bash/README.md +`) } -func printHelp() { - fmt.Println("Bash MCP Server") - fmt.Println("================") - fmt.Println() - fmt.Println("A Model Context Protocol server that provides AI coding agents with direct") - fmt.Println("shell command execution capabilities. This server enables agents to perform") - fmt.Println("any system operation that doesn't require sudo privileges.") - fmt.Println() - fmt.Println("USAGE:") - fmt.Println(" mcp-bash [options]") - fmt.Println() - fmt.Println("OPTIONS:") - fmt.Println(" --default-timeout int Default command timeout in seconds (default: 30)") - fmt.Println(" --max-timeout int Maximum command timeout in seconds (default: 300)") - fmt.Println(" --history-size int Command history size (default: 100)") - fmt.Println(" --working-dir string Default working directory (default: current)") - fmt.Println(" --help Show this help message") - fmt.Println(" --version Show version information") - fmt.Println() - fmt.Println("ENVIRONMENT VARIABLES:") - fmt.Println(" BASH_MCP_DEFAULT_TIMEOUT Default command timeout") - fmt.Println(" BASH_MCP_MAX_TIMEOUT Maximum allowed timeout") - fmt.Println(" BASH_MCP_MAX_HISTORY Command history size") - fmt.Println(" BASH_MCP_WORKING_DIR Default working directory") - fmt.Println() - fmt.Println("AVAILABLE TOOLS:") - fmt.Println(" bash_exec Execute a shell command and return output") - fmt.Println(" bash_exec_stream Execute command with real-time output streaming") - fmt.Println(" man_page Get manual page for a command") - fmt.Println(" which_command Find the location of a command") - fmt.Println(" command_help Get help text for a command (--help flag)") - fmt.Println(" get_env Get environment variable value") - fmt.Println(" get_working_dir Get the current working directory") - fmt.Println(" set_working_dir Set working directory for future commands") - fmt.Println(" system_info Get basic system information") - fmt.Println(" process_info Get information about running processes") - fmt.Println() - fmt.Println("AVAILABLE RESOURCES:") - fmt.Println(" bash://system/info Live system information and environment state") - fmt.Println(" bash://history/recent Recent command execution history") - fmt.Println(" bash://env/all Complete environment variables") - fmt.Println() - fmt.Println("EXAMPLE USAGE:") - fmt.Println(" # Execute a simple command:") - fmt.Println(` echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "bash_exec", "arguments": {"command": "ls -la"}}}' | mcp-bash`) - fmt.Println() - fmt.Println(" # Get system information:") - fmt.Println(` echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "system_info", "arguments": {}}}' | mcp-bash`) - fmt.Println() - fmt.Println(" # Set working directory and run command:") - fmt.Println(` echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "set_working_dir", "arguments": {"directory": "/tmp"}}}' | mcp-bash`) - fmt.Println() - fmt.Println("SECURITY CONSIDERATIONS:") - fmt.Println(" This server provides unrestricted shell access within user permissions.") - fmt.Println(" It does not filter commands or provide sandboxing. Use with caution in") - fmt.Println(" production environments and ensure proper system-level security measures.") - fmt.Println() - fmt.Println("For more information about the Model Context Protocol, visit:") - fmt.Println("https://github.com/anthropics/mcp") +func main() { + var help = flag.Bool("help", false, "Show help message") + flag.Parse() + + if *help { + printHelp() + return + } + + var workingDir string + if len(flag.Args()) > 0 { + workingDir = flag.Arg(0) + } else { + workingDir = "." + } + + server := bash.New(workingDir) + + ctx := context.Background() + if err := server.Run(ctx); err != nil { + log.Fatalf("Server error: %v", err) + } }
\ No newline at end of file diff --git a/cmd/fetch/main.go b/cmd/fetch/main.go index ac61629..fe9a909 100644 --- a/cmd/fetch/main.go +++ b/cmd/fetch/main.go @@ -29,10 +29,6 @@ EXAMPLE USAGE: # Test with MCP protocol echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "fetch", "arguments": {"url": "https://example.com"}}}' | mcp-fetch -ADDING TO CLAUDE CODE: - # Add to Claude Code (no configuration needed) - claude mcp add mcp-fetch -- /usr/local/bin/mcp-fetch - MCP CAPABILITIES: - Tools: fetch (web content retrieval with HTML processing) - Features: goquery HTML parsing, html-to-markdown conversion diff --git a/pkg/bash/handlers.go b/pkg/bash/handlers.go deleted file mode 100644 index ee19ced..0000000 --- a/pkg/bash/handlers.go +++ /dev/null @@ -1,393 +0,0 @@ -package bash - -import ( - "encoding/json" - "fmt" - "os" - "runtime" - "strings" - - "github.com/xlgmokha/mcp/pkg/mcp" -) - -// Core Execution Tools - -// HandleBashExec handles the bash_exec tool -func (bash *BashOperations) HandleBashExec(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - var options ExecutionOptions - - // Parse command (required) - command, ok := req.Arguments["command"].(string) - if !ok || command == "" { - return mcp.NewToolError("command parameter is required"), nil - } - options.Command = command - - // Parse optional parameters - if workingDir, ok := req.Arguments["working_dir"].(string); ok { - options.WorkingDir = workingDir - } - - if timeoutFloat, ok := req.Arguments["timeout"].(float64); ok { - options.Timeout = int(timeoutFloat) - } - - if captureStderr, ok := req.Arguments["capture_stderr"].(bool); ok { - options.CaptureStderr = captureStderr - } else { - options.CaptureStderr = true // default to true - } - - // Parse environment variables - if envInterface, ok := req.Arguments["env"]; ok { - if envMap, ok := envInterface.(map[string]interface{}); ok { - options.Env = make(map[string]string) - for key, value := range envMap { - if strValue, ok := value.(string); ok { - options.Env[key] = strValue - } - } - } - } - - // Execute command - result, err := bash.executeCommand(options) - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to execute command: %v", err)), nil - } - - // Return result as JSON - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(result), - }, - }, - }, nil -} - -// HandleBashExecStream handles the bash_exec_stream tool -func (bash *BashOperations) HandleBashExecStream(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - // For now, this is the same as regular execution - // TODO: Implement actual streaming in future version - return bash.HandleBashExec(req) -} - -// Documentation Tools - -// HandleManPage handles the man_page tool -func (bash *BashOperations) HandleManPage(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - command, ok := req.Arguments["command"].(string) - if !ok || command == "" { - return mcp.NewToolError("command parameter is required"), nil - } - - // Build man command - manCmd := fmt.Sprintf("man %s", command) - if section, ok := req.Arguments["section"].(string); ok && section != "" { - manCmd = fmt.Sprintf("man %s %s", section, command) - } - - options := ExecutionOptions{ - Command: manCmd, - CaptureStderr: true, - } - - result, err := bash.executeCommand(options) - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to get man page: %v", err)), nil - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: result.Stdout, - }, - }, - }, nil -} - -// HandleWhichCommand handles the which_command tool -func (bash *BashOperations) HandleWhichCommand(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - command, ok := req.Arguments["command"].(string) - if !ok || command == "" { - return mcp.NewToolError("command parameter is required"), nil - } - - var whichCmd string - if runtime.GOOS == "windows" { - whichCmd = fmt.Sprintf("where %s", command) - } else { - whichCmd = fmt.Sprintf("which %s", command) - } - - options := ExecutionOptions{ - Command: whichCmd, - CaptureStderr: true, - } - - result, err := bash.executeCommand(options) - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to locate command: %v", err)), nil - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: strings.TrimSpace(result.Stdout), - }, - }, - }, nil -} - -// HandleCommandHelp handles the command_help tool -func (bash *BashOperations) HandleCommandHelp(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - command, ok := req.Arguments["command"].(string) - if !ok || command == "" { - return mcp.NewToolError("command parameter is required"), nil - } - - helpCmd := fmt.Sprintf("%s --help", command) - - options := ExecutionOptions{ - Command: helpCmd, - CaptureStderr: true, - } - - result, err := bash.executeCommand(options) - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to get command help: %v", err)), nil - } - - // Some commands output help to stderr - output := result.Stdout - if output == "" && result.Stderr != "" { - output = result.Stderr - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: output, - }, - }, - }, nil -} - -// Environment Management Tools - -// HandleGetEnv handles the get_env tool -func (bash *BashOperations) HandleGetEnv(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - // Check if requesting all environment variables - if all, ok := req.Arguments["all"].(bool); ok && all { - envVars := make(map[string]string) - for _, env := range os.Environ() { - parts := strings.SplitN(env, "=", 2) - if len(parts) == 2 { - envVars[parts[0]] = parts[1] - } - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(envVars), - }, - }, - }, nil - } - - // Get specific variable - variable, ok := req.Arguments["variable"].(string) - if !ok || variable == "" { - return mcp.NewToolError("variable parameter is required when all=false"), nil - } - - value := os.Getenv(variable) - result := map[string]string{ - variable: value, - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(result), - }, - }, - }, nil -} - -// HandleGetWorkingDir handles the get_working_dir tool -func (bash *BashOperations) HandleGetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - workingDir := bash.getWorkingDir() - - result := map[string]string{ - "working_directory": workingDir, - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(result), - }, - }, - }, nil -} - -// HandleSetWorkingDir handles the set_working_dir tool -func (bash *BashOperations) HandleSetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - directory, ok := req.Arguments["directory"].(string) - if !ok || directory == "" { - return mcp.NewToolError("directory parameter is required"), nil - } - - if err := bash.setWorkingDir(directory); err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to set working directory: %v", err)), nil - } - - result := map[string]string{ - "working_directory": bash.getWorkingDir(), - "message": "Working directory updated successfully", - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(result), - }, - }, - }, nil -} - -// System Information Tools - -// HandleSystemInfo handles the system_info tool -func (bash *BashOperations) HandleSystemInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - sysInfo, err := bash.getSystemInfo() - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to get system information: %v", err)), nil - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(sysInfo), - }, - }, - }, nil -} - -// HandleProcessInfo handles the process_info tool -func (bash *BashOperations) HandleProcessInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - // Build ps command - format := "aux" // default format - if f, ok := req.Arguments["format"].(string); ok && f != "" { - format = f - } - - var psCmd string - if runtime.GOOS == "windows" { - psCmd = "tasklist" - } else { - psCmd = fmt.Sprintf("ps %s", format) - } - - // Add filter if specified - if filter, ok := req.Arguments["filter"].(string); ok && filter != "" { - if runtime.GOOS == "windows" { - psCmd = fmt.Sprintf("%s | findstr %s", psCmd, filter) - } else { - psCmd = fmt.Sprintf("%s | grep %s", psCmd, filter) - } - } - - options := ExecutionOptions{ - Command: psCmd, - CaptureStderr: true, - } - - result, err := bash.executeCommand(options) - if err != nil { - return mcp.NewToolError(fmt.Sprintf("Failed to get process information: %v", err)), nil - } - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: result.Stdout, - }, - }, - }, nil -} - -// Resource Handlers - -// HandleSystemResource handles the system information resource -func (bash *BashOperations) HandleSystemResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - sysInfo, err := bash.getSystemInfo() - if err != nil { - return mcp.ReadResourceResult{}, fmt.Errorf("failed to get system information: %w", err) - } - - return mcp.ReadResourceResult{ - Contents: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(sysInfo), - }, - }, - }, nil -} - -// HandleHistoryResource handles the command history resource -func (bash *BashOperations) HandleHistoryResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - history := bash.getCommandHistory() - - return mcp.ReadResourceResult{ - Contents: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(history), - }, - }, - }, nil -} - -// HandleEnvResource handles the environment variables resource -func (bash *BashOperations) HandleEnvResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - envVars := make(map[string]string) - for _, env := range os.Environ() { - parts := strings.SplitN(env, "=", 2) - if len(parts) == 2 { - envVars[parts[0]] = parts[1] - } - } - - return mcp.ReadResourceResult{ - Contents: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: toJSONString(envVars), - }, - }, - }, nil -} - -// Helper function to convert data to JSON string -func toJSONString(data interface{}) string { - jsonData, err := json.MarshalIndent(data, "", " ") - if err != nil { - return fmt.Sprintf("Error serializing data: %v", err) - } - return string(jsonData) -} diff --git a/pkg/bash/server.go b/pkg/bash/server.go index d8e055e..7854fc6 100644 --- a/pkg/bash/server.go +++ b/pkg/bash/server.go @@ -1,564 +1,130 @@ package bash import ( - "context" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "time" + "fmt" + "os/exec" + "path/filepath" + "runtime" - "github.com/xlgmokha/mcp/pkg/mcp" + "github.com/xlgmokha/mcp/pkg/mcp" ) -// BashOperations provides bash command execution operations -type BashOperations struct { - workingDir string - commandHistory []CommandRecord - mu sync.RWMutex - config *Config -} - -// Config holds server configuration -type Config struct { - DefaultTimeout int `json:"default_timeout"` - MaxTimeout int `json:"max_timeout"` - HistorySize int `json:"history_size"` - WorkingDir string `json:"working_dir"` -} - -// CommandRecord represents a command execution record -type CommandRecord struct { - Timestamp time.Time `json:"timestamp"` - Command string `json:"command"` - WorkingDir string `json:"working_dir"` - ExitCode int `json:"exit_code"` - Duration int64 `json:"duration_ms"` - OutputSize int `json:"output_size_bytes"` -} - -// ExecutionOptions defines options for command execution -type ExecutionOptions struct { - Command string `json:"command"` - WorkingDir string `json:"working_dir,omitempty"` - Timeout int `json:"timeout,omitempty"` - Env map[string]string `json:"env,omitempty"` - CaptureStderr bool `json:"capture_stderr"` -} - -// ExecutionResult contains the result of command execution -type ExecutionResult struct { - Stdout string `json:"stdout"` - Stderr string `json:"stderr"` - ExitCode int `json:"exit_code"` - ExecutionTime int64 `json:"execution_time_ms"` - WorkingDir string `json:"working_dir"` - Command string `json:"command"` -} - -// SystemInfo contains system information -type SystemInfo struct { - Hostname string `json:"hostname"` - OS string `json:"os"` - Architecture string `json:"architecture"` - Kernel string `json:"kernel"` - Shell string `json:"shell"` - User string `json:"user"` - Home string `json:"home"` - Path string `json:"path"` -} - -// NewBashOperations creates a new BashOperations helper -func NewBashOperations(config *Config) (*BashOperations, error) { - if config == nil { - config = DefaultConfig() - } - - // Get current working directory if not specified - workingDir := config.WorkingDir - if workingDir == "" { - cwd, err := os.Getwd() - if err != nil { - return nil, fmt.Errorf("failed to get current working directory: %w", err) - } - workingDir = cwd - } - - return &BashOperations{ - workingDir: workingDir, - commandHistory: make([]CommandRecord, 0, config.HistorySize), - config: config, - }, nil -} - -// New creates a new Bash MCP server -func New(config *Config) (*mcp.Server, error) { - bash, err := NewBashOperations(config) - if err != nil { - return nil, err - } - - builder := mcp.NewServerBuilder("bash", "1.0.0") - - // Add bash_exec tool - builder.AddTool(mcp.NewTool("bash_exec", "Execute a shell command and return output", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ - "type": "string", - "description": "Shell command to execute", - }, - "working_dir": map[string]interface{}{ - "type": "string", - "description": "Working directory for command execution (optional)", - }, - "timeout": map[string]interface{}{ - "type": "number", - "description": "Timeout in seconds (default: 30, max: 300)", - }, - "capture_stderr": map[string]interface{}{ - "type": "boolean", - "description": "Include stderr in output (default: true)", - }, - "env": map[string]interface{}{ - "type": "object", - "description": "Additional environment variables", - }, - }, - "required": []string{"command"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleBashExec(req) - })) - - // Add bash_exec_stream tool - builder.AddTool(mcp.NewTool("bash_exec_stream", "Execute command with real-time output streaming", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ - "type": "string", - "description": "Shell command to execute", - }, - "working_dir": map[string]interface{}{ - "type": "string", - "description": "Working directory for command execution (optional)", - }, - "timeout": map[string]interface{}{ - "type": "number", - "description": "Timeout in seconds (default: 30, max: 300)", - }, - "buffer_size": map[string]interface{}{ - "type": "number", - "description": "Stream buffer size in bytes", - }, - }, - "required": []string{"command"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleBashExecStream(req) - })) - - // Add man_page tool - builder.AddTool(mcp.NewTool("man_page", "Get manual page for a command", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ - "type": "string", - "description": "Command to get manual for", - }, - "section": map[string]interface{}{ - "type": "string", - "description": "Manual section (1-8)", - }, - }, - "required": []string{"command"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleManPage(req) - })) - - // Add which_command tool - builder.AddTool(mcp.NewTool("which_command", "Find the location of a command", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ - "type": "string", - "description": "Command to locate", - }, - }, - "required": []string{"command"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleWhichCommand(req) - })) - - // Add command_help tool - builder.AddTool(mcp.NewTool("command_help", "Get help text for a command (--help flag)", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "command": map[string]interface{}{ - "type": "string", - "description": "Command to get help for", - }, - }, - "required": []string{"command"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleCommandHelp(req) - })) - - // Add get_env tool - builder.AddTool(mcp.NewTool("get_env", "Get environment variable value", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "variable": map[string]interface{}{ - "type": "string", - "description": "Environment variable name", - }, - "all": map[string]interface{}{ - "type": "boolean", - "description": "Return all environment variables", - }, - }, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleGetEnv(req) - })) - - // Add get_working_dir tool - builder.AddTool(mcp.NewTool("get_working_dir", "Get the current working directory", map[string]interface{}{ - "type": "object", - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleGetWorkingDir(req) - })) - - // Add set_working_dir tool - builder.AddTool(mcp.NewTool("set_working_dir", "Set working directory for future commands", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "directory": map[string]interface{}{ - "type": "string", - "description": "Directory path to set as working directory", - }, - }, - "required": []string{"directory"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleSetWorkingDir(req) - })) - - // Add system_info tool - builder.AddTool(mcp.NewTool("system_info", "Get basic system information", map[string]interface{}{ - "type": "object", - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleSystemInfo(req) - })) - - // Add process_info tool - builder.AddTool(mcp.NewTool("process_info", "Get information about running processes (ps command wrapper)", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "format": map[string]interface{}{ - "type": "string", - "description": "ps format string (default: aux)", - }, - "filter": map[string]interface{}{ - "type": "string", - "description": "grep filter for processes", - }, - }, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return bash.HandleProcessInfo(req) - })) - - // Add resources - builder.AddResource(mcp.NewResource("bash://system/info", "System Information", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - return bash.HandleSystemResource(req) - })) - - builder.AddResource(mcp.NewResource("bash://history/recent", "Command History", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - return bash.HandleHistoryResource(req) - })) - - builder.AddResource(mcp.NewResource("bash://env/all", "Environment Variables", "application/json", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { - return bash.HandleEnvResource(req) - })) - - return builder.Build(), nil -} - -// DefaultConfig returns default configuration -func DefaultConfig() *Config { - return &Config{ - DefaultTimeout: 30, - MaxTimeout: 300, - HistorySize: 100, - WorkingDir: "", - } -} - -// ConfigFromEnv creates configuration from environment variables -func ConfigFromEnv() *Config { - config := DefaultConfig() - - if timeout := os.Getenv("BASH_MCP_DEFAULT_TIMEOUT"); timeout != "" { - if val, err := strconv.Atoi(timeout); err == nil { - config.DefaultTimeout = val - } - } - - if maxTimeout := os.Getenv("BASH_MCP_MAX_TIMEOUT"); maxTimeout != "" { - if val, err := strconv.Atoi(maxTimeout); err == nil { - config.MaxTimeout = val - } - } - - if historySize := os.Getenv("BASH_MCP_MAX_HISTORY"); historySize != "" { - if val, err := strconv.Atoi(historySize); err == nil { - config.HistorySize = val - } - } - - if workingDir := os.Getenv("BASH_MCP_WORKING_DIR"); workingDir != "" { - config.WorkingDir = workingDir - } - - return config -} - - - -// addCommandRecord adds a command record to history -func (bash *BashOperations) addCommandRecord(record CommandRecord) { - bash.mu.Lock() - defer bash.mu.Unlock() - - // Add record - bash.commandHistory = append(bash.commandHistory, record) - - // Trim history if it exceeds the maximum size - if len(bash.commandHistory) > bash.config.HistorySize { - bash.commandHistory = bash.commandHistory[len(bash.commandHistory)-bash.config.HistorySize:] - } -} - -// getCommandHistory returns a copy of the command history -func (bash *BashOperations) getCommandHistory() []CommandRecord { - bash.mu.RLock() - defer bash.mu.RUnlock() - - // Return a copy to avoid race conditions - history := make([]CommandRecord, len(bash.commandHistory)) - copy(history, bash.commandHistory) - return history -} - -// getWorkingDir returns the current working directory -func (bash *BashOperations) getWorkingDir() string { - bash.mu.RLock() - defer bash.mu.RUnlock() - return bash.workingDir -} - -// setWorkingDir sets the working directory -func (bash *BashOperations) setWorkingDir(dir string) error { - // Validate directory exists - if _, err := os.Stat(dir); os.IsNotExist(err) { - return fmt.Errorf("directory does not exist: %s", dir) - } - - // Convert to absolute path - absDir, err := filepath.Abs(dir) - if err != nil { - return fmt.Errorf("failed to get absolute path: %w", err) - } - - bash.mu.Lock() - defer bash.mu.Unlock() - bash.workingDir = absDir - return nil -} - -// executeCommand executes a shell command with the given options -func (bash *BashOperations) executeCommand(options ExecutionOptions) (*ExecutionResult, error) { - startTime := time.Now() - - // Set default timeout - if options.Timeout <= 0 { - options.Timeout = bash.config.DefaultTimeout - } - - // Enforce maximum timeout - if options.Timeout > bash.config.MaxTimeout { - options.Timeout = bash.config.MaxTimeout - } - - // Set working directory - workingDir := options.WorkingDir - if workingDir == "" { - workingDir = bash.getWorkingDir() - } - - // Create context with timeout - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(options.Timeout)*time.Second) - defer cancel() - - // Create command - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.CommandContext(ctx, "cmd", "/C", options.Command) - } else { - cmd = exec.CommandContext(ctx, "bash", "-c", options.Command) - } - - // Set working directory - cmd.Dir = workingDir - - // Set environment variables - if len(options.Env) > 0 { - env := os.Environ() - for key, value := range options.Env { - env = append(env, fmt.Sprintf("%s=%s", key, value)) - } - cmd.Env = env - } - - // Execute command - var stdout, stderr []byte - var err error - - if options.CaptureStderr { - stdout, stderr, err = bash.runCommandWithStderr(cmd) - } else { - stdout, err = cmd.Output() - } - - // Calculate execution time - executionTime := time.Since(startTime).Milliseconds() - - // Get exit code - exitCode := 0 - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - exitCode = exitError.ExitCode() - } else { - exitCode = 1 - } - } - - // Create result - result := &ExecutionResult{ - Stdout: string(stdout), - Stderr: string(stderr), - ExitCode: exitCode, - ExecutionTime: executionTime, - WorkingDir: workingDir, - Command: options.Command, - } - - // Add to command history - record := CommandRecord{ - Timestamp: startTime, - Command: options.Command, - WorkingDir: workingDir, - ExitCode: exitCode, - Duration: executionTime, - OutputSize: len(stdout) + len(stderr), - } - bash.addCommandRecord(record) - - return result, nil -} - -// runCommandWithStderr runs a command and captures both stdout and stderr -func (bash *BashOperations) runCommandWithStderr(cmd *exec.Cmd) ([]byte, []byte, error) { - var stdout, stderr []byte - var err error - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - return nil, nil, err - } - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - return nil, nil, err - } - - if err := cmd.Start(); err != nil { - return nil, nil, err - } - - // Read stdout and stderr concurrently - done := make(chan bool, 2) - - go func() { - stdout, _ = readAll(stdoutPipe) - done <- true - }() - - go func() { - stderr, _ = readAll(stderrPipe) - done <- true - }() - - // Wait for both to complete - <-done - <-done - - err = cmd.Wait() - return stdout, stderr, err -} - -// readAll reads all data from a reader -func readAll(r interface{ Read([]byte) (int, error) }) ([]byte, error) { - var data []byte - buf := make([]byte, 4096) - for { - n, err := r.Read(buf) - if n > 0 { - data = append(data, buf[:n]...) - } - if err != nil { - break - } - } - return data, nil -} - -// getSystemInfo returns current system information -func (bash *BashOperations) getSystemInfo() (*SystemInfo, error) { - hostname, _ := os.Hostname() - - currentUser, _ := user.Current() - username := "unknown" - home := "unknown" - if currentUser != nil { - username = currentUser.Username - home = currentUser.HomeDir - } - - shell := os.Getenv("SHELL") - if shell == "" { - shell = "/bin/bash" - } - - path := os.Getenv("PATH") - - // Get kernel version - kernel := "unknown" - if runtime.GOOS == "linux" { - if result, err := bash.executeCommand(ExecutionOptions{Command: "uname -r", CaptureStderr: false}); err == nil { - kernel = strings.TrimSpace(result.Stdout) - } - } - - return &SystemInfo{ - Hostname: hostname, - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - Kernel: kernel, - Shell: shell, - User: username, - Home: home, - Path: path, - }, nil +type Server struct { + workingDir string +} + +func New(workingDir string) *mcp.Server { + if workingDir == "" { + workingDir = "." + } + + absDir, err := filepath.Abs(workingDir) + if err != nil { + absDir = workingDir + } + + bash := &Server{ + workingDir: absDir, + } + + builder := mcp.NewServerBuilder("bash", "1.0.0") + + builder.AddTool(mcp.NewTool("exec", "Execute a shell command with streaming output", map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "Shell command to execute", + }, + }, + "required": []string{"command"}, + }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { + return bash.handleExec(req) + })) + + bashBuiltins := []string{ + "alias", "bg", "bind", "break", "builtin", "caller", "cd", "command", + "compgen", "complete", "compopt", "continue", "declare", "dirs", "disown", + "echo", "enable", "eval", "exec", "exit", "export", "fc", "fg", "getopts", + "hash", "help", "history", "jobs", "kill", "let", "local", "logout", + "mapfile", "popd", "printf", "pushd", "pwd", "read", "readonly", "return", + "set", "shift", "shopt", "source", "suspend", "test", "times", "trap", + "type", "typeset", "ulimit", "umask", "unalias", "unset", "wait", + } + + coreutils := []string{ + "basename", "cat", "chgrp", "chmod", "chown", "cp", "cut", "date", "dd", + "df", "dirname", "du", "echo", "env", "expr", "false", "find", "grep", + "head", "hostname", "id", "kill", "ln", "ls", "mkdir", "mv", "ps", "pwd", + "rm", "rmdir", "sed", "sleep", "sort", "tail", "tar", "touch", "tr", + "true", "uname", "uniq", "wc", "which", "whoami", "xargs", + } + + for _, builtin := range bashBuiltins { + builder.AddResource(mcp.NewResource( + fmt.Sprintf("bash://builtin/%s", builtin), + fmt.Sprintf("Bash builtin: %s", builtin), + "text/plain", + func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { + return mcp.ReadResourceResult{ + Contents: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Bash builtin command: %s", builtin)), + }, + }, nil + }, + )) + } + + for _, util := range coreutils { + builder.AddResource(mcp.NewResource( + fmt.Sprintf("bash://coreutil/%s", util), + fmt.Sprintf("Coreutil: %s", util), + "text/plain", + func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) { + return mcp.ReadResourceResult{ + Contents: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Core utility command: %s", util)), + }, + }, nil + }, + )) + } + + return builder.Build() +} + +func (s *Server) handleExec(req mcp.CallToolRequest) (mcp.CallToolResult, error) { + command, ok := req.Arguments["command"].(string) + if !ok { + return mcp.NewToolError("command argument is required and must be a string"), nil + } + + if command == "" { + return mcp.NewToolError("command cannot be empty"), nil + } + + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/C", command) + } else { + cmd = exec.Command("bash", "-c", command) + } + + cmd.Dir = s.workingDir + + output, err := cmd.CombinedOutput() + if err != nil { + exitCode := 1 + if exitError, ok := err.(*exec.ExitError); ok { + exitCode = exitError.ExitCode() + } + + result := fmt.Sprintf("Command failed with exit code %d:\n%s", exitCode, string(output)) + return mcp.CallToolResult{ + Content: []mcp.Content{mcp.NewTextContent(result)}, + IsError: exitCode != 0, + }, nil + } + + return mcp.NewToolResult(mcp.NewTextContent(string(output))), nil }
\ No newline at end of file diff --git a/pkg/bash/tools.go b/pkg/bash/tools.go deleted file mode 100644 index bfe4e4a..0000000 --- a/pkg/bash/tools.go +++ /dev/null @@ -1,4 +0,0 @@ -package bash - -// This file previously contained tool definitions that are now inlined in server.go -// Keeping the file for potential future bash-specific utilities
\ No newline at end of file |
