Commit ce09b14
Changed files (5)
pkg
bash
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
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
pkg/bash/handlers.go
@@ -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)
-}
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
pkg/bash/tools.go
@@ -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