diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 8847525..4a24ca2 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: ./ @@ -60,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 4b6959e..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. @@ -101,6 +139,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. @@ -1471,45 +1559,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 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"] 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 diff --git a/shell/Dockerfile b/shell/Dockerfile index 6d34caf..0633674 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 \ @@ -23,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 diff --git a/shell/main.go b/shell/main.go index 2b0b5ff..86f3cc7 100644 --- a/shell/main.go +++ b/shell/main.go @@ -6,7 +6,9 @@ import ( "log" "os" "os/exec" + "strconv" "strings" + "syscall" "time" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -43,6 +45,41 @@ 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 +} + +// 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, @@ -52,7 +89,12 @@ 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() + } + + // 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 @@ -79,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 @@ -105,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 } @@ -125,14 +177,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 @@ -149,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. 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 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) + } +}