Self-host the MCP server
This guide is for teams that want to run their own instance of the Plane MCP server — either because they use a self-hosted Plane installation that needs OAuth against their own domain, or because they want full control over the MCP infrastructure.
If you're a Plane Cloud user connecting to mcp.plane.so, you don't need this. Use the MCP server setup guide instead.
There are three ways to run it:
| Option | Best for |
|---|---|
| Docker Compose | Single-host setups and anyone already running Plane with Docker Compose |
| Plane Commercial Edition Helm chart | Recommended for Kubernetes installs of Plane Commercial Edition — the MCP server runs inside the same release and is served on your Plane domain under /mcp |
| Standalone Helm chart | Kubernetes, with the MCP server on its own hostname (for example mcp.yourdomain.com), including against Plane Cloud |
Prerequisites
- A running Plane instance (self-hosted or Cloud) with workspace admin access. OAuth application registration is available on Plane Cloud and Plane Commercial Edition; Plane Community Edition does not include it, so the OAuth transport cannot be used against a Community Edition instance. Community Edition users should run the server in local (stdio) mode with a personal access token instead.
- Docker and Docker Compose v2+, or Kubernetes v1.21+ with Helm v3+
- A public URL for the MCP server (e.g.
https://mcp.yourdomain.com) — OAuth callbacks must reach it over HTTPS. With the Plane Commercial Edition Helm chart there is no separate URL: the server is reachable on your Plane domain under/mcp(e.g.https://plane.yourdomain.com/mcp).
Register an OAuth app in Plane
The MCP server authenticates users through Plane's OAuth 2.0 system. You need to register an app to get a Client ID and Client Secret.
Go to Workspace settings → Integrations:
texthttps://<your-plane-domain>/<workspace>/settings/integrations/Click Build your own.
Fill in the application details:
Field Value App Name Anything descriptive (e.g. Plane MCP Server)Setup URL Your MCP server's public URL (e.g. https://mcp.yourdomain.com, orhttps://plane.yourdomain.com/mcpwhen it runs inside the Plane chart)Redirect URI Both URIs listed below, space-separated Webhook URL Leave empty unless you need webhook events Add both redirect URIs
FastMCP exposes one callback under the HTTP mount and one under the SSE mount:
Transport Redirect URI Streamable HTTP <MCP_SERVER_URL>/http/auth/callbackSSE (deprecated) <MCP_SERVER_URL>/auth/callbackFor
https://mcp.yourdomain.com, paste this into the Redirect URI field:texthttps://mcp.yourdomain.com/http/auth/callback https://mcp.yourdomain.com/auth/callbackA previously registered
https://mcp.yourdomain.com/callbackURI is harmless but unnecessary.If the server runs inside the Plane Commercial Edition Helm chart,
<MCP_SERVER_URL>is your Plane domain plus the path prefix (services.mcp_server.path_prefix,/mcpby default). Forhttps://plane.yourdomain.comwith the default prefix, register:texthttps://plane.yourdomain.com/mcp/http/auth/callback https://plane.yourdomain.com/mcp/auth/callbackUnder Scopes & permissions, tick both Read and Write on the All workspace resources row.
Grant full read and write — granular scopes will not work
The MCP server requests the workspace-wide
readandwritescopes, because its tools span every resource (projects, work items, cycles, modules, pages, members, and more).Scopes & permissions Read Write All workspace resources ✅ ✅ If you select only granular scopes (for example
projects:read) or none at all, Plane rejects the authorization request witherror=invalid_scopeand the token exchange never happens.Save. Copy the generated Client ID and Client Secret - you'll need them in the next step.
WARNING
Never expose the Client Secret in client-side code or commit it to version control.
For more detail on OAuth app creation, see Create an OAuth Application.
Deploy
Option A: Docker Compose
1. Create a docker-compose.yaml:
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:2. Create a variables.env with your OAuth credentials from Step 1:
# Image tag - pin to a specific version in production
APP_RELEASE_VERSION=latest
# Plane API URL - use your self-hosted instance URL or https://api.plane.so for Cloud
PLANE_BASE_URL=https://api.plane.so
# Optional: internal URL for server-to-server calls (same-network setups)
# PLANE_INTERNAL_BASE_URL=
# OAuth credentials from Step 1
PLANE_OAUTH_PROVIDER_CLIENT_ID=your-client-id
PLANE_OAUTH_PROVIDER_CLIENT_SECRET=your-client-secret
# Public URL where MCP clients reach this server (must match what you registered in Step 1)
PLANE_OAUTH_PROVIDER_BASE_URL=https://mcp.yourdomain.com3. Start:
docker compose up -d4. Verify:
docker compose logs -f mcp # follow startup logs
curl http://localhost:8211/http/mcp # expect: 401 or MCP protocol responseTerminate TLS in front of this container
The container listens on plain HTTP at :8211. Put it behind a reverse proxy (nginx, Caddy, Traefik, Cloudflare) that handles TLS. OAuth callbacks will fail without HTTPS, and PLANE_OAUTH_PROVIDER_BASE_URL must be the https:// URL that proxy exposes.
Environment variable reference
| Variable | Required | Description |
|---|---|---|
APP_RELEASE_VERSION | No | Image tag to deploy. Defaults to latest. Pin in production. |
PLANE_BASE_URL | No | Public Plane API URL. Defaults to https://api.plane.so. |
PLANE_INTERNAL_BASE_URL | No | Internal Plane URL for server-to-server calls. Falls back to PLANE_BASE_URL. |
PLANE_OAUTH_PROVIDER_CLIENT_ID | Yes | OAuth Client ID from Step 1. |
PLANE_OAUTH_PROVIDER_CLIENT_SECRET | Yes | OAuth Client Secret from Step 1. |
PLANE_OAUTH_PROVIDER_BASE_URL | Yes | Public URL of this MCP server, not your Plane instance. |
PLANE_OAUTH_PROVIDER_ENABLE_CIMD | No | Enables client ID metadata documents. Defaults to false. |
PLANE_OAUTH_ALLOWED_REDIRECT_URIS | No | Comma-separated extra client redirect patterns. * can match a port, path segment, or subdomain; keep hosts pinned. |
MCP_PATH_PREFIX | No | Prefix for every route. For example, /plane serves MCP at /plane/http/mcp. |
REDIS_URL | No | Redis or Valkey connection URL (redis:// or rediss:// for TLS) for persistent OAuth token storage. Takes precedence over REDIS_HOST/REDIS_PORT. Available from v0.3.2. |
REDIS_HOST | No | Redis or Valkey host for persistent OAuth token storage. Without it (or REDIS_URL), tokens use in-memory storage. |
REDIS_PORT | No | Redis or Valkey port. |
REDIS_PASSWORD | No | Static Redis or Valkey password. |
REDIS_SSL | No | Enables TLS for Redis or Valkey when set to true. |
ELASTICACHE_SECRET_ARN | No | AWS Secrets Manager ARN containing a rotating ElastiCache authentication token. |
AWS_REGION | No | AWS region for ELASTICACHE_SECRET_ARN. |
REDIS_AUTH_TOKEN_KEY | No | JSON key that contains the rotating token in the AWS secret. |
LOG_USER_INFO | No | Logs the user's display name when true. Defaults to false; the display name is PII. |
Onboard a new MCP client
The built-in redirect allowlist contains:
http://localhost:*,http://localhost:*/*,http://127.0.0.1:*, andhttp://127.0.0.1:*/*cursor://anysphere.cursor-mcp/oauth/*andhttps://www.cursor.com/*https://vscode.dev/redirectandhttps://insiders.vscode.dev/redirecthttps://antigravity.google/oauth-callbackhttps://claude.ai/*https://chatgpt.com/connector/oauth/*andhttps://chatgpt.com/connector_platform_oauth_redirecthttps://oauth-redirect.googleusercontent.com/r/*— Gemini custom apps (Spark). Added in v0.3.3https://grok.com/connectors-oauth-exchange-code/— Grok custom connectors on the web. Added in v0.3.3
Grok Build, the CLI, calls back to http://127.0.0.1:8080/callback and is already covered by the loopback patterns.
Append new client callbacks without releasing a new server version:
PLANE_OAUTH_ALLOWED_REDIRECT_URIS=https://newclient.com/cb,https://other.app/oauth/*The * wildcard can match any port, path segment, or subdomain. Keep the host pinned to a domain you trust.
Upgrading
docker compose pull
docker compose up -dOption B: Plane Commercial Edition Helm chart (recommended)
This is the recommended way to run the MCP server for a self-hosted Plane Commercial Edition. If you deploy Plane with the plane-enterprise Helm chart, enable the MCP server as a service of that release instead of installing a second chart. The chart wires it to the release's own API service and Redis, and adds a route for it to the Plane ingress, so it is served on your Plane domain under a path prefix (/mcp by default).
Requires plane-enterprise chart 3.9.0 or later. The MCP server is off by default.
Custom path prefix
The URLs in this section use the default /mcp prefix. If you set services.mcp_server.path_prefix to something else, use that prefix everywhere /mcp appears — in the setup URL, both redirect URIs, the client URL and the /.well-known discovery paths. The chart's ingress routes follow the value, so the registered OAuth app must too.
1. Register the OAuth app as described above, using your Plane domain plus /mcp for the setup URL and the two redirect URIs (https://plane.yourdomain.com/mcp/http/auth/callback and https://plane.yourdomain.com/mcp/auth/callback).
2. Add to your values.yaml:
services:
mcp_server:
enabled: true
plane_oauth:
client_id: "<your-oauth-client-id>"
client_secret: "<your-oauth-client-secret>"client_id and client_secret are required whenever services.mcp_server.enabled is true — the server's HTTP transport does not start without an OAuth app, so helm template and helm upgrade fail with the reason if they are missing (unless they come from external_secrets.mcp_server_env_existingSecret).
Everything else is derived from the release: PLANE_BASE_URL and PLANE_OAUTH_PROVIDER_BASE_URL from license.licenseDomain and your TLS setting (the server appends <path_prefix>/http itself), PLANE_INTERNAL_BASE_URL from the in-cluster API service, and REDIS_URL from the same Redis the API and live services use (bundled, env.remote_redis_url, or external_secrets.redis).
3. Upgrade the release:
helm upgrade plane-app plane/plane-enterprise \
--namespace plane \
-f values.yaml4. Verify:
kubectl logs -n plane deploy/plane-app-mcp-server-wl | grep "Token store"
# expect: Token store: Redis (url=redis://plane-app-redis...)
curl -i https://plane.yourdomain.com/mcp/http/mcp
# expect: 401 with a WWW-Authenticate headerIngress routes are added for you
The chart's nginx, Traefik and OpenShift ingress templates route <path_prefix>/ to the MCP server, plus the two OAuth discovery prefixes MCP clients fetch at the root of the host: /.well-known/oauth-protected-resource<path_prefix> and /.well-known/oauth-authorization-server<path_prefix> (/mcp with the default prefix). If you manage the ingress yourself (ingress.enabled: false), add those three paths to the <release>-mcp-server service on port 8211 — without the two /.well-known paths, clients fail OAuth discovery with an "is not valid JSON" error.
Chart values reference
| Value | Default | Description |
|---|---|---|
services.mcp_server.enabled | false | Run the MCP server inside the release |
services.mcp_server.image | makeplane/plane-mcp-server:v0.3.2 | Image, including the tag. Independent of planeVersion; the MCP server has its own release cadence |
services.mcp_server.replicas | 1 | Number of replicas |
services.mcp_server.path_prefix | /mcp | Path prefix on the Plane domain (MCP_PATH_PREFIX); the ingress route follows it |
services.mcp_server.plane_base_url | "" | Override PLANE_BASE_URL; empty derives it from license.licenseDomain |
services.mcp_server.plane_internal_base_url | "" | Override PLANE_INTERNAL_BASE_URL; empty uses the release's in-cluster API service |
services.mcp_server.plane_oauth.client_id | "" | OAuth Client ID. Required when the MCP server is enabled |
services.mcp_server.plane_oauth.client_secret | "" | OAuth Client Secret. Required when the MCP server is enabled |
services.mcp_server.plane_oauth.provider_base_url | "" | Override PLANE_OAUTH_PROVIDER_BASE_URL (your Plane URL, without /mcp); empty derives it from license.licenseDomain |
services.mcp_server.pullPolicy | Always | Image pull policy |
services.mcp_server.assign_cluster_ip | false | Assign a ClusterIP to the Service |
services.mcp_server.memoryRequest / cpuRequest | 150Mi / 100m | Resource requests |
services.mcp_server.memoryLimit / cpuLimit | 1000Mi / 500m | Resource limits |
services.mcp_server.nodeSelector / tolerations / affinity | {} / [] / {} | Scheduling, same shape as the other services |
services.mcp_server.labels / annotations | {} / {} | Custom labels and annotations for the Deployment |
external_secrets.mcp_server_env_existingSecret | "" | Name of a Secret you manage that replaces the chart's MCP Secret (PLANE_OAUTH_PROVIDER_CLIENT_*, REDIS_URL) |
To keep the OAuth credentials out of values.yaml, create a Secret with the PLANE_OAUTH_PROVIDER_CLIENT_ID and PLANE_OAUTH_PROVIDER_CLIENT_SECRET keys and point external_secrets.mcp_server_env_existingSecret at it; the chart then renders no MCP Secret of its own. Add REDIS_URL (rediss:// for TLS) unless external_secrets.redis.secretName is set, in which case the Redis connection comes from that Secret. PLANE_OAUTH_PROVIDER_BASE_URL is not needed — the chart puts it in the <release>-mcp-server-vars ConfigMap — but a value in your Secret overrides it.
Environment variables that have no chart value — for example PLANE_OAUTH_ALLOWED_REDIRECT_URIS or LOG_USER_INFO — can be set through the chart's extraEnv list, which applies to every workload.
Upgrading
Upgrade the Plane release as usual. To move the MCP server to a newer version on its own, change the tag in services.mcp_server.image and run helm upgrade.
Disabling
Set services.mcp_server.enabled: false and upgrade; the Service, Deployment, ConfigMap, Secret and ingress routes are removed.
Option C: Standalone Helm chart
Use the standalone plane-mcp-server chart when the MCP server needs its own hostname or runs against Plane Cloud. For a self-hosted Plane Commercial Edition, prefer Option B, which is where the chart-based deployment is maintained going forward.
1. Add the Plane Helm repo:
helm repo add plane https://helm.plane.so
helm repo update2. Create a values.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: "<your-oauth-client-id>"
client_secret: "<your-oauth-client-secret>"
provider_base_url: "https://mcp.yourdomain.com"3. Install:
helm install plane-mcp plane/plane-mcp-server \
--namespace plane-mcp \
--create-namespace \
-f values.yamlHelm values reference
| 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 this 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) |
Environment variables that have no Helm value — for example PLANE_OAUTH_ALLOWED_REDIRECT_URIS or LOG_USER_INFO — must be set as environment variables on the MCP server deployment.
Upgrading
helm upgrade plane-mcp plane/plane-mcp-server \
--namespace plane-mcp \
-f values.yamlUninstalling
helm uninstall plane-mcp --namespace plane-mcpLogging and observability
The server emits structured JSON logs with the tool name, duration, status, opaque user ID, and workspace slug.
LOG_USER_INFO defaults to false. Setting it to true also logs the user's display name, which is personally identifiable information.
Even with LOG_USER_INFO=false, log entries contain the opaque user ID and the workspace slug, which can identify a person or organisation when combined with other data. Treat log storage as sensitive: restrict who can read it, set a retention period, and redact those fields before sharing logs outside your team.
Connect AI clients
Once the server is running, your available endpoints are:
| Endpoint | Auth | Description |
|---|---|---|
https://mcp.yourdomain.com/http/mcp | OAuth | Recommended for most clients |
https://mcp.yourdomain.com/http/api-key/mcp | Authorization: Bearer <PAT>, x-workspace-slug: <slug> | CI, scripts, and headless setups |
https://mcp.yourdomain.com/sse | OAuth | Deprecated HTTP+SSE transport |
When the server runs inside the Plane Commercial Edition Helm chart, the same three endpoints live under your Plane domain and path prefix, for example https://plane.yourdomain.com/mcp/http/mcp.
Client configuration is identical to the MCP server setup guide. Swap https://mcp.plane.so for your server's host (and path prefix, if any) in each configuration.
Troubleshooting
Server not starting:
docker compose logs mcpValkey not reachable:
docker compose exec valkey valkey-cli ping
# Expect: PONGIf Valkey is unhealthy, tokens are stored in-memory and lost on restart. Verify REDIS_HOST and REDIS_PORT are set correctly in your environment.
OAuth errors:
If Plane redirects back with
error=invalid_scope, the OAuth app is missing the workspace-wide scopes. Open the app in Workspace settings → Integrations and tick both Read and Write on the All workspace resources row, then restart the authorization flow. Granular scopes alone are not enough — see step 4 of Register an OAuth app in Plane above.Confirm both redirect URIs are registered in your Plane OAuth app:
/http/auth/callbackand/auth/callback. An existing/callbackregistration is harmless but unnecessary.Check that
PLANE_OAUTH_PROVIDER_CLIENT_IDandPLANE_OAUTH_PROVIDER_CLIENT_SECRETmatch what Plane generated.Check that
PLANE_OAUTH_PROVIDER_BASE_URLis the publicly reachablehttps://URL of this MCP server - not your Plane instance URL.If the client reports
redirect_uri is not allowed, add its exact callback or a host-pinned pattern toPLANE_OAUTH_ALLOWED_REDIRECT_URIS, then restart the deployment.Clear any cached auth tokens on the client side:
bashrm -rf ~/.mcp-auth
Plane Commercial Edition Helm chart:
- If
helm upgradefails because the MCP server's OAuth client ID or secret is missing, setservices.mcp_server.plane_oauth.client_idandclient_secret(or supply them throughexternal_secrets.mcp_server_env_existingSecret) and upgrade again. - If the
<release>-mcp-server-wlpod restarts withclient_id is required - set via parameter or PLANE_OAUTH_PROVIDER_CLIENT_ID, yourmcp_server_env_existingSecretis missing thePLANE_OAUTH_PROVIDER_CLIENT_ID/PLANE_OAUTH_PROVIDER_CLIENT_SECRETkeys. - If it restarts with
Redis connection failed during startup PING, the release's Redis is not reachable — check the bundled<release>-redisStatefulSet orenv.remote_redis_url. The MCP server uses the same Redis as the API. - If a client fails discovery with
Unexpected token '<' ... is not valid JSON, the two/.well-known/oauth-*paths are reaching the Plane web app instead of the MCP server. With the chart-managed ingress this cannot happen; with your own ingress, route them as described in Option B.
Reset Docker Compose (deletes Valkey data):
docker compose down -v
docker compose up -dStill stuck:
- Double-check OAuth credentials and redirect URIs in Plane workspace settings.
- Check the plane-mcp-server repo for known issues.
- Contact support@plane.so.
→ For client configuration details, see the MCP server setup guide. → For the full list of available tools, see the tool reference.

