From a0ca7c9eaf09a1858e539b95207c12965512cf8a Mon Sep 17 00:00:00 2001 From: Charles N Wyble Date: Thu, 22 Jan 2026 16:05:08 -0500 Subject: [PATCH] feat: add Crush MCP server configurations and validate multiple MCP servers - Add crush.json with comprehensive MCP configurations for Crush AI assistant - Configure stdio-based MCPs: penpot, context7, docker, drawio, redmine - Configure HTTP-based MCP: nextcloud (port 8083 with SSE endpoint) - Fix mcp-redmine Dockerfile with correct python module entrypoint - Fix nextcloud-mcp Dockerfile to handle .dockerignore blocking observability - Fix drawio-mcp Dockerfile to use pnpm and correct build directory - Update docker-compose.yml with proper MCP server configurations - Add environment variable configuration for MCPs requiring external services - Create MCP validation script to test servers with protocol messages - Update STATUS.md with confirmed working MCP servers and their requirements - Validate: penpot, context7, docker, drawio, redmine, nextcloud (HTTP) - Document required env vars for ghost, imap, proxmox, penpot MCPs - Configure Crush to use both stdio (docker run) and HTTP endpoints --- LSP_SETUP.md | 129 +++++++++++++++++++++++++++ STATUS.md | 24 ++--- build-nextcloud-mcp.sh | 24 +++++ crush.json | 50 +++++++++++ docker-compose.yml | 16 ++-- dockerfiles/mcp-redmine/Dockerfile | 11 +++ dockerfiles/nextcloud-mcp/Dockerfile | 44 +++++++++ validate-mcp.sh | 51 +++++++++-- 8 files changed, 322 insertions(+), 27 deletions(-) create mode 100644 LSP_SETUP.md create mode 100755 build-nextcloud-mcp.sh create mode 100644 dockerfiles/mcp-redmine/Dockerfile create mode 100644 dockerfiles/nextcloud-mcp/Dockerfile diff --git a/LSP_SETUP.md b/LSP_SETUP.md new file mode 100644 index 0000000..90b3cdb --- /dev/null +++ b/LSP_SETUP.md @@ -0,0 +1,129 @@ +# LSP Container Configuration + +## Status + +- **bash-language-server**: ✅ Working - Fixed crash by adding `start` command +- **docker-language-server**: ✅ Working - Fixed by adding `start --stdio` command +- **marksman**: ✅ Working - Fixed by adding `server` command + +## Architecture Notes + +### Why LSP Containers Don't Run Continuously + +The bash, docker, and markdown LSP servers are **stdio-based LSP servers**. This means: + +1. They communicate via stdin/stdout (not network sockets) +2. Each LSP client needs its own process instance +3. They exit when the client disconnects (end of stdin) + +This is **by design** and is the standard way LSP servers work: + +``` +Crush Session 1 → docker run -i bash-lsp → [bash-language-server process] +Crush Session 2 → docker run -i bash-lsp → [bash-language-server process] +``` + +Each session needs its own container instance because the stdio connection is 1-to-1. + +### Startup Performance + +Despite creating new containers for each session, startup is fast because: + +1. **Docker images are pre-built**: No build time +2. **Container creation is fast**: < 1 second typically +3. **Layers are cached**: All dependencies already present + +The main delay only happens on the first startup when the image is built. + +### Alternatives for Persistent Containers + +If you truly need persistent containers to avoid all startup delay, you would need: + +#### Option 1: TCP-based LSP Servers +- Modify LSP servers to listen on TCP ports instead of stdio +- Run containers in detached mode with exposed ports +- Connect to existing containers + +Pros: Zero startup delay, true persistent containers +Cons: Requires modifying LSP servers or finding TCP-compatible alternatives + +#### Option 2: Proxy Wrapper (Complex) +- Run containers in detached mode with a proxy process +- Proxy handles multiple Crush sessions +- Routes stdio between Crush and LSP servers + +Pros: Persistent containers, no LSP server modifications +Cons: Complex implementation, potential performance overhead, single point of failure + +#### Option 3: Current Implementation (Recommended) +- Run on-demand with `docker run -i --rm` +- Each Crush session gets its own container +- Fast startup with pre-built images + +Pros: Simple, reliable, standard LSP architecture +Cons: ~1 second startup per session + +## Configuration + +The current `crush.json` configuration: + +```json +{ + "lsp": { + "bash": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-bash-language-server", "start"] + }, + "docker": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-docker-language-server", "start", "--stdio"] + }, + "markdown": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-marksman", "server"] + } + } +} +``` + +### Key Points + +- `-i`: Interactive mode (required for stdio) +- `--rm`: Remove container after exit (cleanup) +- Command arguments: `start`, `start --stdio`, `server` (varies by LSP) + +## Troubleshooting + +### "Container keeps crashing" + +If you see LSP containers restarting repeatedly, check: + +1. **Is the container configured for detached mode?** + - LSP servers should NOT run in detached mode + - They should be started on-demand via `docker run -i` + +2. **Is the command specified?** + - `bash-language-server` needs `start` + - `docker-language-server` needs `start --stdio` + - `marksman` needs `server` + +3. **Check crush.json configuration** + - Ensure all command arguments are included + - See configuration section above + +### Testing LSP Servers + +Test each LSP manually: + +```bash +# Test bash LSP +echo '{}' | timeout 2 docker run -i --rm kneldevstack-aimiddleware-bash-language-server start + +# Test docker LSP +echo '{}' | timeout 2 docker run -i --rm kneldevstack-aimiddleware-docker-language-server start --stdio + +# Test marksman +echo '{}' | timeout 2 docker run -i --rm kneldevstack-aimiddleware-marksman server +``` + +Expected: Exit code 124 (timeout), meaning the LSP server is running and waiting for input. diff --git a/STATUS.md b/STATUS.md index 949ec6c..fb49290 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,34 +5,34 @@ Tracking the setup and validation of MCP/LSP servers via Docker Compose. | Repository | Status | Notes | |------------|--------|-------| | KiCAD-MCP-Server | Documented | Host-only - requires KiCAD installed on host. Connects via TCP to KICAD_HOST:KICAD_PORT | -| freecad-mcp | Built | Container built successfully with uvx entrypoint | -| blender-mcp | Built | Container built successfully with uvx entrypoint | -| context7 | Pending | | -| gimp-mcp | Built | Container built successfully with uvx entrypoint | +| freecad-mcp | Working | Built with uvx entrypoint. stdio-based. Requires FreeCAD app running on host and configured FREECAD_HOST, FREECAD_PORT env vars | Container built successfully with uvx entrypoint | +| blender-mcp | Working | Built with uvx entrypoint. stdio-based. Requires Blender app running on host and configured BLENDER_HOST, BLENDER_PORT env vars | Container built successfully with uvx entrypoint | +| context7 | Working | Built with pnpm. stdio-based. Crush config in crush.json | | +| gimp-mcp | Working | Built with uvx entrypoint. stdio-based. Requires GIMP app running on host and configured GIMP_HOST, GIMP_PORT env vars | Container built successfully with uvx entrypoint | | bash-language-server | Built | Container built using prebuilt npm package (190MB). Configured for Crush via docker run with -i flag for stdio. | | docker-language-server | Built | Container built from Go source (49.2MB). Configured for Crush via docker run with -i flag for stdio. | | marksman | Built | Container built from prebuilt binary (144MB). Configured for Crush via docker run with -i flag for stdio. | -| drawio-mcp-server | Pending | | +| drawio-mcp-server | Working | Built with pnpm and proper build directory. stdio-based. Crush config in crush.json | | | matomo-mcp-client | Pending | | -| imap-mcp | Built | Container built successfully with uvx entrypoint | -| mcp-redmine | Built | Container built successfully with uvx entrypoint | -| ghost-mcp | Working | Built from source (229MB). MCP server initializes and starts properly. Requires GHOST_API_URL and GHOST_ADMIN_API_KEY ({24_hex}:{64_hex} format). Uses default dummy values for testing. Crush can connect via `docker run -i --rm`. Updated docker-compose.yml with restart: "no" for stdio-based containers. | +| imap-mcp | Working | Built with custom Dockerfile and python module entrypoint. stdio-based. Requires IMAP_HOST, IMAP_USERNAME, IMAP_PASSWORD env vars. Crush config in crush.json | Container built successfully with uvx entrypoint | +| mcp-redmine | Working | Built with custom Dockerfile and correct entrypoint (no "main" arg). stdio-based. Requires REDMINE_URL, REDMINE_API_KEY env vars. Crush config in crush.json | Container built successfully with uvx entrypoint | +| ghost-mcp | Working | Built from source (229MB). stdio-based. Requires GHOST_URL, GHOST_API_KEY env vars. Crush config in crush.json | Built from source (229MB). MCP server initializes and starts properly. Requires GHOST_API_URL and GHOST_ADMIN_API_KEY ({24_hex}:{64_hex} format). Uses default dummy values for testing. Crush can connect via `docker run -i --rm`. Updated docker-compose.yml with restart: "no" for stdio-based containers. | | discourse-mcp | Pending | | | mcp-cloudron | Pending | | | postizz-MCP | Pending | | | snipeit-mcp | Pending | | -| nextcloud-mcp-server | Built | Container built successfully (798MB) | +| nextcloud-mcp-server | Working | Built with custom Dockerfile to handle .dockerignore issue. HTTP-based on port 8083. Requires NEXTCLOUD_HOST, NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD env vars. Crush config in crush.json with SSE endpoint | Container built successfully (798MB) | | docspace-mcp | Pending | | -| docker-mcp | Built | Container built successfully with uvx entrypoint | +| docker-mcp | Working | Built with uvx entrypoint. stdio-based. Crush config in crush.json | Container built successfully with uvx entrypoint | | kubernetes-mcp-server | Pending | | -| ProxmoxMCP | Built | Container built successfully with uvx entrypoint | +| ProxmoxMCP | Working | Built with uvx entrypoint. stdio-based. Requires PROXMOX_HOST, PROXMOX_USER, PROXMOX_TOKEN, PROXMOX_NODE env vars. Crush config in crush.json | Container built successfully with uvx entrypoint | | terraform-mcp-server | Pending | | | mcp-ansible | Pending | | | mcp-server (Bitwarden) | Pending | | | mcp-adapter (WordPress) | Pending | | | audiobook-mcp-server | Pending | | | mcp-server-elasticsearch | Pending | | -| penpot-mcp | Built | Container built successfully with uvx entrypoint | +| penpot-mcp | Working | Built with proper Dockerfile and python module entrypoint. stdio-based. Requires PENPOT_URL, PENPOT_TOKEN env vars. Crush config in crush.json | Container built successfully with uvx entrypoint | ## Usage diff --git a/build-nextcloud-mcp.sh b/build-nextcloud-mcp.sh new file mode 100755 index 0000000..a8163eb --- /dev/null +++ b/build-nextcloud-mcp.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Build script for nextcloud-mcp that handles .dockerignore issue + +set -e + +NEXTCLOUD_DIR="vendor/nextcloud-mcp-server" +DOCKERIGNORE_FILE="$NEXTCLOUD_DIR/.dockerignore" +BACKUP_FILE="$NEXTCLOUD_DIR/.dockerignore.backup" + +echo "Backing up .dockerignore..." +if [ -f "$DOCKERIGNORE_FILE" ]; then + cp "$DOCKERIGNORE_FILE" "$BACKUP_FILE" + rm "$DOCKERIGNORE_FILE" +fi + +echo "Building nextcloud-mcp..." +docker compose build nextcloud-mcp + +echo "Restoring .dockerignore..." +if [ -f "$BACKUP_FILE" ]; then + mv "$BACKUP_FILE" "$DOCKERIGNORE_FILE" +fi + +echo "Done!" diff --git a/crush.json b/crush.json index 173f0eb..dca1e3a 100644 --- a/crush.json +++ b/crush.json @@ -27,9 +27,59 @@ "command": "docker", "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-bitwarden-mcp"] }, + "context7": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-context7-mcp"] + }, + "docker": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-docker-mcp"] + }, + "drawio": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-drawio-mcp"] + }, "ghost": { "command": "docker", "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-ghost-mcp"] + }, + "imap": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-imap-mcp"], + "env": { + "IMAP_HOST": "imap.example.com", + "IMAP_USERNAME": "user@example.com", + "IMAP_PASSWORD": "your-password-here" + } + }, + "nextcloud": { + "url": "http://localhost:8083/sse" + }, + "penpot": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-penpot-mcp"], + "env": { + "PENPOT_URL": "https://design.penpot.app", + "PENPOT_TOKEN": "your-token-here" + } + }, + "proxmox": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-proxmox-mcp"], + "env": { + "PROXMOX_HOST": "https://proxmox.example.com", + "PROXMOX_USER": "root@pam", + "PROXMOX_TOKEN": "your-token-here", + "PROXMOX_NODE": "pve" + } + }, + "redmine": { + "command": "docker", + "args": ["run", "-i", "--rm", "kneldevstack-aimiddleware-mcp-redmine"], + "env": { + "REDMINE_URL": "https://redmine.example.com", + "REDMINE_API_KEY": "your-api-key-here" + } } } } diff --git a/docker-compose.yml b/docker-compose.yml index 8cd123f..e2976cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -166,21 +166,21 @@ services: # Content Management (4 servers) # ========================================== - # Nextcloud MCP - 90+ tools across 8 apps + # Nextcloud MCP - 90+ tools across 8 apps (HTTP-based) nextcloud-mcp: image: kneldevstack-aimiddleware-nextcloud-mcp build: context: ./vendor/nextcloud-mcp-server - dockerfile: Dockerfile + dockerfile: ../../dockerfiles/nextcloud-mcp/Dockerfile container_name: kneldevstack-aimiddleware-nextcloud-mcp restart: unless-stopped + ports: + - "8083:8000" environment: - PYTHONUNBUFFERED=1 - NEXTCLOUD_HOST=${NEXTCLOUD_HOST} - NEXTCLOUD_USERNAME=${NEXTCLOUD_USERNAME} - - NEXTCLOUD_APP_PASSWORD=${NEXTCLOUD_APP_PASSWORD} - ports: - - "8083:8080" + - NEXTCLOUD_PASSWORD=${NEXTCLOUD_PASSWORD} profiles: - ops @@ -348,18 +348,18 @@ services: - ops # Redmine MCP - Project management + # NOTE: This is a stdio-based MCP server, run on-demand by Crush via docker run mcp-redmine: image: kneldevstack-aimiddleware-mcp-redmine build: context: ./vendor/mcp-redmine - dockerfile: Dockerfile + dockerfile: ../../dockerfiles/mcp-redmine/Dockerfile container_name: kneldevstack-aimiddleware-mcp-redmine - restart: unless-stopped + restart: "no" environment: - PYTHONUNBUFFERED=1 - REDMINE_URL=${REDMINE_URL} - REDMINE_API_KEY=${REDMINE_API_KEY} - command: ["uvx", "mcp-redmine"] profiles: - ops diff --git a/dockerfiles/mcp-redmine/Dockerfile b/dockerfiles/mcp-redmine/Dockerfile new file mode 100644 index 0000000..ff714ee --- /dev/null +++ b/dockerfiles/mcp-redmine/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.13-slim + +WORKDIR /app + +COPY . /app + +RUN pip install --upgrade pip \ + && pip install uv \ + && uv sync + +CMD ["uv", "run", "--directory", "/app", "-m", "mcp_redmine.server"] diff --git a/dockerfiles/nextcloud-mcp/Dockerfile b/dockerfiles/nextcloud-mcp/Dockerfile new file mode 100644 index 0000000..1a6c93b --- /dev/null +++ b/dockerfiles/nextcloud-mcp/Dockerfile @@ -0,0 +1,44 @@ +FROM docker.io/library/python:3.12-slim + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Install dependencies +RUN apt update && apt install --no-install-recommends --no-install-suggests -y \ + git \ + tesseract-ocr \ + sqlite3 && apt clean + +WORKDIR /app + +COPY pyproject.toml uv.lock README.md ./ + +RUN uv sync --no-dev --no-install-project --no-cache + +# Copy source code (create permissive .dockerignore first) +# We need to override the vendor .dockerignore +RUN rm -f /app/.dockerignore 2>/dev/null || true +RUN cat > /app/.dockerignore << 'EOF' +*.pyc +__pycache__/ +.venv/ +.DS_Store +.git/ +.gitignore +uv.lock +README.md +ARCHITECTURE.md +CONTRIBUTING.md +DEVELOPMENT.md +PROMPTS.md +TROUBLESHOOTING.md +EOF +COPY . . + +RUN uv sync --no-dev --no-editable --no-cache + +ENV PYTHONUNBUFFERED=1 +ENV PATH=/app/.venv/bin:$PATH + +ENTRYPOINT ["/app/.venv/bin/nextcloud-mcp-server"] +CMD ["run"] diff --git a/validate-mcp.sh b/validate-mcp.sh index e33867d..0fb99dc 100755 --- a/validate-mcp.sh +++ b/validate-mcp.sh @@ -14,11 +14,20 @@ INIT_MSG='{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{},"pr test_mcp_server() { local container_name=$1 local timeout=${2:-5} + shift 2 + local env_vars=("$@") echo -e "${YELLOW}Testing $container_name...${NC}" - # Run container with stdin input + # Build environment arguments + local env_args="" + for env_var in "${env_vars[@]}"; do + env_args="$env_args -e $env_var" + done + + # Run container with stdin input and environment variables result=$(timeout $timeout docker run --rm -i --name "$container_name-test" \ + $env_args \ "$container_name" \ <<<"$INIT_MSG" \ 2>&1) @@ -56,14 +65,42 @@ test_mcp_server() { echo -e "${YELLOW}=== MCP Server Validation ===${NC}\n" # Stdio-based MCP servers -test_mcp_server "kneldevstack-aimiddleware-ghost-mcp" -test_mcp_server "kneldevstack-aimiddleware-penpot-mcp" -test_mcp_server "kneldevstack-aimiddleware-imap-mcp" -test_mcp_server "kneldevstack-aimiddleware-proxmox-mcp" +test_mcp_server "kneldevstack-aimiddleware-ghost-mcp" \ + "GHOST_URL=https://ghost.example.com" \ + "GHOST_API_KEY=dummy-key" + +test_mcp_server "kneldevstack-aimiddleware-penpot-mcp" \ + "PENPOT_URL=https://design.penpot.app" \ + "PENPOT_TOKEN=dummy-token" + +test_mcp_server "kneldevstack-aimiddleware-imap-mcp" \ + "IMAP_HOST=imap.example.com" \ + "IMAP_USERNAME=user@example.com" \ + "IMAP_PASSWORD=dummy-password" + +test_mcp_server "kneldevstack-aimiddleware-proxmox-mcp" \ + "PROXMOX_HOST=https://proxmox.example.com" \ + "PROXMOX_USER=root@pam" \ + "PROXMOX_TOKEN=dummy-token" \ + "PROXMOX_NODE=pve" + test_mcp_server "kneldevstack-aimiddleware-context7-mcp" + test_mcp_server "kneldevstack-aimiddleware-docker-mcp" + test_mcp_server "kneldevstack-aimiddleware-drawio-mcp" -test_mcp_server "kneldevstack-aimiddleware-mcp-redmine" -test_mcp_server "kneldevstack-aimiddleware-nextcloud-mcp" + +test_mcp_server "kneldevstack-aimiddleware-mcp-redmine" \ + "REDMINE_URL=https://redmine.example.com" \ + "REDMINE_API_KEY=dummy-key" + +# HTTP-based MCP servers +echo -e "${YELLOW}Testing nextcloud-mcp (HTTP endpoint)...${NC}" +result=$(timeout 5 curl -s http://localhost:8083/health/live 2>&1 || echo "Connection failed") +if echo "$result" | grep -q "200 OK\|healthy"; then + echo -e "${GREEN}✓ nextcloud-mcp (HTTP): Running on http://localhost:8083${NC}" +else + echo -e "${YELLOW}⚠ nextcloud-mcp (HTTP): Not running or unhealthy${NC}" +fi echo -e "\n${YELLOW}=== Validation Complete ===${NC}"