Skip to content

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:

OptionBest for
Docker ComposeSingle-host setups and anyone already running Plane with Docker Compose
Plane Commercial Edition Helm chartRecommended 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 chartKubernetes, 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.

  1. Go to Workspace settings → Integrations:

    text
    https://<your-plane-domain>/<workspace>/settings/integrations/
  2. Click Build your own.

  3. Fill in the application details:

    FieldValue
    App NameAnything descriptive (e.g. Plane MCP Server)
    Setup URLYour MCP server's public URL (e.g. https://mcp.yourdomain.com, or https://plane.yourdomain.com/mcp when it runs inside the Plane chart)
    Redirect URIBoth URIs listed below, space-separated
    Webhook URLLeave empty unless you need webhook events

    Add both redirect URIs

    FastMCP exposes one callback under the HTTP mount and one under the SSE mount:

    TransportRedirect URI
    Streamable HTTP<MCP_SERVER_URL>/http/auth/callback
    SSE (deprecated)<MCP_SERVER_URL>/auth/callback

    For https://mcp.yourdomain.com, paste this into the Redirect URI field:

    text
    https://mcp.yourdomain.com/http/auth/callback https://mcp.yourdomain.com/auth/callback

    A previously registered https://mcp.yourdomain.com/callback URI 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, /mcp by default). For https://plane.yourdomain.com with the default prefix, register:

    text
    https://plane.yourdomain.com/mcp/http/auth/callback https://plane.yourdomain.com/mcp/auth/callback
  4. Under 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 read and write scopes, because its tools span every resource (projects, work items, cycles, modules, pages, members, and more).

    Scopes & permissionsReadWrite
    All workspace resources✅✅

    If you select only granular scopes (for example projects:read) or none at all, Plane rejects the authorization request with error=invalid_scope and the token exchange never happens.

  5. 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:

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:

ini
# 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.com

3. Start:

bash
docker compose up -d

4. Verify:

bash
docker compose logs -f mcp           # follow startup logs
curl http://localhost:8211/http/mcp  # expect: 401 or MCP protocol response

Terminate 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 ​

VariableRequiredDescription
APP_RELEASE_VERSIONNoImage tag to deploy. Defaults to latest. Pin in production.
PLANE_BASE_URLNoPublic Plane API URL. Defaults to https://api.plane.so.
PLANE_INTERNAL_BASE_URLNoInternal Plane URL for server-to-server calls. Falls back to PLANE_BASE_URL.
PLANE_OAUTH_PROVIDER_CLIENT_IDYesOAuth Client ID from Step 1.
PLANE_OAUTH_PROVIDER_CLIENT_SECRETYesOAuth Client Secret from Step 1.
PLANE_OAUTH_PROVIDER_BASE_URLYesPublic URL of this MCP server, not your Plane instance.
PLANE_OAUTH_PROVIDER_ENABLE_CIMDNoEnables client ID metadata documents. Defaults to false.
PLANE_OAUTH_ALLOWED_REDIRECT_URISNoComma-separated extra client redirect patterns. * can match a port, path segment, or subdomain; keep hosts pinned.
MCP_PATH_PREFIXNoPrefix for every route. For example, /plane serves MCP at /plane/http/mcp.
REDIS_URLNoRedis 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_HOSTNoRedis or Valkey host for persistent OAuth token storage. Without it (or REDIS_URL), tokens use in-memory storage.
REDIS_PORTNoRedis or Valkey port.
REDIS_PASSWORDNoStatic Redis or Valkey password.
REDIS_SSLNoEnables TLS for Redis or Valkey when set to true.
ELASTICACHE_SECRET_ARNNoAWS Secrets Manager ARN containing a rotating ElastiCache authentication token.
AWS_REGIONNoAWS region for ELASTICACHE_SECRET_ARN.
REDIS_AUTH_TOKEN_KEYNoJSON key that contains the rotating token in the AWS secret.
LOG_USER_INFONoLogs 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:*, and http://127.0.0.1:*/*
  • cursor://anysphere.cursor-mcp/oauth/* and https://www.cursor.com/*
  • https://vscode.dev/redirect and https://insiders.vscode.dev/redirect
  • https://antigravity.google/oauth-callback
  • https://claude.ai/*
  • https://chatgpt.com/connector/oauth/* and https://chatgpt.com/connector_platform_oauth_redirect
  • https://oauth-redirect.googleusercontent.com/r/* — Gemini custom apps (Spark). Added in v0.3.3
  • https://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:

ini
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 ​

bash
docker compose pull
docker compose up -d

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:

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:

bash
helm upgrade plane-app plane/plane-enterprise \
  --namespace plane \
  -f values.yaml

4. Verify:

bash
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 header

Ingress 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 ​

ValueDefaultDescription
services.mcp_server.enabledfalseRun the MCP server inside the release
services.mcp_server.imagemakeplane/plane-mcp-server:v0.3.2Image, including the tag. Independent of planeVersion; the MCP server has its own release cadence
services.mcp_server.replicas1Number of replicas
services.mcp_server.path_prefix/mcpPath 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.pullPolicyAlwaysImage pull policy
services.mcp_server.assign_cluster_ipfalseAssign a ClusterIP to the Service
services.mcp_server.memoryRequest / cpuRequest150Mi / 100mResource requests
services.mcp_server.memoryLimit / cpuLimit1000Mi / 500mResource 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:

bash
helm repo add plane https://helm.plane.so
helm repo update

2. Create a 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: "<your-oauth-client-id>"
      client_secret: "<your-oauth-client-secret>"
      provider_base_url: "https://mcp.yourdomain.com"

3. Install:

bash
helm install plane-mcp plane/plane-mcp-server \
  --namespace plane-mcp \
  --create-namespace \
  -f values.yaml

Helm values reference ​

ValueDefaultDescription
dockerRegistry.default_taglatestImage tag to deploy
ingress.enabledtrueEnable ingress
ingress.hostmcp.example.comPublic hostname
ingress.ingressClassnginxIngress class name
ingress.ssl.enabledfalseEnable TLS via cert-manager
ingress.ssl.issuercloudflareACME issuer (cloudflare, digitalocean, http)
services.api.replicas1Number of MCP server replicas
services.api.plane_base_url""Plane API URL
services.api.plane_oauth.enabledfalseEnable 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_setuptrueDeploy 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 ​

bash
helm upgrade plane-mcp plane/plane-mcp-server \
  --namespace plane-mcp \
  -f values.yaml

Uninstalling ​

bash
helm uninstall plane-mcp --namespace plane-mcp

Logging 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:

EndpointAuthDescription
https://mcp.yourdomain.com/http/mcpOAuthRecommended for most clients
https://mcp.yourdomain.com/http/api-key/mcpAuthorization: Bearer <PAT>, x-workspace-slug: <slug>CI, scripts, and headless setups
https://mcp.yourdomain.com/sseOAuthDeprecated 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:

bash
docker compose logs mcp

Valkey not reachable:

bash
docker compose exec valkey valkey-cli ping
# Expect: PONG

If 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/callback and /auth/callback. An existing /callback registration is harmless but unnecessary.

  • Check that PLANE_OAUTH_PROVIDER_CLIENT_ID and PLANE_OAUTH_PROVIDER_CLIENT_SECRET match what Plane generated.

  • Check that PLANE_OAUTH_PROVIDER_BASE_URL is the publicly reachable https:// 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 to PLANE_OAUTH_ALLOWED_REDIRECT_URIS, then restart the deployment.

  • Clear any cached auth tokens on the client side:

    bash
    rm -rf ~/.mcp-auth

Plane Commercial Edition Helm chart:

  • If helm upgrade fails because the MCP server's OAuth client ID or secret is missing, set services.mcp_server.plane_oauth.client_id and client_secret (or supply them through external_secrets.mcp_server_env_existingSecret) and upgrade again.
  • If the <release>-mcp-server-wl pod restarts with client_id is required - set via parameter or PLANE_OAUTH_PROVIDER_CLIENT_ID, your mcp_server_env_existingSecret is missing the PLANE_OAUTH_PROVIDER_CLIENT_ID / PLANE_OAUTH_PROVIDER_CLIENT_SECRET keys.
  • If it restarts with Redis connection failed during startup PING, the release's Redis is not reachable — check the bundled <release>-redis StatefulSet or env.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):

bash
docker compose down -v
docker compose up -d

Still stuck:

  1. Double-check OAuth credentials and redirect URIs in Plane workspace settings.
  2. Check the plane-mcp-server repo for known issues.
  3. 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.