Commit 36dbbc1
Changed files (6)
cmd/bash/main.go
@@ -0,0 +1,140 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+
+ "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.NewBashServer(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.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")
+}
\ No newline at end of file
pkg/bash/handlers.go
@@ -0,0 +1,393 @@
+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 (bs *BashServer) 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 := bs.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 (bs *BashServer) HandleBashExecStream(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ // For now, this is the same as regular execution
+ // TODO: Implement actual streaming in future version
+ return bs.HandleBashExec(req)
+}
+
+// Documentation Tools
+
+// HandleManPage handles the man_page tool
+func (bs *BashServer) 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 := bs.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 (bs *BashServer) 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 := bs.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 (bs *BashServer) 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 := bs.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 (bs *BashServer) 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 (bs *BashServer) HandleGetWorkingDir(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ workingDir := bs.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 (bs *BashServer) 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 := bs.setWorkingDir(directory); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to set working directory: %v", err)), nil
+ }
+
+ result := map[string]string{
+ "working_directory": bs.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 (bs *BashServer) HandleSystemInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ sysInfo, err := bs.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 (bs *BashServer) 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 := bs.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 (bs *BashServer) HandleSystemResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ sysInfo, err := bs.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 (bs *BashServer) HandleHistoryResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ history := bs.getCommandHistory()
+
+ return mcp.ReadResourceResult{
+ Contents: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: toJSONString(history),
+ },
+ },
+ }, nil
+}
+
+// HandleEnvResource handles the environment variables resource
+func (bs *BashServer) 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)
+}
\ No newline at end of file
pkg/bash/server.go
@@ -0,0 +1,437 @@
+package bash
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+// BashServer implements the Bash MCP server
+type BashServer struct {
+ *mcp.Server
+ 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"`
+}
+
+// NewBashServer creates a new Bash MCP server
+func NewBashServer(config *Config) (*BashServer, 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
+ }
+
+ // Create base MCP server
+ baseServer := mcp.NewServer("bash", "1.0.0")
+
+ server := &BashServer{
+ Server: baseServer,
+ workingDir: workingDir,
+ commandHistory: make([]CommandRecord, 0, config.HistorySize),
+ config: config,
+ }
+
+ // Register all tools
+ server.registerTools()
+
+ // Register resources
+ server.registerResources()
+
+ return server, 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
+}
+
+// registerTools registers all bash tools with the server
+func (bs *BashServer) registerTools() {
+ // Core execution tools
+ bs.RegisterTool("bash_exec", bs.HandleBashExec)
+ bs.RegisterTool("bash_exec_stream", bs.HandleBashExecStream)
+
+ // Documentation tools
+ bs.RegisterTool("man_page", bs.HandleManPage)
+ bs.RegisterTool("which_command", bs.HandleWhichCommand)
+ bs.RegisterTool("command_help", bs.HandleCommandHelp)
+
+ // Environment management tools
+ bs.RegisterTool("get_env", bs.HandleGetEnv)
+ bs.RegisterTool("get_working_dir", bs.HandleGetWorkingDir)
+ bs.RegisterTool("set_working_dir", bs.HandleSetWorkingDir)
+
+ // System information tools
+ bs.RegisterTool("system_info", bs.HandleSystemInfo)
+ bs.RegisterTool("process_info", bs.HandleProcessInfo)
+}
+
+// registerResources registers MCP resources
+func (bs *BashServer) registerResources() {
+ // System information resource
+ systemResource := mcp.Resource{
+ URI: "bash://system/info",
+ Name: "System Information",
+ Description: "Live system information and environment state",
+ MimeType: "application/json",
+ }
+ bs.Server.RegisterResourceWithDefinition(systemResource, bs.HandleSystemResource)
+
+ // Command history resource
+ historyResource := mcp.Resource{
+ URI: "bash://history/recent",
+ Name: "Command History",
+ Description: "Recent command execution history",
+ MimeType: "application/json",
+ }
+ bs.Server.RegisterResourceWithDefinition(historyResource, bs.HandleHistoryResource)
+
+ // Environment variables resource
+ envResource := mcp.Resource{
+ URI: "bash://env/all",
+ Name: "Environment Variables",
+ Description: "Complete environment variables",
+ MimeType: "application/json",
+ }
+ bs.Server.RegisterResourceWithDefinition(envResource, bs.HandleEnvResource)
+}
+
+// addCommandRecord adds a command record to history
+func (bs *BashServer) addCommandRecord(record CommandRecord) {
+ bs.mu.Lock()
+ defer bs.mu.Unlock()
+
+ // Add record
+ bs.commandHistory = append(bs.commandHistory, record)
+
+ // Trim history if it exceeds the maximum size
+ if len(bs.commandHistory) > bs.config.HistorySize {
+ bs.commandHistory = bs.commandHistory[len(bs.commandHistory)-bs.config.HistorySize:]
+ }
+}
+
+// getCommandHistory returns a copy of the command history
+func (bs *BashServer) getCommandHistory() []CommandRecord {
+ bs.mu.RLock()
+ defer bs.mu.RUnlock()
+
+ // Return a copy to avoid race conditions
+ history := make([]CommandRecord, len(bs.commandHistory))
+ copy(history, bs.commandHistory)
+ return history
+}
+
+// getWorkingDir returns the current working directory
+func (bs *BashServer) getWorkingDir() string {
+ bs.mu.RLock()
+ defer bs.mu.RUnlock()
+ return bs.workingDir
+}
+
+// setWorkingDir sets the working directory
+func (bs *BashServer) 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)
+ }
+
+ bs.mu.Lock()
+ defer bs.mu.Unlock()
+ bs.workingDir = absDir
+ return nil
+}
+
+// executeCommand executes a shell command with the given options
+func (bs *BashServer) executeCommand(options ExecutionOptions) (*ExecutionResult, error) {
+ startTime := time.Now()
+
+ // Set default timeout
+ if options.Timeout <= 0 {
+ options.Timeout = bs.config.DefaultTimeout
+ }
+
+ // Enforce maximum timeout
+ if options.Timeout > bs.config.MaxTimeout {
+ options.Timeout = bs.config.MaxTimeout
+ }
+
+ // Set working directory
+ workingDir := options.WorkingDir
+ if workingDir == "" {
+ workingDir = bs.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 = bs.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),
+ }
+ bs.addCommandRecord(record)
+
+ return result, nil
+}
+
+// runCommandWithStderr runs a command and captures both stdout and stderr
+func (bs *BashServer) 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 (bs *BashServer) 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 := bs.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
+}
\ No newline at end of file
pkg/bash/server_test.go
@@ -0,0 +1,423 @@
+package bash
+
+import (
+ "testing"
+ "time"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func TestNewBashServer(t *testing.T) {
+ tests := []struct {
+ name string
+ config *Config
+ wantErr bool
+ }{
+ {
+ name: "default config",
+ config: nil,
+ wantErr: false,
+ },
+ {
+ name: "custom config",
+ config: &Config{
+ DefaultTimeout: 60,
+ MaxTimeout: 600,
+ HistorySize: 200,
+ WorkingDir: "/tmp",
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server, err := NewBashServer(tt.config)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("NewBashServer() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && server == nil {
+ t.Error("NewBashServer() returned nil server")
+ }
+ })
+ }
+}
+
+func TestDefaultConfig(t *testing.T) {
+ config := DefaultConfig()
+
+ if config.DefaultTimeout != 30 {
+ t.Errorf("DefaultTimeout = %v, want 30", config.DefaultTimeout)
+ }
+ if config.MaxTimeout != 300 {
+ t.Errorf("MaxTimeout = %v, want 300", config.MaxTimeout)
+ }
+ if config.HistorySize != 100 {
+ t.Errorf("HistorySize = %v, want 100", config.HistorySize)
+ }
+ if config.WorkingDir != "" {
+ t.Errorf("WorkingDir = %v, want empty string", config.WorkingDir)
+ }
+}
+
+func TestConfigFromEnv(t *testing.T) {
+ // Set environment variables
+ t.Setenv("BASH_MCP_DEFAULT_TIMEOUT", "45")
+ t.Setenv("BASH_MCP_MAX_TIMEOUT", "600")
+ t.Setenv("BASH_MCP_MAX_HISTORY", "50")
+ t.Setenv("BASH_MCP_WORKING_DIR", "/tmp/test")
+
+ config := ConfigFromEnv()
+
+ if config.DefaultTimeout != 45 {
+ t.Errorf("DefaultTimeout = %v, want 45", config.DefaultTimeout)
+ }
+ if config.MaxTimeout != 600 {
+ t.Errorf("MaxTimeout = %v, want 600", config.MaxTimeout)
+ }
+ if config.HistorySize != 50 {
+ t.Errorf("HistorySize = %v, want 50", config.HistorySize)
+ }
+ if config.WorkingDir != "/tmp/test" {
+ t.Errorf("WorkingDir = %v, want /tmp/test", config.WorkingDir)
+ }
+}
+
+func TestExecuteCommand(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ options ExecutionOptions
+ wantErr bool
+ check func(*testing.T, *ExecutionResult)
+ }{
+ {
+ name: "simple echo command",
+ options: ExecutionOptions{
+ Command: "echo 'Hello, World!'",
+ },
+ wantErr: false,
+ check: func(t *testing.T, result *ExecutionResult) {
+ if result.ExitCode != 0 {
+ t.Errorf("ExitCode = %v, want 0", result.ExitCode)
+ }
+ if result.Stdout != "Hello, World!\n" {
+ t.Errorf("Stdout = %v, want 'Hello, World!\\n'", result.Stdout)
+ }
+ },
+ },
+ {
+ name: "command with error",
+ options: ExecutionOptions{
+ Command: "exit 1",
+ CaptureStderr: true,
+ },
+ wantErr: false,
+ check: func(t *testing.T, result *ExecutionResult) {
+ if result.ExitCode != 1 {
+ t.Errorf("ExitCode = %v, want 1", result.ExitCode)
+ }
+ },
+ },
+ {
+ name: "command with stderr",
+ options: ExecutionOptions{
+ Command: "echo 'error' >&2",
+ CaptureStderr: true,
+ },
+ wantErr: false,
+ check: func(t *testing.T, result *ExecutionResult) {
+ if result.ExitCode != 0 {
+ t.Errorf("ExitCode = %v, want 0", result.ExitCode)
+ }
+ if result.Stderr != "error\n" {
+ t.Errorf("Stderr = %v, want 'error\\n'", result.Stderr)
+ }
+ },
+ },
+ {
+ name: "command with timeout",
+ options: ExecutionOptions{
+ Command: "sleep 0.1",
+ Timeout: 1,
+ },
+ wantErr: false,
+ check: func(t *testing.T, result *ExecutionResult) {
+ if result.ExitCode != 0 {
+ t.Errorf("ExitCode = %v, want 0", result.ExitCode)
+ }
+ },
+ },
+ {
+ name: "command with environment variable",
+ options: ExecutionOptions{
+ Command: "echo $TEST_VAR",
+ Env: map[string]string{
+ "TEST_VAR": "test_value",
+ },
+ },
+ wantErr: false,
+ check: func(t *testing.T, result *ExecutionResult) {
+ if result.ExitCode != 0 {
+ t.Errorf("ExitCode = %v, want 0", result.ExitCode)
+ }
+ if result.Stdout != "test_value\n" {
+ t.Errorf("Stdout = %v, want 'test_value\\n'", result.Stdout)
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := server.executeCommand(tt.options)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("executeCommand() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && result != nil && tt.check != nil {
+ tt.check(t, result)
+ }
+ })
+ }
+}
+
+func TestCommandHistory(t *testing.T) {
+ config := &Config{
+ DefaultTimeout: 30,
+ MaxTimeout: 300,
+ HistorySize: 3,
+ WorkingDir: "",
+ }
+
+ server, err := NewBashServer(config)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ // Execute multiple commands
+ for i := 0; i < 5; i++ {
+ _, err := server.executeCommand(ExecutionOptions{
+ Command: "echo test",
+ })
+ if err != nil {
+ t.Fatalf("Failed to execute command: %v", err)
+ }
+ }
+
+ // Check history size is limited
+ history := server.getCommandHistory()
+ if len(history) != 3 {
+ t.Errorf("History length = %v, want 3", len(history))
+ }
+}
+
+func TestSetWorkingDir(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ dir string
+ wantErr bool
+ }{
+ {
+ name: "valid directory",
+ dir: "/tmp",
+ wantErr: false,
+ },
+ {
+ name: "non-existent directory",
+ dir: "/this/does/not/exist",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := server.setWorkingDir(tt.dir)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("setWorkingDir() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestGetSystemInfo(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ info, err := server.getSystemInfo()
+ if err != nil {
+ t.Fatalf("getSystemInfo() error = %v", err)
+ }
+
+ // Check that required fields are populated
+ if info.Hostname == "" {
+ t.Error("Hostname is empty")
+ }
+ if info.OS == "" {
+ t.Error("OS is empty")
+ }
+ if info.Architecture == "" {
+ t.Error("Architecture is empty")
+ }
+ if info.User == "" {
+ t.Error("User is empty")
+ }
+}
+
+func TestHandleBashExec(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ req := mcp.CallToolRequest{
+ Name: "bash_exec",
+ Arguments: map[string]interface{}{
+ "command": "echo 'test output'",
+ },
+ }
+
+ result, err := server.HandleBashExec(req)
+ if err != nil {
+ t.Fatalf("HandleBashExec() error = %v", err)
+ }
+
+ // Check that we got content back
+ if len(result.Content) == 0 {
+ t.Fatal("No content returned")
+ }
+
+ // The content should be a TextContent
+ textContent, ok := result.Content[0].(mcp.TextContent)
+ if !ok {
+ t.Fatal("Content is not TextContent")
+ }
+
+ // Should contain the output
+ if textContent.Text == "" {
+ t.Error("Text content is empty")
+ }
+}
+
+func TestHandleSystemInfo(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ req := mcp.CallToolRequest{
+ Name: "system_info",
+ Arguments: map[string]interface{}{},
+ }
+
+ result, err := server.HandleSystemInfo(req)
+ if err != nil {
+ t.Fatalf("HandleSystemInfo() error = %v", err)
+ }
+
+ // Check that we got content back
+ if len(result.Content) == 0 {
+ t.Fatal("No content returned")
+ }
+}
+
+func TestHandleSetWorkingDir(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ req := mcp.CallToolRequest{
+ Name: "set_working_dir",
+ Arguments: map[string]interface{}{
+ "directory": "/tmp",
+ },
+ }
+
+ _, err = server.HandleSetWorkingDir(req)
+ if err != nil {
+ t.Fatalf("HandleSetWorkingDir() error = %v", err)
+ }
+
+ // Verify the working directory was changed
+ if server.getWorkingDir() != "/tmp" {
+ t.Errorf("Working directory = %v, want /tmp", server.getWorkingDir())
+ }
+}
+
+func TestHandleGetEnv(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ // Set a test environment variable
+ t.Setenv("TEST_ENV_VAR", "test_value")
+
+ req := mcp.CallToolRequest{
+ Name: "get_env",
+ Arguments: map[string]interface{}{
+ "name": "TEST_ENV_VAR",
+ },
+ }
+
+ result, err := server.HandleGetEnv(req)
+ if err != nil {
+ t.Fatalf("HandleGetEnv() error = %v", err)
+ }
+
+ // Check that we got content back
+ if len(result.Content) == 0 {
+ t.Fatal("No content returned")
+ }
+}
+
+func TestCommandRecordCreation(t *testing.T) {
+ server, err := NewBashServer(nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ // Execute a command
+ startTime := time.Now()
+ result, err := server.executeCommand(ExecutionOptions{
+ Command: "echo test",
+ })
+ if err != nil {
+ t.Fatalf("Failed to execute command: %v", err)
+ }
+
+ // Get history
+ history := server.getCommandHistory()
+ if len(history) != 1 {
+ t.Fatalf("Expected 1 history record, got %d", len(history))
+ }
+
+ record := history[0]
+
+ // Verify record fields
+ if record.Command != "echo test" {
+ t.Errorf("Command = %v, want 'echo test'", record.Command)
+ }
+ if record.ExitCode != 0 {
+ t.Errorf("ExitCode = %v, want 0", record.ExitCode)
+ }
+ if record.Timestamp.Before(startTime) {
+ t.Error("Timestamp is before command start time")
+ }
+ if record.Duration != result.ExecutionTime {
+ t.Errorf("Duration = %v, want %v", record.Duration, result.ExecutionTime)
+ }
+}
\ No newline at end of file
pkg/bash/tools.go
@@ -0,0 +1,180 @@
+package bash
+
+import "github.com/xlgmokha/mcp/pkg/mcp"
+
+// ListTools returns all available bash tools with their definitions
+func (bs *BashServer) ListTools() []mcp.Tool {
+ return []mcp.Tool{
+ // Core Execution Tools
+ {
+ Name: "bash_exec",
+ Description: "Execute a shell command and return output",
+ InputSchema: 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"},
+ },
+ },
+ {
+ Name: "bash_exec_stream",
+ Description: "Execute command with real-time output streaming",
+ InputSchema: 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"},
+ },
+ },
+
+ // Documentation Tools
+ {
+ Name: "man_page",
+ Description: "Get manual page for a command",
+ InputSchema: 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"},
+ },
+ },
+ {
+ Name: "which_command",
+ Description: "Find the location of a command",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Command to locate",
+ },
+ },
+ "required": []string{"command"},
+ },
+ },
+ {
+ Name: "command_help",
+ Description: "Get help text for a command (--help flag)",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "command": map[string]interface{}{
+ "type": "string",
+ "description": "Command to get help for",
+ },
+ },
+ "required": []string{"command"},
+ },
+ },
+
+ // Environment Management Tools
+ {
+ Name: "get_env",
+ Description: "Get environment variable value",
+ InputSchema: 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",
+ },
+ },
+ },
+ },
+ {
+ Name: "get_working_dir",
+ Description: "Get the current working directory",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ },
+ },
+ {
+ Name: "set_working_dir",
+ Description: "Set working directory for future commands",
+ InputSchema: 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"},
+ },
+ },
+
+ // System Information Tools
+ {
+ Name: "system_info",
+ Description: "Get basic system information",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ },
+ },
+ {
+ Name: "process_info",
+ Description: "Get information about running processes (ps command wrapper)",
+ InputSchema: 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",
+ },
+ },
+ },
+ },
+ }
+}
\ No newline at end of file
Makefile
@@ -11,7 +11,7 @@ BINDIR = bin
INSTALLDIR = /usr/local/bin
# Server binaries
-SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap
+SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap bash
BINARIES = $(addprefix $(BINDIR)/mcp-,$(SERVERS))
# Build flags
@@ -109,6 +109,7 @@ maildir: $(BINDIR)/mcp-maildir ## Build maildir server only
signal: $(BINDIR)/mcp-signal ## Build signal server only
gitlab: $(BINDIR)/mcp-gitlab ## Build gitlab server only
imap: $(BINDIR)/mcp-imap ## Build imap server only
+bash: $(BINDIR)/mcp-bash ## Build bash server only
help: ## Show this help message
@echo "Go MCP Servers - Available targets:"
@@ -116,4 +117,4 @@ help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Individual servers:"
- @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap"
+ @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap, bash"