diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..d15ba1e --- /dev/null +++ b/.env.docker @@ -0,0 +1,28 @@ +# Plane MCP Server — Docker development environment template +# Copy this file to .env and fill in your values: +# cp .env.docker .env + +# ------------------------------------------------------------------- +# Plane API +# ------------------------------------------------------------------- +PLANE_BASE_URL=https://api.plane.so + +# Internal URL for server-to-server calls (optional; leave blank for cloud) +PLANE_INTERNAL_BASE_URL= + +# ------------------------------------------------------------------- +# OAuth Provider (required for OAuth flow via /mcp) +# For local dev without OAuth, use dummy values below — the server will +# start and the header API-key endpoint (/http/api-key/mcp) will work. +# Real OAuth flow requires valid credentials from your Plane OAuth app. +# ------------------------------------------------------------------- +PLANE_OAUTH_PROVIDER_CLIENT_ID=dev +PLANE_OAUTH_PROVIDER_CLIENT_SECRET=dev +# Should match the public URL your MCP clients reach the server on +PLANE_OAUTH_PROVIDER_BASE_URL=http://localhost:8211 + +# ------------------------------------------------------------------- +# Redis — set automatically by docker-compose; override only if needed +# ------------------------------------------------------------------- +# REDIS_HOST=redis +# REDIS_PORT=6379 diff --git a/.gitignore b/.gitignore index 88f9cb1..8f85fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ dmypy.json .env .env.local .env.test.local +.env.docker.local +deployments/variables.env.local # Ignore cursor AI rules .cursor/rules/codacy.mdc diff --git a/Dockerfile b/Dockerfile index 97e65f9..ce7707d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,8 @@ FROM python:3.11-slim # Set working directory WORKDIR /app -# Install system dependencies -RUN apt-get update && apt-get install -y \ - curl \ - && rm -rf /var/lib/apt/lists/* - # Install uv for faster package management -COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN pip install --no-cache-dir uv # Copy dependency files and application code COPY pyproject.toml ./ diff --git a/README.md b/README.md index 24392f9..dfd9579 100644 --- a/README.md +++ b/README.md @@ -226,18 +226,84 @@ The server provides comprehensive tools for interacting with Plane. All tools us **Total Tools**: 55+ tools across 8 categories -## Development +## Self-Hosting + +Use the production compose setup in the `deployments/` folder. It pulls the published image — no build required. + +```bash +cd deployments +# edit variables.env with your values +docker compose up -d +``` + +The server will be available at: +- `http://localhost:8211/http/mcp` — OAuth endpoint +- `http://localhost:8211/http/api-key/mcp` — PAT / header API key endpoint + +To pin a specific version, set `APP_RELEASE_VERSION` in `variables.env`: +```env +APP_RELEASE_VERSION=v0.2.8 +``` + +--- + +## Local Development + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and Docker Compose +- [uv](https://docs.astral.sh/uv/) (for running outside Docker) + +### Running with Docker Compose (recommended) + +```bash +# 1. Clone the repo +git clone https://github.com/makeplane/plane-mcp-server.git +cd plane-mcp-server + +# 2. Create your local env file +cp .env.docker .env +# Edit .env and fill in PLANE_OAUTH_PROVIDER_* values + +# 3. Start the server and Redis +docker compose up --build +``` + +The server starts in HTTP mode at `http://localhost:8211`. +Source code is mounted into the container — changes to `plane_mcp/` take effect on container restart (`docker compose restart mcp`). + +### Running without Docker + +```bash +# Install dependencies +uv pip install -e ".[dev]" + +# Stdio mode (simplest — no Redis or OAuth needed) +export PLANE_API_KEY=your-api-key +export PLANE_WORKSPACE_SLUG=your-workspace-slug +python -m plane_mcp stdio + +# HTTP mode +export PLANE_OAUTH_PROVIDER_CLIENT_ID=... +export PLANE_OAUTH_PROVIDER_CLIENT_SECRET=... +export PLANE_OAUTH_PROVIDER_BASE_URL=http://localhost:8211 +python -m plane_mcp http +``` ### Running Tests ```bash -pytest +# Copy and fill in test env +cp .env.test .env.test.local +# Edit .env.test.local with your Plane API key and workspace slug + +export $(cat .env.test.local | xargs) && pytest tests/ -v ``` -### Code Formatting +### Code Formatting & Linting ```bash -black plane_mcp/ +ruff format plane_mcp/ ruff check plane_mcp/ ``` diff --git a/deployments/README.md b/deployments/README.md new file mode 100644 index 0000000..b378790 --- /dev/null +++ b/deployments/README.md @@ -0,0 +1,167 @@ +# Plane MCP Server — Production Deployment + +This folder contains production deployment configurations for the Plane MCP Server. + +> **Note**: These setups use the published Docker image. For local development, see the [Local Development](../README.md#local-development) section in the root README. + +--- + +## Option 1: Docker Compose + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and Docker Compose v2+ + +### Setup + +```bash +cd deployments + +# 1. Edit variables.env with your values +# (fill in OAuth credentials and Plane API URL) +vi variables.env + +# 2. Start the server +docker compose up -d + +# 3. Check logs +docker compose logs -f mcp +``` + +### Services + +| Service | Port | Description | +|---------|------|-------------| +| `mcp` | `8211` | Plane MCP Server (HTTP mode) | +| `valkey` | — | Token storage for OAuth (internal only) | + +### Endpoints + +| Endpoint | Auth | Description | +|----------|------|-------------| +| `http://:8211/mcp` | OAuth | OAuth-based MCP endpoint | +| `http://:8211/http/api-key/mcp` | PAT header | Personal Access Token endpoint | +| `http://:8211/sse` | OAuth | Legacy SSE endpoint (deprecated) | + +### Configuration + +All configuration is done via `variables.env`. Key variables: + +| Variable | Required | Description | +|----------|----------|-------------| +| `APP_RELEASE_VERSION` | No | Image tag to deploy (default: `latest`) | +| `PLANE_BASE_URL` | No | Plane API URL (default: `https://api.plane.so`) | +| `PLANE_INTERNAL_BASE_URL` | No | Internal API URL for server-to-server calls | +| `PLANE_OAUTH_PROVIDER_CLIENT_ID` | Yes | OAuth client ID | +| `PLANE_OAUTH_PROVIDER_CLIENT_SECRET` | Yes | OAuth client secret | +| `PLANE_OAUTH_PROVIDER_BASE_URL` | Yes | Public URL the server is reachable on | + +### Upgrading + +```bash +# Pull the latest image and restart +docker compose pull +docker compose up -d +``` + +--- + +## Option 2: Helm Chart + +### Prerequisites + +- Kubernetes cluster (v1.21+) +- [Helm](https://helm.sh/docs/intro/install/) v3+ +- An ingress controller (e.g. nginx) + +### Add the Helm Repository + +```bash +helm repo add plane https://helm.plane.so +helm repo update +``` + +### Install + +```bash +helm install plane-mcp plane/plane-mcp-server \ + --namespace plane-mcp \ + --create-namespace \ + -f values.yaml +``` + +### Minimal `values.yaml` + +```yaml +ingress: + enabled: true + host: mcp.yourdomain.com + ingressClass: nginx + ssl: + enabled: true + issuer: cloudflare # cloudflare | digitalocean | http + email: you@yourdomain.com + +services: + api: + plane_base_url: 'https://api.plane.so' + plane_oauth: + enabled: true + client_id: '' + client_secret: '' + provider_base_url: 'https://mcp.yourdomain.com' +``` + +### Key Values + +| Value | Default | Description | +|-------|---------|-------------| +| `dockerRegistry.default_tag` | `latest` | Image tag to deploy | +| `ingress.enabled` | `true` | Enable ingress | +| `ingress.host` | `mcp.example.com` | Public hostname | +| `ingress.ingressClass` | `nginx` | Ingress class name | +| `ingress.ssl.enabled` | `false` | Enable TLS via cert-manager | +| `ingress.ssl.issuer` | `cloudflare` | ACME issuer (`cloudflare`, `digitalocean`, `http`) | +| `services.api.replicas` | `1` | Number of MCP server replicas | +| `services.api.plane_base_url` | `''` | Plane API URL | +| `services.api.plane_oauth.enabled` | `false` | Enable OAuth endpoints | +| `services.api.plane_oauth.client_id` | `''` | OAuth client ID | +| `services.api.plane_oauth.client_secret` | `''` | OAuth client secret | +| `services.api.plane_oauth.provider_base_url` | `''` | Public URL the server is reachable on | +| `services.redis.local_setup` | `true` | Deploy Valkey in-cluster | +| `services.redis.external_redis_url` | `''` | External Valkey/Redis URL (if not using in-cluster) | +| `services.proxy.enabled` | `false` | Enable nginx proxy sidecar | + +### Upgrade + +```bash +helm upgrade plane-mcp plane/plane-mcp-server \ + --namespace plane-mcp \ + -f values.yaml +``` + +### Uninstall + +```bash +helm uninstall plane-mcp --namespace plane-mcp +``` + +--- + +## Troubleshooting + +**Server not starting?** +```bash +docker compose logs mcp +``` + +**Valkey connection issues?** +```bash +docker compose exec valkey valkey-cli ping +``` + +**Reset and start fresh:** +```bash +docker compose down -v # removes Redis volume too +docker compose up -d +``` diff --git a/deployments/docker-compose.yaml b/deployments/docker-compose.yaml new file mode 100644 index 0000000..c6d34d4 --- /dev/null +++ b/deployments/docker-compose.yaml @@ -0,0 +1,36 @@ +# Plane MCP Server — Production Deployment +# +# Setup: +# # edit variables.env with your values +# docker compose up -d + +name: plane-mcp + +services: + mcp: + image: makeplane/plane-mcp-server:${APP_RELEASE_VERSION:-latest} + restart: always + ports: + - "8211:8211" + env_file: + - variables.env + environment: + REDIS_HOST: valkey + REDIS_PORT: "6379" + depends_on: + valkey: + condition: service_healthy + + valkey: + image: valkey/valkey:8-alpine + restart: always + volumes: + - valkey-data:/data + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + valkey-data: diff --git a/deployments/variables.env b/deployments/variables.env new file mode 100644 index 0000000..44ea76a --- /dev/null +++ b/deployments/variables.env @@ -0,0 +1,30 @@ +# Plane MCP Server — Production Environment Variables +# Edit this file with your values before running docker compose up + +# ------------------------------------------------------------------- +# Release +# ------------------------------------------------------------------- +APP_RELEASE_VERSION=latest + +# ------------------------------------------------------------------- +# Plane API +# ------------------------------------------------------------------- +PLANE_BASE_URL=https://api.plane.so + +# Internal URL for server-to-server calls (optional) +# Use this if the MCP server and Plane API are on the same network +PLANE_INTERNAL_BASE_URL= + +# ------------------------------------------------------------------- +# OAuth Provider (required for OAuth flow via /mcp) +# ------------------------------------------------------------------- +PLANE_OAUTH_PROVIDER_CLIENT_ID= +PLANE_OAUTH_PROVIDER_CLIENT_SECRET= +# Public URL your MCP clients reach the server on +PLANE_OAUTH_PROVIDER_BASE_URL=http://localhost:8211 + +# ------------------------------------------------------------------- +# Redis — managed by docker-compose, override only if using external Redis +# ------------------------------------------------------------------- +# REDIS_HOST=redis +# REDIS_PORT=6379 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..19762bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +# Docker Compose for local development +# +# Setup: +# cp .env.docker .env # copy the template +# # edit .env with your values +# docker compose up --build +# +# The MCP server runs in HTTP mode and is available at: +# http://localhost:8211/mcp (OAuth) +# http://localhost:8211/http/api-key/mcp (header API key) + +name: plane-mcp-dev + +services: + mcp: + build: + context: . + dockerfile: Dockerfile + # Override entrypoint to install in editable mode before starting the server + # (command would be appended to the Dockerfile ENTRYPOINT, causing arg parse errors) + entrypoint: ["sh", "-c", "uv pip install --system -e '.[dev]' && python -m plane_mcp http"] + ports: + - "8211:8211" + volumes: + # Mount local source so code changes don't require a full rebuild + - ./plane_mcp:/app/plane_mcp + env_file: + - .env + environment: + # Valkey is wired automatically via the valkey service below + REDIS_HOST: valkey + REDIS_PORT: "6379" + depends_on: + valkey: + condition: service_healthy + restart: unless-stopped + + valkey: + image: valkey/valkey:8-alpine + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + restart: unless-stopped