diff --git a/Makefile b/Makefile index e2ef23e72..4da92a92b 100644 --- a/Makefile +++ b/Makefile @@ -207,12 +207,12 @@ run: build ## docker-build: Build Docker image (minimal Alpine-based) docker-build: @echo "Building minimal Docker image (Alpine-based)..." - docker compose build picoclaw-agent picoclaw-gateway + docker compose -f docker/docker-compose.yml build picoclaw-agent picoclaw-gateway ## docker-build-full: Build Docker image with full MCP support (Node.js 24) docker-build-full: @echo "Building full-featured Docker image (Node.js 24)..." - docker compose -f docker-compose.full.yml build picoclaw-agent picoclaw-gateway + docker compose -f docker/docker-compose.full.yml build picoclaw-agent picoclaw-gateway ## docker-test: Test MCP tools in Docker container docker-test: @@ -222,24 +222,24 @@ docker-test: ## docker-run: Run picoclaw gateway in Docker (Alpine-based) docker-run: - docker compose --profile gateway up + docker compose -f docker/docker-compose.yml --profile gateway up ## docker-run-full: Run picoclaw gateway in Docker (full-featured) docker-run-full: - docker compose -f docker-compose.full.yml --profile gateway up + docker compose -f docker/docker-compose.full.yml --profile gateway up ## docker-run-agent: Run picoclaw agent in Docker (interactive, Alpine-based) docker-run-agent: - docker compose run --rm picoclaw-agent + docker compose -f docker/docker-compose.yml run --rm picoclaw-agent ## docker-run-agent-full: Run picoclaw agent in Docker (interactive, full-featured) docker-run-agent-full: - docker compose -f docker-compose.full.yml run --rm picoclaw-agent + docker compose -f docker/docker-compose.full.yml run --rm picoclaw-agent ## docker-clean: Clean Docker images and volumes docker-clean: - docker compose down -v - docker compose -f docker-compose.full.yml down -v + docker compose -f docker/docker-compose.yml down -v + docker compose -f docker/docker-compose.full.yml down -v docker rmi picoclaw:latest picoclaw:full 2>/dev/null || true ## help: Show this help message diff --git a/config/config.example.json b/config/config.example.json index 762bb90aa..c4f901caf 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -279,6 +279,18 @@ "@modelcontextprotocol/server-postgres", "postgresql://user:password@localhost/dbname" ] + }, + "slack": { + "enabled": false, + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-slack" + ], + "env": { + "SLACK_BOT_TOKEN": "YOUR_SLACK_BOT_TOKEN", + "SLACK_TEAM_ID": "YOUR_SLACK_TEAM_ID" + } } } }, diff --git a/Dockerfile.full b/docker/Dockerfile.full similarity index 100% rename from Dockerfile.full rename to docker/Dockerfile.full diff --git a/docker-compose.full.yml b/docker/docker-compose.full.yml similarity index 78% rename from docker-compose.full.yml rename to docker/docker-compose.full.yml index ff2694173..6f34448c4 100644 --- a/docker-compose.full.yml +++ b/docker/docker-compose.full.yml @@ -1,17 +1,17 @@ services: # ───────────────────────────────────────────── # PicoClaw Agent (one-shot query) - Full MCP Support - # docker compose -f docker-compose.full.yml run --rm picoclaw-agent -m "Hello" + # docker compose -f docker/docker-compose.full.yml run --rm picoclaw-agent -m "Hello" # ───────────────────────────────────────────── picoclaw-agent: build: - context: . - dockerfile: Dockerfile.full + context: .. + dockerfile: docker/Dockerfile.full container_name: picoclaw-agent-full profiles: - agent volumes: - - ./config/config.json:/root/.picoclaw/config.json:ro + - ../config/config.json:/root/.picoclaw/config.json:ro - picoclaw-workspace:/root/.picoclaw/workspace - picoclaw-npm-cache:/root/.npm # npm cache for faster MCP server installs entrypoint: ["picoclaw", "agent"] @@ -20,19 +20,19 @@ services: # ───────────────────────────────────────────── # PicoClaw Gateway (Long-running Bot) - Full MCP Support - # docker compose -f docker-compose.full.yml --profile gateway up + # docker compose -f docker/docker-compose.full.yml --profile gateway up # ───────────────────────────────────────────── picoclaw-gateway: build: - context: . - dockerfile: Dockerfile.full + context: .. + dockerfile: docker/Dockerfile.full container_name: picoclaw-gateway-full restart: unless-stopped profiles: - gateway volumes: # Configuration file - - ./config/config.json:/root/.picoclaw/config.json:ro + - ../config/config.json:/root/.picoclaw/config.json:ro # Persistent workspace (sessions, memory, logs) - picoclaw-workspace:/root/.picoclaw/workspace # NPM cache for faster MCP server installs diff --git a/pkg/mcp/manager.go b/pkg/mcp/manager.go index e79c9d3f8..8b6d6d9aa 100644 --- a/pkg/mcp/manager.go +++ b/pkg/mcp/manager.go @@ -243,9 +243,9 @@ func (m *Manager) ConnectServer( ) error { logger.InfoCF("mcp", "Connecting to MCP server", map[string]any{ - "server": name, - "command": cfg.Command, - "args": cfg.Args, + "server": name, + "command": cfg.Command, + "args_count": len(cfg.Args), }) // Create client diff --git a/pkg/tools/mcp_tool.go b/pkg/tools/mcp_tool.go index 69615b4bc..1bdf248f7 100644 --- a/pkg/tools/mcp_tool.go +++ b/pkg/tools/mcp_tool.go @@ -35,10 +35,67 @@ func NewMCPTool(manager MCPManager, serverName string, tool *mcp.Tool) *MCPTool } } +// sanitizeIdentifierComponent normalizes a string so it can be safely used +// as part of a tool/function identifier for downstream providers. +// It: +// - lowercases the string +// - replaces any character not in [a-z0-9_-] with '_' +// - collapses multiple consecutive '_' into a single '_' +// - trims leading/trailing '_' +// - falls back to "unnamed" if the result is empty +// - truncates overly long components to a reasonable length +func sanitizeIdentifierComponent(s string) string { + const maxLen = 64 + + s = strings.ToLower(s) + var b strings.Builder + b.Grow(len(s)) + + prevUnderscore := false + for _, r := range s { + isAllowed := (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '_' || r == '-' + + if !isAllowed { + // Normalize any disallowed character to '_' + if !prevUnderscore { + b.WriteRune('_') + prevUnderscore = true + } + continue + } + + if r == '_' { + if prevUnderscore { + continue + } + prevUnderscore = true + } else { + prevUnderscore = false + } + + b.WriteRune(r) + } + + result := strings.Trim(b.String(), "_") + if result == "" { + result = "unnamed" + } + + if len(result) > maxLen { + result = result[:maxLen] + } + + return result +} + // Name returns the tool name, prefixed with the server name func (t *MCPTool) Name() string { - // Prefix with server name to avoid conflicts - return fmt.Sprintf("mcp_%s_%s", t.serverName, t.tool.Name) + // Prefix with server name to avoid conflicts, and sanitize components + sanitizedServer := sanitizeIdentifierComponent(t.serverName) + sanitizedTool := sanitizeIdentifierComponent(t.tool.Name) + return fmt.Sprintf("mcp_%s_%s", sanitizedServer, sanitizedTool) } // Description returns the tool description diff --git a/scripts/test-docker-mcp.sh b/scripts/test-docker-mcp.sh index 5c4e5cb56..7ad8640f2 100755 --- a/scripts/test-docker-mcp.sh +++ b/scripts/test-docker-mcp.sh @@ -3,7 +3,7 @@ set -e -COMPOSE_FILE="docker-compose.full.yml" +COMPOSE_FILE="docker/docker-compose.full.yml" SERVICE="picoclaw-agent" echo "🧪 Testing MCP tools in Docker container (full-featured image)..." @@ -38,8 +38,8 @@ echo "✅ Testing uv..." docker compose -f "$COMPOSE_FILE" run --rm --entrypoint sh "$SERVICE" -c 'uv --version' # Test MCP server installation (quick) -echo "✅ Testing MCP server install with npx..." -docker compose -f "$COMPOSE_FILE" run --rm --entrypoint sh "$SERVICE" -c 'npx -y cowsay "MCP works!"' +echo "✅ Testing @modelcontextprotocol/server-filesystem MCP server install with npx..." +docker compose -f "$COMPOSE_FILE" run --rm --entrypoint sh "$SERVICE" -c 'npx -y @modelcontextprotocol/server-filesystem --help' echo "" echo "🎉 All MCP tools are working correctly!"