diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b3d1256 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules/ +build/ +.git/ +.github/ +test/ +images/ +*.md +.gitignore +.dockerignore +install-systemd.sh +run-service.sh +skills/ +docs/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..2cb8d53 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,65 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU (multi-platform) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix=sha-,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93033a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Stage 1: build +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY tsconfig.json ./ +COPY src/ ./src/ +COPY scripts/ ./scripts/ + +RUN npm run build + +# Stage 2: runtime +FROM node:22-alpine AS runner + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY --from=builder /app/build ./build +COPY docker-entrypoint.sh ./ + +RUN chmod +x docker-entrypoint.sh + +# HTTP transport env vars +ENV HTTP_HOST=0.0.0.0 +ENV HTTP_PORT=3001 +ENV HTTP_PATH=/mcp +# HTTP_API_KEY — set at runtime, do not hardcode +# SSH_CONFIG_FILE — path inside container to mounted JSON config + +EXPOSE 3001 + +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/README.md b/README.md index ae7a0ca..cb61843 100644 --- a/README.md +++ b/README.md @@ -561,6 +561,133 @@ Options: --pre-connect Pre-connect to all configured SSH servers on startup ``` +## 🐳 Running with Docker (HTTP Transport) + +The pre-built Docker image is published to GitHub Container Registry on every push to `main` and for every version tag. + +### Pull the image + +```bash +# Latest build from main +docker pull ghcr.io/someblackmagic/ssh-mcp-server:latest + +# Specific release +docker pull ghcr.io/someblackmagic/ssh-mcp-server:1.8.2 +``` + +### Step 1 — Create an SSH server config file + +Create `ssh-config.json` on the host machine (it will be mounted read-only into the container): + +```json +[ + { + "name": "dev", + "host": "192.168.1.10", + "port": 22, + "username": "alice", + "privateKey": "/root/.ssh/id_rsa", + "commandWhitelist": ["^ls", "^cat ", "^df", "^free", "^ps"] + }, + { + "name": "prod", + "host": "10.0.0.5", + "port": 22, + "username": "deploy", + "password": "secret", + "commandWhitelist": ["^ls", "^systemctl status"] + } +] +``` + +> When `privateKey` is used, the path must match the location **inside** the container. Mount your `~/.ssh` directory to `/root/.ssh` (see the `docker run` example below). + +### Step 2 — Start the container + +```bash +docker run -d \ + --name ssh-mcp-server \ + -p 3001:3001 \ + -e HTTP_API_KEY=change-me-to-a-strong-secret \ + -v "$HOME/.ssh:/root/.ssh:ro" \ + -v "$(pwd)/ssh-config.json:/app/ssh-config.json:ro" \ + -e SSH_CONFIG_FILE=/app/ssh-config.json \ + ghcr.io/someblackmagic/ssh-mcp-server:latest +``` + +Environment variables: + +| Variable | Default | Description | +|---|---|---| +| `HTTP_HOST` | `0.0.0.0` | Bind address | +| `HTTP_PORT` | `3001` | HTTP port | +| `HTTP_PATH` | `/mcp` | MCP endpoint path | +| `HTTP_API_KEY` | *(empty)* | Bearer token for auth — set this in production | +| `SSH_CONFIG_FILE` | *(empty)* | Absolute path inside the container to the JSON config file | + +Check that the server is ready: + +```bash +docker logs ssh-mcp-server +# MCP HTTP server listening on http://0.0.0.0:3001/mcp +``` + +### Step 3 — Connect an MCP client over HTTP + +**Claude Desktop** (`claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "ssh-mcp-server": { + "url": "http://localhost:3001/mcp", + "headers": { + "Authorization": "Bearer change-me-to-a-strong-secret" + } + } + } +} +``` + +**Claude Code** (`.mcp.json` or `~/.claude.json`): + +```json +{ + "mcpServers": { + "ssh-mcp-server": { + "type": "http", + "url": "http://localhost:3001/mcp", + "headers": { + "Authorization": "Bearer change-me-to-a-strong-secret" + } + } + } +} +``` + +### Docker Compose example + +```yaml +services: + ssh-mcp-server: + image: ghcr.io/someblackmagic/ssh-mcp-server:latest + restart: unless-stopped + ports: + - "3001:3001" + environment: + HTTP_API_KEY: change-me-to-a-strong-secret + SSH_CONFIG_FILE: /app/ssh-config.json + volumes: + - ~/.ssh:/root/.ssh:ro + - ./ssh-config.json:/app/ssh-config.json:ro +``` + +```bash +docker compose up -d +``` + +--- + ## 🛡️ Security Considerations This server provides powerful capabilities to execute commands and transfer files on remote servers. To ensure it is used securely, please consider the following: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..3ac39f2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +set -- \ + "--mcp-transport" "http" \ + "--http-host" "${HTTP_HOST:-0.0.0.0}" \ + "--http-port" "${HTTP_PORT:-3001}" \ + "--http-path" "${HTTP_PATH:-/mcp}" + +if [ -n "${HTTP_API_KEY:-}" ]; then + set -- "$@" "--http-api-key" "${HTTP_API_KEY}" +fi + +if [ -n "${SSH_CONFIG_FILE:-}" ]; then + set -- "$@" "--config-file" "${SSH_CONFIG_FILE}" +fi + +exec node build/index.js "$@"