From 85ddf6bc83f7db08715587f8546ee9b511f5114d Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:31:23 +0100 Subject: [PATCH 01/12] feat: fix shell init script output handling (#9) Co-authored-by: localai-bot --- shell/main.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/shell/main.go b/shell/main.go index 2b0b5ff..cb94f2e 100644 --- a/shell/main.go +++ b/shell/main.go @@ -125,14 +125,11 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute func main() { // Run initialization script if SHELL_INIT_SCRIPT is set if initScript := os.Getenv("SHELL_INIT_SCRIPT"); initScript != "" { - log.Printf("Running initialization script: %s", initScript) cmd := exec.CommandContext(context.Background(), "sh", "-c", initScript) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - log.Fatalf("Initialization script failed: %v", err) + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("Initialization script failed: %v\nOutput: %s", err, string(output)) } - log.Println("Initialization script completed successfully") } // Create MCP server for shell command execution From bd302f773aa4cf8b28f3316f430fe46ba47c4bbb Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 22 Feb 2026 10:34:41 +0100 Subject: [PATCH 02/12] Switch base image to Ubuntu and modify CLI install Changed base image from Debian to Ubuntu and updated GitHub CLI installation method. --- shell/Dockerfile | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/shell/Dockerfile b/shell/Dockerfile index 6d34caf..67f983e 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -13,9 +13,7 @@ COPY shell/ ./shell/ # Build the binary RUN CGO_ENABLED=0 GOOS=linux go build -o shell-mcp-server ./shell/ -# Final stage - use Debian instead of Alpine for glibc compatibility -FROM debian:bookworm-slim - +FROM ubuntu # Install required runtime dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -27,13 +25,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI -RUN (type -p wget >/dev/null || (apt-get update && apt-get install wget -y)) \ - && mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && mkdir -p -m 755 /etc/apt/sources.list.d \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0 \ + && apt-add-repository https://cli.github.com/packages \ && apt-get update \ && apt-get install gh -y \ && rm -rf /var/lib/apt/lists/* From 68a83ead57cbc4e8ce6b3246d99013f32edf6adc Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 22 Feb 2026 10:43:05 +0100 Subject: [PATCH 03/12] Update Dockerfile --- shell/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/Dockerfile b/shell/Dockerfile index 67f983e..308a2f2 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ wget \ git jq \ - openssh-client \ + openssh-client gnupg2 \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI From bbe05e9f70199b4d0ec889526b4af6aea25897d2 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 22 Feb 2026 18:49:17 +0100 Subject: [PATCH 04/12] Add software-properties-common to Dockerfile --- shell/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/Dockerfile b/shell/Dockerfile index 308a2f2..2f03df8 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ wget \ git jq \ - openssh-client gnupg2 \ + openssh-client gnupg2 software-properties-common \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI From e150e463b370fe3270cf91198bcafde35f47f26a Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 22 Feb 2026 22:51:37 +0100 Subject: [PATCH 05/12] Update Dockerfile to install GitHub CLI securely --- shell/Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shell/Dockerfile b/shell/Dockerfile index 2f03df8..9197278 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -25,8 +25,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0 \ - && apt-add-repository https://cli.github.com/packages \ +RUN (type -p wget >/dev/null || (apt-get update && apt-get install wget -y)) \ + && mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && mkdir -p -m 755 /etc/apt/sources.list.d \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ && apt-get update \ && apt-get install gh -y \ && rm -rf /var/lib/apt/lists/* From 31d00e2e3d596a1b9685c0d8de5d6af06bba61ce Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 22 Feb 2026 22:51:59 +0100 Subject: [PATCH 06/12] Update Dockerfile to remove unnecessary package Removed 'software-properties-common' from the Dockerfile. --- shell/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/Dockerfile b/shell/Dockerfile index 9197278..0633674 100644 --- a/shell/Dockerfile +++ b/shell/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ wget \ git jq \ - openssh-client gnupg2 software-properties-common \ + openssh-client gnupg2 \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI From 9ba1b159446c8ae8c5c37e56072c09213bf1d1ca Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 23 Feb 2026 22:48:37 +0100 Subject: [PATCH 07/12] Update Docker images section in README Removed specific Docker image entries and added placeholders for components. --- README.md | 42 +++--------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4b6959e..93ac643 100644 --- a/README.md +++ b/README.md @@ -1471,45 +1471,9 @@ func main() { Docker images are automatically built and pushed to GitHub Container Registry: -- `ghcr.io/mudler/mcps/duckduckgo:latest` - Latest DuckDuckGo server -- `ghcr.io/mudler/mcps/duckduckgo:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/duckduckgo:master` - Development versions -- `ghcr.io/mudler/mcps/weather:latest` - Latest Weather server -- `ghcr.io/mudler/mcps/weather:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/weather:master` - Development versions -- `ghcr.io/mudler/mcps/wait:latest` - Latest Wait server -- `ghcr.io/mudler/mcps/wait:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/wait:master` - Development versions -- `ghcr.io/mudler/mcps/memory:latest` - Latest Memory server -- `ghcr.io/mudler/mcps/memory:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/memory:master` - Development versions -- `ghcr.io/mudler/mcps/shell:latest` - Latest Shell server -- `ghcr.io/mudler/mcps/shell:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/shell:master` - Development versions -- `ghcr.io/mudler/mcps/ssh:latest` - Latest SSH server -- `ghcr.io/mudler/mcps/ssh:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/ssh:master` - Development versions -- `ghcr.io/mudler/mcps/homeassistant:latest` - Latest Home Assistant server -- `ghcr.io/mudler/mcps/homeassistant:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/homeassistant:master` - Development versions -- `ghcr.io/mudler/mcps/scripts:latest` - Latest Script Runner server -- `ghcr.io/mudler/mcps/scripts:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/scripts:master` - Development versions -- `ghcr.io/mudler/mcps/localrecall:latest` - Latest LocalRecall server -- `ghcr.io/mudler/mcps/localrecall:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/localrecall:master` - Development versions -- `ghcr.io/mudler/mcps/todo:latest` - Latest TODO server -- `ghcr.io/mudler/mcps/todo:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/todo:master` - Development versions -- `ghcr.io/mudler/mcps/mailbox:latest` - Latest Mailbox server -- `ghcr.io/mudler/mcps/mailbox:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/mailbox:master` - Development versions -- `ghcr.io/mudler/mcps/opencode:latest` - Latest Opencode server -- `ghcr.io/mudler/mcps/opencode:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/opencode:master` - Development versions -- `ghcr.io/mudler/mcps/filesystem:latest` - Latest Filesystem server -- `ghcr.io/mudler/mcps/filesystem:v1.0.0` - Tagged versions -- `ghcr.io/mudler/mcps/filesystem:master` - Development versions +- `ghcr.io/mudler/mcps/:latest` - Latest component tagged version +- `ghcr.io/mudler/mcps/:v` - Specific tagged versions +- `ghcr.io/mudler/mcps/:master` - Development versions ## Contributing From 2833e07c83b3da817e6eeff518b7c95238ea5597 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:00:46 +0100 Subject: [PATCH 08/12] feat: add think MCP server (#11) A no-op tool that forces the model to think about a message. Takes a message string input and echoes it back, useful for debugging or forcing explicit reasoning steps in the model. --- .github/workflows/image.yml | 3 +++ README.md | 50 +++++++++++++++++++++++++++++++++++++ think/main.go | 43 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 think/main.go diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 8847525..4fb55f5 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -27,6 +27,9 @@ jobs: - mcp: wait dockerfile: ./Dockerfile context: ./ + - mcp: think + dockerfile: ./Dockerfile + context: ./ - mcp: memory dockerfile: ./Dockerfile context: ./ diff --git a/README.md b/README.md index 93ac643..a673bf1 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,56 @@ mcp: } ``` + +### 🧠 Think Server + +A no-op tool that forces the model to think about a message. Useful for debugging or forcing explicit reasoning steps in the model. + +**Features:** +- Simple message input that gets echoed back +- Forces the model to explicitly process and think about the message +- Input validation (non-empty message) +- JSON schema validation for inputs/outputs + +**Tool:** +- `think` - Think about a given message + +**Input Format:** +```json +{ + "message": "What is the capital of France?" +} +``` + +**Output Format:** +```json +{ + "result": "Thinking about: What is the capital of France?" +} +``` + +**Docker Image:** +```bash +docker run ghcr.io/mudler/mcps/think:latest +``` + +**LocalAI configuration (to add to the model config):** +```yaml +mcp: + stdio: | + { + "mcpServers": { + "think": { + "command": "docker", + "args": [ + "run", "-i", "--rm", + "ghcr.io/mudler/mcps/think:master" + ] + } + } + } +``` + ### ⏱️ Wait Server A simple wait/sleep server that allows AI models to autonomously wait for a specified duration. Useful for waiting for asynchronous operations to complete. diff --git a/think/main.go b/think/main.go new file mode 100644 index 0000000..eac7303 --- /dev/null +++ b/think/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type Input struct { + Message string `json:"message" jsonschema:"the message to think about - the model should process and analyze this message"` +} + +type Output struct { + Result string `json:"result" jsonschema:"the result of thinking about the message"` +} + +func Think(ctx context.Context, req *mcp.CallToolRequest, input Input) ( + *mcp.CallToolResult, + Output, + error, +) { + // Validate input + if input.Message == "" { + return nil, Output{}, fmt.Errorf("message cannot be empty") + } + + // Simple no-op: just echo back the message + // This forces the model to think by processing and echoing the message + result := fmt.Sprintf("Thinking about: %s", input.Message) + + return nil, Output{Result: result}, nil +} + +func main() { + // Create a server with a single tool. + server := mcp.NewServer(&mcp.Implementation{Name: "think", Version: "v1.0.0"}, nil) + mcp.AddTool(server, &mcp.Tool{Name: "think", Description: "A no-op tool that forces the model to think about a message"}, Think) + if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil { + log.Fatal(err) + } +} From ce16c681af0e195d18bcc815a406a65ca9db56d1 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:07:22 +0100 Subject: [PATCH 09/12] feat(localrecall): gate all log messages under DEBUG flag (#12) * feat: add think MCP server A no-op tool that forces the model to think about a message. Takes a message string input and echoes it back, useful for debugging or forcing explicit reasoning steps in the model. * feat(localrecall): gate all log messages under DEBUG flag - Add debugMode boolean that reads from DEBUG environment variable - Create debugLog() helper function that only prints when DEBUG=1 - Replace all log.Printf/log.Println calls with debugLog() - By default, no logs are emitted to preserve JSON-RPC stdio protocol Fixes: The LocalRecall MCP was corrupting JSON-RPC communication by emitting logs to stdout, causing the container to fail. Logs are now only shown when DEBUG=1 is set. --- localrecall/main.go | 53 +++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/localrecall/main.go b/localrecall/main.go index 734369d..0c5bc2f 100644 --- a/localrecall/main.go +++ b/localrecall/main.go @@ -21,6 +21,14 @@ var httpClient *http.Client var localRecallURL string var apiKey string var defaultCollectionName string +var debugMode bool + +// debugLog prints debug messages only when DEBUG=1 is set +func debugLog(format string, args ...interface{}) { + if debugMode { + log.Printf("[DEBUG] "+format, args...) + } +} // LocalRecall API response structure type APIResponse struct { @@ -601,6 +609,9 @@ func deleteEntryWithCollection(ctx context.Context, collectionName, entry string } func main() { + // Check for debug mode + debugMode = os.Getenv("DEBUG") == "1" + // Get configuration from environment variables localRecallURL = os.Getenv("LOCALRECALL_URL") if localRecallURL == "" { @@ -621,13 +632,13 @@ func main() { // Valid tool names validTools := map[string]bool{ - "search": true, + "search": true, "create_collection": true, - "reset_collection": true, - "add_document": true, - "list_collections": true, - "list_files": true, - "delete_entry": true, + "reset_collection": true, + "add_document": true, + "list_collections": true, + "list_files": true, + "delete_entry": true, } if enabledToolsStr != "" { @@ -641,7 +652,7 @@ func main() { if validTools[tool] { enabledTools[tool] = true } else { - log.Printf("Warning: Unknown tool name '%s' will be ignored", tool) + debugLog("Warning: Unknown tool name '%s' will be ignored", tool) } } } else { @@ -665,13 +676,13 @@ func main() { Name: "search", Description: desc, }, SearchWithoutCollection) - log.Printf("Tool 'search' enabled (using default collection: %s)", defaultCollectionName) + debugLog("Tool 'search' enabled (using default collection: %s)", defaultCollectionName) } else { mcp.AddTool(server, &mcp.Tool{ Name: "search", Description: "Search content in a LocalRecall collection", }, Search) - log.Println("Tool 'search' enabled") + debugLog("Tool 'search' enabled") } } @@ -680,7 +691,7 @@ func main() { Name: "create_collection", Description: "Create a new collection in LocalRecall", }, CreateCollection) - log.Println("Tool 'create_collection' enabled") + debugLog("Tool 'create_collection' enabled") } if enabledTools["reset_collection"] { @@ -688,7 +699,7 @@ func main() { Name: "reset_collection", Description: "Reset (clear) a collection in LocalRecall", }, ResetCollection) - log.Println("Tool 'reset_collection' enabled") + debugLog("Tool 'reset_collection' enabled") } if enabledTools["add_document"] { @@ -698,13 +709,13 @@ func main() { Name: "add_document", Description: desc, }, AddDocumentWithoutCollection) - log.Printf("Tool 'add_document' enabled (using default collection: %s)", defaultCollectionName) + debugLog("Tool 'add_document' enabled (using default collection: %s)", defaultCollectionName) } else { mcp.AddTool(server, &mcp.Tool{ Name: "add_document", Description: "Add a document to a LocalRecall collection", }, AddDocument) - log.Println("Tool 'add_document' enabled") + debugLog("Tool 'add_document' enabled") } } @@ -713,7 +724,7 @@ func main() { Name: "list_collections", Description: "List all collections in LocalRecall", }, ListCollections) - log.Println("Tool 'list_collections' enabled") + debugLog("Tool 'list_collections' enabled") } if enabledTools["list_files"] { @@ -723,13 +734,13 @@ func main() { Name: "list_files", Description: desc, }, ListFilesWithoutCollection) - log.Printf("Tool 'list_files' enabled (using default collection: %s)", defaultCollectionName) + debugLog("Tool 'list_files' enabled (using default collection: %s)", defaultCollectionName) } else { mcp.AddTool(server, &mcp.Tool{ Name: "list_files", Description: "List files in a LocalRecall collection", }, ListFiles) - log.Println("Tool 'list_files' enabled") + debugLog("Tool 'list_files' enabled") } } @@ -740,21 +751,21 @@ func main() { Name: "delete_entry", Description: desc, }, DeleteEntryWithoutCollection) - log.Printf("Tool 'delete_entry' enabled (using default collection: %s)", defaultCollectionName) + debugLog("Tool 'delete_entry' enabled (using default collection: %s)", defaultCollectionName) } else { mcp.AddTool(server, &mcp.Tool{ Name: "delete_entry", Description: "Delete an entry from a LocalRecall collection", }, DeleteEntry) - log.Println("Tool 'delete_entry' enabled") + debugLog("Tool 'delete_entry' enabled") } } - log.Printf("LocalRecall MCP server initialized. URL: %s", localRecallURL) + debugLog("LocalRecall MCP server initialized. URL: %s", localRecallURL) if len(enabledTools) == 0 { - log.Println("Warning: No tools enabled!") + debugLog("Warning: No tools enabled!") } else { - log.Printf("Enabled %d tool(s)", len(enabledTools)) + debugLog("Enabled %d tool(s)", len(enabledTools)) } // Run the server From 08c2ad89c3d0b5ce7b09e8789ba3880fabbdc27f Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:35:04 +0100 Subject: [PATCH 10/12] feat: add codemogger MCP server (#13) * feat: add codemogger MCP server - Add Dockerfile for codemogger (Node.js based MCP server) - Add codemogger to GitHub Actions CI pipeline * fix: remove redundant npm install -g npx (npx is included in node:20-slim) * docs: add codemogger to README --- .github/workflows/image.yml | 3 +++ README.md | 38 +++++++++++++++++++++++++++++++++++++ codemogger/Dockerfile | 3 +++ 3 files changed, 44 insertions(+) create mode 100644 codemogger/Dockerfile diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 4fb55f5..4a24ca2 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -63,6 +63,9 @@ jobs: - mcp: opencode dockerfile: ./opencode/Dockerfile context: ./ + - mcp: codemogger + dockerfile: ./codemogger/Dockerfile + context: ./ permissions: packages: write contents: read diff --git a/README.md b/README.md index a673bf1..f35a66a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,44 @@ mcp: } ``` +### 🤖 Codemogger Server + +A codemogger MCP server that provides code analysis and indexing capabilities. + +**Features:** +- Code search functionality +- Code indexing capabilities +- Reindex functionality +- JSON schema validation for inputs/outputs + +**Tools:** +- `codemogger_search` - Search code using codemogger +- `codemogger_index` - Index code using codemogger +- `codemogger_reindex` - Reindex code using codemogger + +**Docker Image:** +```bash +docker run ghcr.io/mudler/mcps/codemogger:latest +``` + +**LocalAI configuration ( to add to the model config):** +```yaml +mcp: + stdio: | + { + "mcpServers": { + "codemogger": { + "command": "docker", + "args": [ + "run", "-i", "--rm", + "ghcr.io/mudler/mcps/codemogger:master" + ] + } + } + } +``` + + ### 🌤️ Weather Server A weather information server that provides current weather and forecast data for cities worldwide. diff --git a/codemogger/Dockerfile b/codemogger/Dockerfile new file mode 100644 index 0000000..ef2f027 --- /dev/null +++ b/codemogger/Dockerfile @@ -0,0 +1,3 @@ +FROM node:20-slim + +ENTRYPOINT ["npx", "-y", "codemogger", "mcp"] From e1e114214610d62d51b1f6cab09bc855a9e27bcf Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:02:10 +0100 Subject: [PATCH 11/12] feat: add configurable SHELL_TIMEOUT environment variable (#15) - Add getTimeout() function to read SHELL_TIMEOUT env var (default: 30s) - Update ExecuteCommand to use getTimeout() instead of hardcoded value - Update tool description to document the new env variable --- shell/main.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/shell/main.go b/shell/main.go index cb94f2e..17ca484 100644 --- a/shell/main.go +++ b/shell/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "strconv" "strings" "time" @@ -43,6 +44,20 @@ func getWorkingDirectory() string { return os.Getenv("SHELL_WORKING_DIR") } +// getTimeout returns the default timeout from SHELL_TIMEOUT env var, +// or 30 seconds if not set or invalid +func getTimeout() int { + timeoutStr := os.Getenv("SHELL_TIMEOUT") + if timeoutStr == "" { + return 30 + } + timeout, err := strconv.Atoi(timeoutStr) + if err != nil || timeout <= 0 { + return 30 + } + return timeout +} + // ExecuteCommand executes a shell script and returns the output func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input ExecuteCommandInput) ( *mcp.CallToolResult, @@ -52,7 +67,7 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute // Set default timeout if not provided timeout := input.Timeout if timeout <= 0 { - timeout = 30 + timeout = getTimeout() } // Create a context with timeout @@ -146,7 +161,7 @@ func main() { // Add tool for executing shell scripts mcp.AddTool(server, &mcp.Tool{ Name: configurableName, - Description: "Execute a shell script and return the output, exit code, and any errors. The shell command can be configured via SHELL_CMD environment variable (default: 'sh -c'). The working directory can be set via SHELL_WORKING_DIR environment variable. An initialization script can be run before server startup via SHELL_INIT_SCRIPT environment variable.", + Description: "Execute a shell script and return the output, exit code, and any errors. The shell command can be configured via SHELL_CMD environment variable (default: 'sh -c'). The working directory can be set via SHELL_WORKING_DIR environment variable. The default timeout can be configured via SHELL_TIMEOUT environment variable (default: 30 seconds). An initialization script can be run before server startup via SHELL_INIT_SCRIPT environment variable.", }, ExecuteCommand) // Run the server From 779d76def836c1298797cc0c6e9040bac1beb4bf Mon Sep 17 00:00:00 2001 From: localai-bot Date: Mon, 2 Mar 2026 17:02:58 +0000 Subject: [PATCH 12/12] fix: add process group management and interactive command detection for shell MCP - Add syscall.Setpgid to create new process group for proper child process cleanup - Add detectInteractiveCommands function to warn about potentially interactive commands - Add process group kill on timeout to ensure all child processes are terminated - Update description to reflect new capabilities Fixes issue #20 - Interactive commands causing MCP shell to stall indefinitely --- shell/main.go | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/shell/main.go b/shell/main.go index 17ca484..86f3cc7 100644 --- a/shell/main.go +++ b/shell/main.go @@ -8,6 +8,7 @@ import ( "os/exec" "strconv" "strings" + "syscall" "time" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -58,6 +59,27 @@ func getTimeout() int { return timeout } +// detectInteractiveCommands checks if a command might require interactive input +func detectInteractiveCommands(script string) bool { + // List of commands that typically require interactive input + interactiveCommands := []string{ + "vim", "vi", "nano", "emacs", "top", "htop", "less", "more", + "ftp", "sftp", "ssh", "telnet", "passwd", "su", "sudo -i", + "bash -i", "sh -i", "python -i", "python3 -i", "node -i", + "irb", "rails console", "rake console", "dbshell", + "mysql", "psql", "mongosh", "redis-cli", + "nano", "mc", "nmtui", "systemctl edit", + } + + scriptLower := strings.ToLower(script) + for _, cmd := range interactiveCommands { + if strings.Contains(scriptLower, strings.ToLower(cmd)) { + return true + } + } + return false +} + // ExecuteCommand executes a shell script and returns the output func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input ExecuteCommandInput) ( *mcp.CallToolResult, @@ -70,6 +92,11 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute timeout = getTimeout() } + // Check for potentially interactive commands and warn + if detectInteractiveCommands(input.Script) { + log.Printf("Warning: Detected potentially interactive command: %s", input.Script) + } + // Create a context with timeout cmdCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() @@ -94,7 +121,11 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute // Set working directory from environment variable if specified if workDir := getWorkingDirectory(); workDir != "" { - cmd.Dir = workDir + cmd.Dir = workDir} + + // Create process group for proper cleanup of child processes + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, // Create new process group } // Create buffers to capture stdout and stderr separately @@ -120,6 +151,12 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute // Context timeout or other error if cmdCtx.Err() == context.DeadlineExceeded { errorMsg = "Command timed out" + + // Kill the entire process group on timeout + if cmd.Process != nil { + pgid, _ := syscall.Getpgid(cmd.Process.Pid) + syscall.Kill(-pgid, syscall.SIGKILL) + } } exitCode = -1 } @@ -161,7 +198,7 @@ func main() { // Add tool for executing shell scripts mcp.AddTool(server, &mcp.Tool{ Name: configurableName, - Description: "Execute a shell script and return the output, exit code, and any errors. The shell command can be configured via SHELL_CMD environment variable (default: 'sh -c'). The working directory can be set via SHELL_WORKING_DIR environment variable. The default timeout can be configured via SHELL_TIMEOUT environment variable (default: 30 seconds). An initialization script can be run before server startup via SHELL_INIT_SCRIPT environment variable.", + Description: "Execute a shell script and return the output, exit code, and any errors. The shell command can be configured via SHELL_CMD environment variable (default: 'sh -c'). The working directory can be set via SHELL_WORKING_DIR environment variable. The default timeout can be set via SHELL_TIMEOUT environment variable (default: 30 seconds). An initialization script can be run before server startup via SHELL_INIT_SCRIPT environment variable. Interactive commands are detected and warned about.", }, ExecuteCommand) // Run the server