From ead1551db696e511a114ba43ab6fd075db88c901 Mon Sep 17 00:00:00 2001 From: vihar Date: Sat, 15 Aug 2026 19:31:34 +0530 Subject: [PATCH 1/2] docs: sync MCP server docs to plane-mcp-server v0.3.0 Rewrite the MCP server setup guide around the hosted OAuth endpoint with per-client setup (Claude, Claude Code, ChatGPT, Codex, Cursor, VS Code, Windsurf, Zed, Antigravity, mcp-remote), correct the access-token headers (Authorization: Bearer + x-workspace-slug), and add security, permissions, upgrading, troubleshooting and FAQ sections. Rebuild the tool reference for the 28 action-dispatch tools (183 actions, 169 retired-name aliases, 7 unmappable) from the v0.3.0 tool declarations, and update the self-hosting guide with the correct OAuth callback URIs, new environment variables (PLANE_OAUTH_ALLOWED_REDIRECT_URIS, LOG_USER_INFO, Redis/ElastiCache options) and logging notes. --- docs/.vitepress/config.mts | 8 +- docs/dev-tools/mcp-server-self-host.md | 122 ++- docs/dev-tools/mcp-server-tools.md | 1326 +++++++++++++----------- docs/dev-tools/mcp-server.md | 918 +++++++++------- docs/index.md | 2 +- vercel.json | 2 +- 6 files changed, 1321 insertions(+), 1057 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 15be8a58..d9402339 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -314,7 +314,7 @@ export default extendConfig( items: [ { text: "Webhooks", link: "/dev-tools/intro-webhooks" }, { text: "Build Plane App", link: "/dev-tools/build-plane-app/overview" }, - { text: "MCP Server", link: "/dev-tools/mcp-server" }, + { text: "MCP server", link: "/dev-tools/mcp-server" }, { text: "Agents", link: "/dev-tools/agents/overview" }, ], }, @@ -1118,12 +1118,12 @@ export default extendConfig( }, { - text: "MCP Server", + text: "MCP server", link: "/dev-tools/mcp-server", collapsed: false, items: [ - { text: "Self-host MCP Server", link: "/dev-tools/mcp-server-self-host" }, - { text: "Tool Reference", link: "/dev-tools/mcp-server-tools" }, + { text: "Self-host the MCP server", link: "/dev-tools/mcp-server-self-host" }, + { text: "Tool reference", link: "/dev-tools/mcp-server-tools" }, ], }, { text: "Plane Compose", link: "/dev-tools/plane-compose" }, diff --git a/docs/dev-tools/mcp-server-self-host.md b/docs/dev-tools/mcp-server-self-host.md index 00134e8d..34e5b43d 100644 --- a/docs/dev-tools/mcp-server-self-host.md +++ b/docs/dev-tools/mcp-server-self-host.md @@ -1,20 +1,23 @@ --- -title: Self-host MCP Server -description: Deploy your own Plane MCP Server with Docker Compose or Helm. Register an OAuth app, configure credentials, and point AI clients at your own instance. +title: Self-host the MCP server +description: Deploy the Plane MCP server with Docker Compose or Helm, register OAuth callbacks, configure storage and security, and connect AI clients. keywords: plane mcp server, self-hosted mcp, plane mcp deployment, docker compose mcp, helm mcp, plane oauth mcp, mcp server setup --- -# Self-host MCP Server +# 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're on a self-hosted Plane installation that needs OAuth against their own domain, or because they want full control over the MCP infrastructure. +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](/dev-tools/mcp-server) instead. +If you're a Plane Cloud user connecting to `mcp.plane.so`, you don't need this. Use the +[MCP server setup guide](/dev-tools/mcp-server) instead. ## Prerequisites - A running **Plane instance** (self-hosted or Cloud) with workspace admin access - **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 be able to reach it over HTTPS +- A **public URL** for the MCP server (e.g. `https://mcp.yourdomain.com`) — OAuth callbacks must reach it over HTTPS --- @@ -22,9 +25,9 @@ If you're a Plane Cloud user connecting to `mcp.plane.so`, you don't need this. 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**: +1. Go to **Workspace settings → Integrations**: - ``` + ```text https:////settings/integrations/ ``` @@ -36,27 +39,28 @@ The MCP server authenticates users through Plane's OAuth 2.0 system. You need to | ---------------- | ---------------------------------------------------------------- | | **App Name** | Anything descriptive (e.g. `Plane MCP Server`) | | **Setup URL** | Your MCP server's public URL (e.g. `https://mcp.yourdomain.com`) | - | **Redirect URI** | All three URIs listed below, space-separated | + | **Redirect URI** | Both URIs listed below, space-separated | | **Webhook URL** | Leave empty unless you need webhook events | - ::: tip Add all three redirect URIs - The server exposes callbacks on three paths to support all transports and MCP clients: + ::: tip Add both redirect URIs + FastMCP exposes one callback under the HTTP mount and one under the SSE mount: - | Transport | Redirect URI | - | --------------- | ------------------------------------- | - | OAuth callback | `/callback` | - | HTTP with OAuth | `/http/auth/callback` | - | SSE (legacy) | `/auth/callback` | + | Transport | Redirect URI | + | ---------------- | ------------------------------------- | + | Streamable HTTP | `/http/auth/callback` | + | SSE (deprecated) | `/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 ``` - https://mcp.yourdomain.com/callback 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. ::: -4. Under **Scopes & Permissions**, select both **read** and **write** scopes. +4. Under **Scopes & permissions**, select both **read** and **write** scopes. 5. Save. Copy the generated **Client ID** and **Client Secret** - you'll need them in the next step. @@ -146,15 +150,45 @@ The container listens on plain HTTP at `:8211`. Put it behind a reverse proxy (n #### Environment variable reference -| Variable | Required | Description | -| ------------------------------------ | -------- | ------------------------------------------------------------------------------ | -| `APP_RELEASE_VERSION` | No | Image tag to deploy. Defaults to `latest`. Pin in production. | -| `PLANE_BASE_URL` | No | 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. | -| `MCP_PATH_PREFIX` | No | Path prefix for all routes. Use when reverse-proxying alongside other apps. | +| 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_HOST` | No | Redis or Valkey host for persistent OAuth token storage. Without it, 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:*`, 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` + +Append new client callbacks without releasing a new server version: + +```env +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 @@ -224,6 +258,9 @@ helm install plane-mcp plane/plane-mcp-server \ | `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 ```bash @@ -240,17 +277,25 @@ 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. + ## 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` | PAT header | For CI/CD, scripts, headless setups | -| `https://mcp.yourdomain.com/sse` | OAuth | Legacy SSE transport (deprecated) | +| Endpoint | Auth | Description | +| --------------------------------------------- | --------------------------------------------------------- | -------------------------------- | +| `https://mcp.yourdomain.com/http/mcp` | OAuth | Recommended for most clients | +| `https://mcp.yourdomain.com/http/api-key/mcp` | `Authorization: Bearer `, `x-workspace-slug: ` | CI, scripts, and headless setups | +| `https://mcp.yourdomain.com/sse` | OAuth | Deprecated HTTP+SSE transport | -Client configuration is identical to the [MCP Server setup guide](/dev-tools/mcp-server) - replace `https://mcp.plane.so` with your server's URL in every config snippet. +Client configuration is identical to the [MCP server setup guide](/dev-tools/mcp-server). Swap +`https://mcp.plane.so` for your server's host in each configuration. --- @@ -273,9 +318,12 @@ If Valkey is unhealthy, tokens are stored in-memory and lost on restart. Verify **OAuth errors:** -- Confirm all three redirect URIs are registered in your Plane OAuth app: `/callback`, `/http/auth/callback`, `/auth/callback`. +- 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 @@ -297,5 +345,5 @@ docker compose up -d --- -→ For client configuration details, see the [MCP Server setup guide](/dev-tools/mcp-server). -→ For the full list of available tools, see the [MCP Server Tool Reference](/dev-tools/mcp-server-tools). +→ For client configuration details, see the [MCP server setup guide](/dev-tools/mcp-server). +→ For the full list of available tools, see the [tool reference](/dev-tools/mcp-server-tools). diff --git a/docs/dev-tools/mcp-server-tools.md b/docs/dev-tools/mcp-server-tools.md index 832b7b9d..1b575f52 100644 --- a/docs/dev-tools/mcp-server-tools.md +++ b/docs/dev-tools/mcp-server-tools.md @@ -1,763 +1,855 @@ --- -title: MCP Server Tool Reference -description: Complete reference for all tools exposed by the Plane MCP Server — work items, projects, cycles, modules, worklogs, epics, milestones, and more. -keywords: plane mcp server, mcp tools, plane api tools, work items mcp, projects mcp, cycles mcp, mcp tool reference +title: MCP server tool reference +description: Reference for the 28 tools and 183 actions exposed by the Plane MCP server — work items, cycles, modules, releases, customers, pages, and more. +keywords: plane mcp tools, mcp tool reference, plane mcp actions, workitem tool, cycle tool, release tool, customer tool --- -# MCP Server Tool Reference +# Tool reference -The Plane MCP Server exposes 100+ tools across 20 modules. All tools are available regardless of transport mode — stdio, HTTP/OAuth, HTTP/PAT, and SSE expose the same set. +The Plane MCP server exposes 28 tools, one per resource. Resource tools take an `action` parameter that selects one of 183 operations; `get_pql_reference` is the only exception. Every transport—hosted OAuth, hosted access token, local stdio, and deprecated SSE—exposes the same surface. -For setup and client configuration, see the [MCP Server setup guide](/dev-tools/mcp-server). +The description your MCP client receives is generated from each tool's action declarations. It is the authoritative reference at call time, including required and optional parameters. ---- - -### Users - -#### `get_me` - -Returns the profile of the currently authenticated user. No parameters. - ---- - -### Workspaces - -#### `get_workspace_members` - -Returns all members of the workspace. - -#### `get_workspace_features` - -Returns enabled features for the workspace. - -#### `update_workspace_features` - -Updates workspace-level feature flags. - ---- - -### Projects +**Totals:** 28 tools, 183 actions, and 169 retired-name aliases. -#### `list_projects` +For connection and authentication instructions, see [MCP server](/dev-tools/mcp-server). -Returns all projects the current user is a member of. +## How to read this page -#### `create_project` +Each tool table has these columns: -Creates a new project. +- **Action** is the value to pass as `action`. +- **Required** parameters must be present for that action. +- **Optional** parameters are accepted only for that action. +- **Notes** include action-specific behavior and the **Read-only** or **Destructive** flags. -| Parameter | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------------------------ | -| `name` | string | **Yes** | Project display name | -| `identifier` | string | **Yes** | Short uppercase code, max 12 chars (e.g., `ENG`) | -| `description` | string | No | Project description | -| `network` | string | No | `0` (secret) or `2` (public) | +Actions with `cursor` and `per_page` return a `next_cursor` when another page is available. Follow it until it is empty. Update actions change only the fields you pass. -#### `retrieve_project` +Parameter values are plain strings, numbers, booleans, and lists. The server also coerces string-encoded lists, integers, and booleans. If you pass an argument that belongs to another action, validation rejects it and names the permitted parameters. -Returns details of a single project. +## Conventions -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +### Identifiers -#### `update_project` +Most actions take UUIDs for projects, work items, states, labels, members, and other resources. List or resolve the relevant resource first when you have only its name or short identifier. -Updates project fields. All fields are optional. +`workitem retrieve_by_identifier` is the exception: its `workitem_identifier` accepts a readable identifier such as `ENG-42`. Other `workitem` actions use the work item's UUID in `workitem_id`. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| other fields | partial | No | +### Project vs workspace scope -#### `delete_project` +Pages, work item types, and work item properties can belong to a project or the workspace. Supply `project_id` for the project's own set; omit it for the workspace's set. -Deletes a project. - -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | - -#### `get_project_worklog_summary` - -Returns time-tracking summary for a project. - -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | - -#### `get_project_members` - -Returns all members of a project. - -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | - -#### `get_project_features` - -Returns the feature configuration for a project (modules, cycles, pages, etc.). - -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | - -#### `update_project_features` - -Updates which features are enabled on a project. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| feature fields | partial | No | - ---- +Some workspaces centrally govern the type and property vocabulary. In that mode, project-scoped writes are refused. Use `workitem_type resolve` to find or create a usable type without duplicates, or `workitem_type import_to_project` to link existing workspace types to a project. -### Work Items +### PQL -#### `list_work_items` +`workitem list`, `workitem list_archived`, `workitem count`, `cycle list_workitems`, and `module list_workitems` accept a `pql` filter. Call `get_pql_reference` with `detail="brief"` or `detail="full"` before composing a query, and see the [Plane Query Language guide](https://docs.plane.so/core-concepts/issues/plane-query-language). -Lists work items in a project, or searches across the workspace when filters are provided. +UUID-backed PQL fields—such as project, assignee, state, label, cycle, module, type, milestone, and creator—need UUIDs. Resolve names before inserting them into a query. -When any filter parameter is set, the tool uses Plane's advanced search endpoint (supports workspace-wide search). Without filters it uses the standard paginated list endpoint. - -| Parameter | Type | Required | Description | -| ------------------ | ----------- | ----------- | --------------------------------------------------------------- | -| `project_id` | UUID string | Conditional | Required when no filters are provided | -| `query` | string | No | Free-text search across name and description | -| `assignee_ids` | UUID[] | No | Filter by assignee | -| `state_ids` | UUID[] | No | Filter by state | -| `state_groups` | string[] | No | `backlog` · `unstarted` · `started` · `completed` · `cancelled` | -| `priorities` | string[] | No | `urgent` · `high` · `medium` · `low` · `none` | -| `label_ids` | UUID[] | No | Filter by label | -| `type_ids` | UUID[] | No | Filter by work item type | -| `cycle_ids` | UUID[] | No | Filter by cycle | -| `module_ids` | UUID[] | No | Filter by module | -| `is_archived` | boolean | No | Filter by archived status | -| `created_by_ids` | UUID[] | No | Filter by creator | -| `workspace_search` | boolean | No | Search across all projects (requires filters) | -| `limit` | integer | No | Max results when using filters | -| `cursor` | string | No | Pagination cursor (list mode) | -| `per_page` | integer | No | Results per page, 1–100 (list mode) | -| `expand` | string | No | Comma-separated fields to expand | -| `fields` | string | No | Comma-separated fields to include | -| `order_by` | string | No | Sort field | - -#### `create_work_item` - -Creates a new work item in a project. - -| Parameter | Type | Required | Description | -| ------------------ | ----------- | -------- | --------------------------------------------- | -| `project_id` | UUID string | **Yes** | Target project | -| `name` | string | **Yes** | Work item title | -| `description_html` | string | No | HTML body | -| `state_id` | UUID string | No | Initial state | -| `priority` | string | No | `urgent` · `high` · `medium` · `low` · `none` | -| `assignee_ids` | UUID[] | No | Assigned members | -| `label_ids` | UUID[] | No | Labels | -| `type_id` | UUID string | No | Work item type | -| `parent_id` | UUID string | No | Parent work item (sub-item) | -| `start_date` | string | No | `YYYY-MM-DD` | -| `due_date` | string | No | `YYYY-MM-DD` | - -#### `retrieve_work_item` - -Returns a single work item by UUID. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `retrieve_work_item_by_identifier` - -Returns a work item using its human-readable identifier (e.g., `ENG-42`). - -| Parameter | Type | Required | Description | -| ---------------------- | ------ | -------- | --------------------------- | -| `project_identifier` | string | **Yes** | Project prefix, e.g., `ENG` | -| `work_item_identifier` | string | **Yes** | Issue number, e.g., `42` | - -#### `update_work_item` - -Updates one or more fields on a work item. Only supplied fields are changed. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| other fields | partial | No | - -#### `delete_work_item` - -Permanently deletes a work item. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `search_work_items` - -Searches work items by text query within a project. - -| Parameter | Type | Required | Description | -| ------------ | ----------- | -------- | ----------- | -| `project_id` | UUID string | **Yes** | | -| `query` | string | **Yes** | Search text | - ---- - -### Work Item Activities - -#### `list_work_item_activities` - -Returns the activity log (history of changes) for a work item. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `retrieve_work_item_activity` - -Returns a single activity entry. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `activity_id` | UUID string | **Yes** | - ---- - -### Work Item Comments - -#### `list_work_item_comments` - -Returns all comments on a work item. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `retrieve_work_item_comment` - -Returns a single comment. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `comment_id` | UUID string | **Yes** | - -#### `create_work_item_comment` - -Adds a comment to a work item. Comments are stored as HTML. - -| Parameter | Type | Required | Description | -| -------------- | ----------- | -------- | --------------------------------------------------- | -| `project_id` | UUID string | **Yes** | | -| `work_item_id` | UUID string | **Yes** | | -| `comment_html` | string | **Yes** | HTML content, e.g., `

Fixed in commit abc123

` | - -#### `update_work_item_comment` - -Updates a comment's content. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `comment_id` | UUID string | **Yes** | -| `comment_html` | string | **Yes** | - -#### `delete_work_item_comment` - -Deletes a comment. - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `comment_id` | UUID string | **Yes** | - ---- - -### Work Item Links - -External URLs attached to a work item (e.g., Figma designs, PRs, docs). - -#### `list_work_item_links` - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `retrieve_work_item_link` - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `link_id` | UUID string | **Yes** | - -#### `create_work_item_link` - -| Parameter | Type | Required | Description | -| -------------- | ----------- | -------- | -------------------------- | -| `project_id` | UUID string | **Yes** | | -| `work_item_id` | UUID string | **Yes** | | -| `url` | string | **Yes** | External URL | -| `title` | string | No | Display title for the link | - -#### `update_work_item_link` - -| Parameter | Type | Required | -| --------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `link_id` | UUID string | **Yes** | -| `url` / `title` | string | No | - -#### `delete_work_item_link` - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `link_id` | UUID string | **Yes** | - ---- - -### Work Item Relations - -Relations between work items (e.g., "blocks", "is blocked by", "duplicate of"). - -#### `list_work_item_relations` - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | - -#### `create_work_item_relation` - -| Parameter | Type | Required | Description | -| ---------------------- | ----------- | -------- | ----------------------------------------------------------------------- | -| `project_id` | UUID string | **Yes** | | -| `work_item_id` | UUID string | **Yes** | Source work item | -| `related_work_item_id` | UUID string | **Yes** | Target work item | -| `relation_type` | string | **Yes** | `blocking` · `blocked_by` · `duplicate_of` · `duplicate` · `relates_to` | - -#### `remove_work_item_relation` - -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `relation_id` | UUID string | **Yes** | - ---- - -### Work Item Properties - -Custom fields defined per project. - -#### `list_work_item_properties` +### Epics -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +Plane represents an epic as a work item whose type is named **Epic**. There is no separate epic tool. -#### `create_work_item_property` +1. Call `workitem_type resolve` with `project_id` and `name="Epic"`, then keep the returned type `id`. +2. Call `workitem create` with the project, name, and that `type_id`. +3. List epics with `workitem list` and `pql='type = ""'`. +4. Read, edit, or delete an epic with `workitem retrieve`, `workitem update`, or `workitem delete`. To nest a work item under it, pass the epic's work item UUID as `parent` to `workitem update` or `workitem create`. +5. List an epic's children with `workitem list` and `pql='childOf("PROJ-12")'`, using the epic's readable identifier. -| Parameter | Type | Required | Description | -| --------------- | ----------- | -------- | ------------------------ | -| `project_id` | UUID string | **Yes** | | -| `name` | string | **Yes** | Property name | -| `property_type` | string | **Yes** | Type of the custom field | +### Plan availability -#### `retrieve_work_item_property` +The server declares feature gates for `work_log` (**Time tracking**), `workitem_type` (**Work item types**), `workitem_property` (**Work item properties**), and some `project` features. Customers, initiatives, releases, and pages can also be gated by the Plane API and your plan. When a feature is unavailable, the error names it. -| Parameter | Type | Required | -| ------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `property_id` | UUID string | **Yes** | +## Tools by resource group -#### `update_work_item_property` +Tools are grouped by the Plane resource they manage. Each group opens with an example prompt. -| Parameter | Type | Required | -| ------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `property_id` | UUID string | **Yes** | +## Work items -#### `delete_work_item_property` +_Example prompt: “Summarize what changed on ENG-42 this week, including comments, state changes, and assignees.”_ -| Parameter | Type | Required | -| ------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `property_id` | UUID string | **Yes** | +### `workitem` — Work items ---- +Work items -- issues, tasks and epics. -### Work Item Types +| Action | Required | Optional | Notes | +| ------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `list` | — | `project_id`, `pql`, `order_by`, `per_page`, `cursor`, `expand`, `fields`, `external_id`, `external_source` | omit project_id to search the whole workspace; Read-only | +| `list_archived` | `project_id` | `pql`, `order_by`, `per_page`, `cursor`, `expand`, `fields`, `external_id`, `external_source` | Read-only | +| `retrieve` | `project_id`, `workitem_id` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | Read-only | +| `retrieve_by_identifier` | `workitem_identifier` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | identifier is PROJECT-N, e.g. ENG-42; Read-only | +| `search` | `query` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | Read-only | +| `count` | — | `project_id`, `pql`, `group_by`, `sub_group_by` | counts the whole workspace unless project_id narrows it; Read-only | +| `create` | `project_id`, `name` | `assignees`, `labels`, `type_id`, `point`, `description_html`, `description_stripped`, `priority`, `start_date`, `target_date`, `sort_order`, `is_draft`, `parent`, `state`, `estimate_point`, `external_source`, `external_id` | — | +| `update` | `project_id`, `workitem_id` | `name`, `assignees`, `labels`, `type_id`, `point`, `description_html`, `description_stripped`, `priority`, `start_date`, `target_date`, `sort_order`, `is_draft`, `parent`, `state`, `estimate_point`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id`, `workitem_id` | — | Destructive | +| `archive` | `project_id`, `workitem_id` | `archive` | archive defaults to true; pass archive=false to unarchive. Only completed or cancelled items can be archived | +| `manage_assignee` | `project_id`, `workitem_id` | `add_user_id`, `remove_user_id` | each takes one id or several; the list is merged, not replaced, and removals apply first | +| `manage_label` | `project_id`, `workitem_id` | `add_label_id`, `remove_label_id` | each takes one id or several; the list is merged, not replaced, and removals apply first | -Custom work item type definitions (e.g., Bug, Feature, Task, Epic). +**Notes:** priority: urgent, high, medium, low, none. UUID fields (assignees, labels, state, parent, type_id) need UUIDs -- list the relevant resource first if you only have a name. description_stripped is plain text and is wrapped into HTML on save; description_html wins if both are given. fields is a sparse fieldset: use `project`, not project_id, and `description_html`, not description. count group_by and sub_group_by accept: state_id, state\_\_group, priority, project_id, type_id, labels\_\_id, assignees\_\_id, issue_module\_\_module_id, release_work_items\_\_release_id, cycle_id, milestone_id, created_by, target_date, start_date. These are grouping keys only -- they are not PQL filter fields, and filtering on state\_\_group is rejected. -#### `list_work_item_types` +### `workitem_comment` — Work item comments -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +Comments on a work item. -#### `create_work_item_type` +| Action | Required | Optional | Notes | +| ---------- | ------------------------------------------- | ---------------------------------------------------------- | ----------- | +| `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `workitem_id`, `comment_id` | — | Read-only | +| `create` | `project_id`, `workitem_id`, `comment_html` | `access`, `external_source`, `external_id` | — | +| `update` | `project_id`, `workitem_id`, `comment_id` | `comment_html`, `access`, `external_source`, `external_id` | — | +| `delete` | `project_id`, `workitem_id`, `comment_id` | — | Destructive | -| Parameter | Type | Required | -| ------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `name` | string | **Yes** | -| `description` | string | No | -| `is_active` | boolean | No | +**Notes:** comment_html is HTML, e.g. '<p>Looks good.</p>'. access is INTERNAL or EXTERNAL. -#### `retrieve_work_item_type` +### `workitem_activity` — Work item activity -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `type_id` | UUID string | **Yes** | +Change history for a work item. -#### `update_work_item_type` +| Action | Required | Optional | Notes | +| ---------- | ------------------------------------------ | -------------------- | --------- | +| `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `workitem_id`, `activity_id` | — | Read-only | -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `type_id` | UUID string | **Yes** | +### `workitem_attachment` — Work item attachments -#### `delete_work_item_type` +Files attached to a work item. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `type_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| ----------------- | -------------------------------------------- | -------- | ----------------------------------------------------------------------------- | +| `list` | `project_id`, `workitem_id` | — | Read-only | +| `read` | `project_id`, `workitem_id`, `attachment_id` | — | returns images and text inline; use download_url for anything else; Read-only | +| `download_url` | `project_id`, `workitem_id`, `attachment_id` | — | Read-only | +| `upload_from_url` | `project_id`, `workitem_id`, `url` | `name` | — | +| `delete` | `project_id`, `workitem_id`, `attachment_id` | — | Destructive | ---- +**Notes:** read supports PNG/JPEG/GIF/WEBP up to 5 MB and TXT/MD/CSV/HTML/XML/YAML/JSON up to 1 MB. Get attachment_id from the list action. upload_from_url fetches the file server-side, so the URL must be reachable without authentication and must not resolve to a private address. -### Worklogs +### `workitem_link` — Work item links -Time tracking for work items. All durations are in **minutes**. +External links attached to a work item. -#### `list_work_logs` +| Action | Required | Optional | Notes | +| ---------- | --------------------------------------------- | -------------------- | ----------- | +| `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `workitem_id`, `link_id` | — | Read-only | +| `create` | `project_id`, `workitem_id`, `url` | — | — | +| `update` | `project_id`, `workitem_id`, `link_id`, `url` | — | — | +| `delete` | `project_id`, `workitem_id`, `link_id` | — | Destructive | -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | +### `workitem_relation` — Work item relations -#### `create_work_log` +Relations between work items, and the definitions that type them. -| Parameter | Type | Required | Description | -| -------------- | ----------- | -------- | -------------------- | -| `project_id` | UUID string | **Yes** | | -| `work_item_id` | UUID string | **Yes** | | -| `duration` | integer | **Yes** | Minutes logged (≥ 0) | -| `description` | string | No | What was done | +| Action | Required | Optional | Notes | +| ------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `list` | `project_id`, `workitem_id` | — | Read-only | +| `create` | `project_id`, `workitem_id`, `workitem_ids` | `relation_type`, `relation_definition_id`, `relation_definition_label` | pass relation_type for a dependency, or definition id + label for a custom relation | +| `delete` | `project_id`, `workitem_id`, `related_workitem_id` | `is_dependency` | removes one relation; dependencies and custom relations are independent, so is_dependency must match the kind that was created (default false); Destructive | +| `list_definitions` | — | `is_default`, `is_active` | Read-only | +| `create_definition` | `name` | `outward`, `inward`, `is_active`, `color` | — | +| `update_definition` | `definition_id` | `name`, `outward`, `inward`, `is_active`, `color` | — | +| `delete_definition` | `definition_id` | — | Destructive | -#### `update_work_log` +**Notes:** Call list_definitions first and match the user's wording to an entry. A built_in_dependencies value (blocking, blocked_by, start_before, start_after, finish_before, finish_after) goes in relation_type; a custom definition needs its id in relation_definition_id and the matched outward or inward label in relation_definition_label, which sets direction. -| Parameter | Type | Required | -| -------------------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `work_log_id` | UUID string | **Yes** | -| `duration` / `description` | - | No | +### `work_log` — Work logs -#### `delete_work_log` +Time logged against a work item. -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | -| `work_log_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| -------- | ------------------------------------------ | ------------------------- | ----------- | +| `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | +| `create` | `project_id`, `workitem_id`, `duration` | `description` | — | +| `update` | `project_id`, `workitem_id`, `work_log_id` | `duration`, `description` | — | +| `delete` | `project_id`, `workitem_id`, `work_log_id` | — | Destructive | ---- +**Notes:** duration is in minutes. + +## Types, properties and estimates + +_Example prompt: “Create an Epic type for ENG, add a Customer impact property, and set up point estimates.”_ + +### `workitem_type` — Work item types + +Work item types, at project or workspace scope. -### States +| Action | Required | Optional | Notes | +| ------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| `list` | — | `project_id`, `cursor`, `per_page` | workspace scope when project_id is omitted; Read-only | +| `retrieve` | `workitem_type_id` | `project_id` | Read-only | +| `resolve` | `project_id`, `name` | — | finds or creates a named type usable in the project; never duplicates | +| `create` | `name` | `project_id`, `description`, `project_ids`, `is_active`, `external_source`, `external_id` | — | +| `update` | `workitem_type_id` | `project_id`, `name`, `description`, `project_ids`, `is_active`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `workitem_type_id` | `project_id` | Destructive | +| `import_to_project` | `project_id`, `workitem_type_ids` | — | links workspace types to a project | -Workflow states for a project's work items. +**Notes:** Omit project_id to work at workspace scope. A type's id is the type_id for `workitem create` and the workitem_type_id for `workitem_property list`. Prefer resolve over create when you just need a usable type such as Epic or Initiative: it handles both modes, matches exactly (case-sensitive, whitespace-stripped) and never duplicates. Where the workspace owns the vocabulary, creating a type on a project is rejected and importing is the only valid path -- resolve does that for you. -#### `list_states` / `create_state` / `retrieve_state` / `update_state` / `delete_state` +### `workitem_property` — Work item properties -All state tools accept `project_id`. Create and update accept: +Custom work item properties and their options. -| Field | Type | Required | Description | -| ------------- | ------ | ---------------- | --------------------------------------------------------------- | -| `name` | string | **Yes** (create) | Display name | -| `color` | string | **Yes** (create) | Hex color code, e.g., `#FF5733` | -| `group` | string | **Yes** (create) | `backlog` · `unstarted` · `started` · `completed` · `cancelled` | -| `description` | string | No | | +| Action | Required | Optional | Notes | +| ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `list` | — | `project_id`, `workitem_type_id`, `cursor`, `per_page` | no ids lists every workspace property in one call -- the fast path for PQL; Read-only | +| `retrieve` | `workitem_property_id` | `project_id`, `workitem_type_id` | Read-only | +| `create` | `display_name`, `property_type` | `project_id`, `workitem_type_id`, `description`, `relation_type`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `display_format`, `external_source`, `external_id` | — | +| `update` | `workitem_property_id` | `project_id`, `workitem_type_id`, `display_name`, `property_type`, `description`, `relation_type`, `is_required`, `is_multi`, `is_active`, `default_value`, `display_format`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `workitem_property_id` | `project_id`, `workitem_type_id` | Destructive | +| `manage_type_properties` | `workitem_type_id` | `project_id`, `attach_ids`, `detach_ids` | omit project_id where the workspace owns types; detach removes the association only, it does not delete the property | +| `list_options` | `property_id` | `project_id` | Read-only | +| `retrieve_option` | `property_id`, `option_id` | `project_id` | Read-only | +| `create_option` | `property_id`, `name` | `project_id`, `description`, `color`, `is_default`, `external_source`, `external_id` | — | +| `update_option` | `property_id`, `option_id` | `project_id`, `name`, `description`, `color`, `is_default`, `external_source`, `external_id` | — | +| `delete_option` | `property_id`, `option_id` | `project_id` | Destructive | +| `get_value` | `project_id`, `workitem_id`, `property_id` | — | Read-only | +| `set_value` | `project_id`, `workitem_id`, `property_id`, `value` | `external_source`, `external_id` | upsert; for a multi-value property this replaces every existing value | +| `delete_value` | `project_id`, `workitem_id`, `property_id` | — | Destructive | ---- +**Notes:** property_type is one of: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE, FORMULA. relation_type (for RELATION properties) is one of: ISSUE, USER, RELEASE, RICH_TEXT. A property id is what goes in a PQL cf["<id>"] filter; for OPTION properties the value is an option id. options takes a JSON array of {"name", "color", "is_default"} objects. display_format is required by TEXT (single-line, multi-line, readonly) and DATETIME (MMM dd, yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy/MM/dd) properties. A property lives with its type: where the workspace owns types, pass workitem_type_id without project_id and it is created in the workspace catalogue and associated for you. list resolves scope in this order: project_id + workitem_type_id is type-scoped (falling back to project-flat then workspace when empty), project_id alone is every property in the project, and neither is every workspace property. To filter by property name in PQL, call list with no ids -- one workspace-wide fetch beats iterating types -- then match display_name in memory to get the id for a cf[] condition. The \*\_value actions read and write a property on one work item: pass value in the type the property expects -- TEXT/URL/EMAIL/FILE as a string; DATETIME as a YYYY-MM-DD or YYYY-MM-DD HH:MM:SS string; DECIMAL as a number; BOOLEAN as true or false; OPTION and RELATION as an option or record id string, or an array of them when the property is multi-value. Send the value's own type, not a stringified form: "007" stays the text 007, whereas 7 is the number. -### Labels +### `project_estimate` — Project estimates -Tags for work items. +A project's estimate system and its points. -#### `list_labels` / `create_label` / `retrieve_label` / `update_label` / `delete_label` +| Action | Required | Optional | Notes | +| --------------- | ------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------------- | +| `retrieve` | `project_id` | — | a project has at most one estimate; Read-only | +| `create` | `project_id`, `name` | `type`, `description`, `last_used`, `external_source`, `external_id` | — | +| `update` | `project_id` | `name`, `description`, `external_source`, `external_id` | — | +| `delete` | `project_id` | — | Destructive | +| `link` | `project_id`, `estimate_id` | — | makes that estimate the project's active one | +| `list_points` | `project_id`, `estimate_id` | — | Read-only | +| `create_points` | `project_id`, `estimate_id`, `points` | — | — | +| `update_point` | `project_id`, `estimate_id`, `estimate_point_id` | `value`, `key`, `description`, `external_source`, `external_id` | — | +| `delete_point` | `project_id`, `estimate_id`, `estimate_point_id` | — | Destructive | -All label tools accept `project_id`. Create and update accept: +**Notes:** type is one of: categories, points, time. A point's `value` is its display label ("5", "XL") and its `key` is the sort order. points takes a JSON array such as [{"value": "1", "key": 0}]. To set a work item's estimate: retrieve to get the estimate_id, list_points to see the available values, then pass the chosen point's id to `workitem update` as estimate_point. -| Field | Type | Required | -| -------- | ----------- | ---------------- | -| `name` | string | **Yes** (create) | -| `color` | string | **Yes** (create) | -| `parent` | UUID string | No | +## Planning ---- +_Example prompt: “Move unfinished work from Sprint 14 to Sprint 15, then count it by priority.”_ + +### `cycle` — Cycles + +Cycles (time-boxed iterations) in a project. + +| Action | Required | Optional | Notes | +| -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `list` | `project_id` | `archived`, `status`, `cursor`, `per_page`, `order_by` | Read-only | +| `retrieve` | `project_id`, `cycle_id` | — | Read-only | +| `create` | `project_id`, `name`, `owned_by` | `description`, `start_date`, `end_date`, `timezone`, `external_source`, `external_id` | — | +| `update` | `project_id`, `cycle_id` | `name`, `description`, `start_date`, `end_date`, `owned_by`, `timezone`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id`, `cycle_id` | — | Destructive | +| `list_workitems` | `project_id`, `cycle_id` | `pql`, `order_by`, `cursor`, `per_page`, `expand`, `fields` | Read-only | +| `manage_workitems` | `project_id`, `cycle_id` | `add_ids`, `remove_ids` | pass at least one of add_ids or remove_ids; returns nothing, read back with list_workitems | +| `transfer_workitems` | `project_id`, `cycle_id`, `new_cycle_id` | — | moves everything to new_cycle_id | +| `complete` | `project_id`, `cycle_id` | — | sets end_date to today | +| `archive` | `project_id`, `cycle_id` | — | ends the cycle first if it is still running | +| `unarchive` | `project_id`, `cycle_id` | — | — | -### Cycles +**Notes:** status filters active cycles: current, upcoming, completed, draft, incomplete; it is ignored when archived is true. Dates are ISO 8601 (YYYY-MM-DD). owned_by is a member id. Optional Plane Query Language (PQL) filter. Examples: `priority = "urgent" AND assignee = currentUser()`, `stateGroup IN openStates() AND isOverdue()`. UUID fields (project, assignee, state, label, cycle, module, type, milestone, createdBy) need UUIDs — resolve a name to its UUID first if you only have a name or short identifier (e.g. `LSS` → `project list` and match `identifier` to get `id`). Call `get_pql_reference` for full PQL syntax before composing complex queries. -Time-boxed iterations (sprints). +### `module` — Modules -#### `list_cycles` +Modules (feature groupings) in a project. -Returns all cycles in a project including upcoming, active, and completed. +| Action | Required | Optional | Notes | +| ------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `list` | `project_id` | `archived`, `cursor`, `per_page`, `order_by` | Read-only | +| `retrieve` | `project_id`, `module_id` | — | Read-only | +| `create` | `project_id`, `name` | `description`, `start_date`, `target_date`, `status`, `lead`, `members`, `external_source`, `external_id` | — | +| `update` | `project_id`, `module_id` | `name`, `description`, `start_date`, `target_date`, `status`, `lead`, `members`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id`, `module_id` | — | Destructive | +| `list_workitems` | `project_id`, `module_id` | `pql`, `order_by`, `cursor`, `per_page`, `expand`, `fields` | Read-only | +| `manage_workitems` | `project_id`, `module_id` | `add_ids`, `remove_ids` | pass at least one of add_ids or remove_ids; returns nothing, read back with list_workitems | +| `archive` | `project_id`, `module_id` | — | — | +| `unarchive` | `project_id`, `module_id` | — | — | -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +**Notes:** status is one of: backlog, planned, in-progress, paused, completed, cancelled. Dates are ISO 8601 (YYYY-MM-DD). lead and members are member ids. Optional Plane Query Language (PQL) filter. Examples: `priority = "urgent" AND assignee = currentUser()`, `stateGroup IN openStates() AND isOverdue()`. UUID fields (project, assignee, state, label, cycle, module, type, milestone, createdBy) need UUIDs — resolve a name to its UUID first if you only have a name or short identifier (e.g. `LSS` → `project list` and match `identifier` to get `id`). Call `get_pql_reference` for full PQL syntax before composing complex queries. + +### `milestone` — Milestones + +Milestones within a project. + +| Action | Required | Optional | Notes | +| ------------------ | ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `list` | `project_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `milestone_id` | — | Read-only | +| `create` | `project_id`, `title` | `target_date`, `external_source`, `external_id` | — | +| `update` | `project_id`, `milestone_id` | `title`, `target_date`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id`, `milestone_id` | — | Destructive | +| `list_workitems` | `project_id`, `milestone_id` | `cursor`, `per_page` | Read-only | +| `manage_workitems` | `project_id`, `milestone_id` | `add_ids`, `remove_ids` | pass at least one of add_ids or remove_ids; returns nothing, read back with list_workitems | -#### `list_archived_cycles` +**Notes:** target_date is ISO 8601 (YYYY-MM-DD). add_ids and remove_ids take work item UUIDs. + +### `initiative` — Initiatives + +Workspace initiatives. + +| Action | Required | Optional | Notes | +| ----------------- | ------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `list` | — | — | returns every initiative; this endpoint does not paginate; Read-only | +| `retrieve` | `initiative_id` | — | Read-only | +| `create` | `name` | `description_html`, `start_date`, `end_date`, `state`, `lead` | — | +| `update` | `initiative_id` | `name`, `description_html`, `start_date`, `end_date`, `state`, `lead` | only the fields you pass are changed | +| `delete` | `initiative_id` | — | Destructive | +| `list_projects` | `initiative_id` | `cursor`, `per_page` | Read-only | +| `add_projects` | `initiative_id`, `project_ids` | — | returns nothing, read back with list_projects | +| `remove_projects` | `initiative_id`, `project_ids` | — | returns nothing, read back with list_projects; Destructive | -Returns archived cycles only. +**Notes:** state is one of: DRAFT, PLANNED, ACTIVE, COMPLETED, CLOSED. Dates are ISO 8601 (YYYY-MM-DD). lead is a member id. project_ids takes project UUIDs. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +## Releases -#### `create_cycle` +_Example prompt: “Create release v1.8.0, add ENG-40 through ENG-45, and draft its changelog.”_ -| Parameter | Type | Required | Description | -| ------------- | ----------- | -------- | ------------ | -| `project_id` | UUID string | **Yes** | | -| `name` | string | **Yes** | Cycle name | -| `start_date` | string | No | `YYYY-MM-DD` | -| `end_date` | string | No | `YYYY-MM-DD` | -| `description` | string | No | | +### `release` — Releases -#### `retrieve_cycle` +Releases in the workspace. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `cycle_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `list` | — | `cursor`, `per_page` | Read-only | +| `retrieve` | `release_id` | — | Read-only | +| `create` | `name` | `description_html`, `status`, `release_date`, `target_date`, `tag_id`, `lead_id`, `is_prerelease`, `external_source`, `external_id` | — | +| `update` | `release_id` | `name`, `description_html`, `status`, `release_date`, `target_date`, `tag_id`, `lead_id`, `is_prerelease` | only the fields you pass are changed | +| `delete` | `release_id` | — | Destructive | +| `get_changelog` | `release_id` | — | Read-only | +| `update_changelog` | `release_id` | `description_html`, `description_stripped` | — | +| `list_workitems` | `release_id` | `cursor`, `per_page` | Read-only | +| `manage_workitems` | `release_id` | `add_ids`, `remove_ids` | pass at least one of add_ids or remove_ids; returns nothing, read back with list_workitems | -#### `update_cycle` / `delete_cycle` +**Notes:** status is one of: unreleased, released, cancelled, defaulting to unreleased. release_date is what the Plane UI labels "Target date" (YYYY-MM-DD); target_date is a separate stored date that the UI does not show. tag_id comes from `release_tag list`, lead_id from `member list_workspace`. For the changelog pass description_html, or description_stripped for plain text. A changelog is created empty with the release, so get_changelog always returns one. -Accept `project_id` and `cycle_id`. +### `release_tag` — Release tags -#### `add_work_items_to_cycle` +Release tags (version markers). -| Parameter | Type | Required | -| --------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `cycle_id` | UUID string | **Yes** | -| `work_item_ids` | UUID[] | **Yes** | +| Action | Required | Optional | Notes | +| ---------- | --------- | -------------------------------------------------- | ------------------------------------ | +| `list` | — | `cursor`, `per_page` | Read-only | +| `retrieve` | `tag_id` | — | Read-only | +| `create` | `version` | `description`, `commit_hash`, `git_tag` | — | +| `update` | `tag_id` | `version`, `description`, `commit_hash`, `git_tag` | only the fields you pass are changed | +| `delete` | `tag_id` | — | Destructive | -#### `remove_work_item_from_cycle` +**Notes:** version is a version string such as "v1.2.0". A tag id is what release takes as tag_id. -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `cycle_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | +### `release_label` — Release labels -#### `list_cycle_work_items` +Release labels, workspace palette and per release. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `cycle_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| -------- | ------------------------- | ---------------------------------- | ----------------------------------------------------------- | +| `list` | — | `release_id`, `cursor`, `per_page` | the workspace palette unless release_id is given; Read-only | +| `create` | `name` | `color`, `sort_order` | adds to the workspace palette | +| `update` | `label_id` | `name`, `color`, `sort_order` | — | +| `delete` | `label_id` | — | removes it from the palette entirely; Destructive | +| `attach` | `release_id`, `label_ids` | — | returns nothing, read back with list | +| `detach` | `release_id`, `label_ids` | — | returns nothing, read back with list; Destructive | -#### `transfer_cycle_work_items` +**Notes:** color is a hex code such as #4E5355. label_ids takes palette label ids. Detaching a label leaves it in the palette; delete removes it for everyone. -Moves all incomplete work items from one cycle to another. +## Projects and workspace -| Parameter | Type | Required | Description | -| -------------- | ----------- | -------- | ------------ | -| `project_id` | UUID string | **Yes** | | -| `cycle_id` | UUID string | **Yes** | Source cycle | -| `new_cycle_id` | UUID string | **Yes** | Target cycle | +_Example prompt: “List my active projects and show the work currently assigned to me.”_ ---- +### `project` — Projects -### Modules +Projects in a workspace. -Feature groupings within a project. +| Action | Required | Optional | Notes | +| ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `list` | — | `cursor`, `per_page`, `order_by` | trimmed fields; use retrieve for full detail; Read-only | +| `retrieve` | `project_id` | — | Read-only | +| `create` | `name`, `identifier` | `description`, `project_lead`, `default_assignee`, `emoji`, `cover_image`, `timezone`, `archive_in`, `close_in`, `external_source`, `external_id` | — | +| `update` | `project_id` | `name`, `description`, `identifier`, `project_lead`, `default_assignee`, `emoji`, `cover_image`, `network`, `timezone`, `archive_in`, `close_in`, `default_state`, `estimate`, `is_time_tracking_enabled`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id` | — | Destructive | +| `archive` | `project_id` | — | — | +| `unarchive` | `project_id` | — | — | +| `worklog_summary` | `project_id` | — | Read-only | +| `get_features` | `project_id` | — | Read-only | +| `update_features` | `project_id` | `modules`, `cycles`, `views`, `pages`, `intakes`, `workitem_types`, `epics`, `parallel_cycles`, `project_updates`, `workflows` | toggles project features on or off | -#### `list_modules` / `list_archived_modules` +**Notes:** identifier is the short work item prefix, such as ENG. network is 0 for secret or 2 for public. project_lead and default_assignee are member ids -- get them from `member list_workspace`. Feature toggles are booleans; omitted ones are left as they are. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +### `state` — Workflow states -#### `create_module` +Workflow states within a project. -| Parameter | Type | Required | -| ------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `name` | string | **Yes** | -| `description` | string | No | -| `start_date` | string | No | -| `target_date` | string | No | -| `lead` | UUID string | No | -| `members` | UUID[] | No | +| Action | Required | Optional | Notes | +| ---------- | ----------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------ | +| `list` | `project_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `state_id` | — | Read-only | +| `create` | `project_id`, `name`, `color` | `description`, `sequence`, `group`, `is_triage`, `default`, `external_source`, `external_id` | — | +| `update` | `project_id`, `state_id` | `name`, `color`, `description`, `sequence`, `group`, `is_triage`, `default` | only the fields you pass are changed | +| `delete` | `project_id`, `state_id` | — | Destructive | -#### `retrieve_module` / `update_module` / `delete_module` / `archive_module` +**Notes:** group is one of: backlog, unstarted, started, completed, cancelled, triage. color is a hex code such as #EF4444. -Accept `project_id` and `module_id`. +### `label` — Labels -#### `add_work_items_to_module` +Labels within a project. -| Parameter | Type | Required | -| --------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `module_id` | UUID string | **Yes** | -| `work_item_ids` | UUID[] | **Yes** | +| Action | Required | Optional | Notes | +| ---------- | ------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------ | +| `list` | `project_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `label_id` | — | Read-only | +| `create` | `project_id`, `name` | `color`, `description`, `parent`, `sort_order`, `external_source`, `external_id` | — | +| `update` | `project_id`, `label_id` | `name`, `color`, `description`, `parent`, `sort_order`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `project_id`, `label_id` | — | Destructive | -#### `remove_work_item_from_module` +**Notes:** color is a hex code such as #EF4444. parent is the UUID of another label, for nesting. -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `module_id` | UUID string | **Yes** | -| `work_item_id` | UUID string | **Yes** | +### `member` — Members and roles -#### `list_module_work_items` +Workspace and project members, and role definitions. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `module_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | +| `me` | — | — | the authenticated user; Read-only | +| `list_workspace` | — | `first_name`, `last_name`, `email`, `display_name`, `role_slug`, `is_active`, `is_bot`, `cursor`, `per_page`, `order_by` | name filters match case-insensitively and combine with AND; Read-only | +| `list_project` | `project_id` | — | Read-only | +| `list_roles` | — | `namespace`, `cursor`, `per_page` | Read-only | +| `retrieve_role` | `role_id` | — | Read-only | ---- +**Notes:** namespace is 'workspace' (Owner/Admin/Member/Guest) or 'project' (Admin/Contributor/Commenter/Guest); omit for both. A role slug is stable but not globally unique -- key on (namespace, slug). -### Epics +### `workspace` — Workspace settings -Large work items that group related items. The server resolves the Epic work item type automatically. +Workspace-level feature flags. -#### `list_epics` / `create_epic` / `retrieve_epic` / `update_epic` / `delete_epic` +| Action | Required | Optional | Notes | +| ----------------- | -------- | --------------------------------------------------------------------- | -------------------------------------------------- | +| `get_features` | — | — | feature flags for the current workspace; Read-only | +| `update_features` | — | `project_grouping`, `initiatives`, `teams`, `customers`, `wiki`, `pi` | only the flags you pass are changed | -| Parameter | Type | Required | -| ----------------------- | ----------- | ------------------- | -| `project_id` | UUID string | **Yes** | -| `epic_id` | UUID string | Varies by operation | -| name, description, etc. | - | Varies | +**Notes:** For a project's feature flags use `project get_features` and `project update_features`. ---- +### `intake` — Intake queue -### Milestones +The intake (triage) queue for a project. -Point-in-time goals within a project. +| Action | Required | Optional | Notes | +| ---------- | --------------------------- | ------------------------------------------------------------------ | ------------------------------------- | +| `list` | `project_id` | `cursor`, `per_page` | Read-only | +| `retrieve` | `project_id`, `workitem_id` | — | Read-only | +| `create` | `project_id`, `name` | `description_html`, `priority` | — | +| `update` | `project_id`, `workitem_id` | `status`, `snoozed_till`, `duplicate_to`, `source`, `source_email` | pass status to make a triage decision | +| `delete` | `project_id`, `workitem_id` | — | Destructive | -#### `list_milestones` / `create_milestone` / `retrieve_milestone` / `update_milestone` / `delete_milestone` +**Notes:** workitem_id is the `issue` field of an intake record, not the record's own id. status: -2 pending, -1 declined, 0 snoozed (needs snoozed_till), 1 accepted, 2 duplicate (needs duplicate_to). priority is one of: urgent, high, medium, low, none. -| Parameter | Type | Required | -| -------------- | ----------- | ---------------- | -| `project_id` | UUID string | **Yes** | -| `milestone_id` | UUID string | Varies | -| `name` | string | **Yes** (create) | +### `page` — Pages -#### `add_work_items_to_milestone` +Pages at workspace or project scope. -| Parameter | Type | Required | -| --------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `milestone_id` | UUID string | **Yes** | -| `work_item_ids` | UUID[] | **Yes** | +| Action | Required | Optional | Notes | +| ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `list` | — | `project_id`, `cursor`, `per_page` | workspace pages unless project_id is given; Read-only | +| `retrieve` | `page_id` | `project_id` | Read-only | +| `create` | `name`, `description_html` | `project_id`, `access`, `color`, `is_locked`, `external_source`, `external_id` | — | +| `list_workitem_pages` | `project_id`, `workitem_id` | — | Read-only | +| `attach_to_workitem` | `project_id`, `workitem_id`, `page_id` | — | — | +| `detach_from_workitem` | `project_id`, `workitem_id`, `workitem_page_id` | — | workitem_page_id is the link id from list_workitem_pages, not the page id; Destructive | -#### `remove_work_items_from_milestone` +**Notes:** description_html is the page body as HTML. access is the page access level. Omit project_id to work with workspace-level pages. -| Parameter | Type | Required | -| --------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `milestone_id` | UUID string | **Yes** | -| `work_item_ids` | UUID[] | **Yes** | +## Customers -#### `list_milestone_work_items` +_Example prompt: “Create Acme as a customer and link its checkout request to the relevant work items.”_ -| Parameter | Type | Required | -| -------------- | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `milestone_id` | UUID string | **Yes** | +### `customer` — Customers ---- +Customers in the workspace. -### Initiatives +| Action | Required | Optional | Notes | +| ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `list` | — | `query`, `cursor`, `per_page` | Read-only | +| `retrieve` | `customer_id` | — | Read-only | +| `create` | `name` | `description_html`, `email`, `website_url`, `domain`, `employees`, `stage`, `contract_status`, `revenue`, `external_source`, `external_id` | upsert: matches on external_source + external_id, else on name, so it never duplicates | +| `update` | `customer_id` | `name`, `description_html`, `email`, `website_url`, `domain`, `employees`, `stage`, `contract_status`, `revenue`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | — | `customer_id`, `external_source`, `external_id` | address by customer_id, or by external_source plus external_id; Destructive | +| `list_workitems` | `customer_id` | `customer_request_id`, `search` | Read-only | +| `manage_workitems` | `customer_id` | `link_ids`, `unlink_ids`, `customer_request_id` | pass at least one of link_ids or unlink_ids; returns nothing, read back with list_workitems | -Workspace-scoped strategic goals that span multiple projects. +**Notes:** domain is the customer's industry, shown as "Industry" in Plane -- the website goes in website_url. stage renders as one of: lead, sales_qualified_lead, contract_negotiation, closed_won, closed_lost. contract_status renders as one of: active, pre_contract, signed, inactive. Both are stored free-form; anything else is kept but not displayed. revenue is annual revenue as a string. -#### `list_initiatives` / `create_initiative` / `retrieve_initiative` / `update_initiative` / `delete_initiative` +### `customer_request` — Customer requests -Initiatives are workspace-scoped - no `project_id` required. `retrieve_initiative`, `update_initiative`, and `delete_initiative` accept an `initiative_id` UUID. +Requests raised by a customer. ---- +| Action | Required | Optional | Notes | +| ---------- | --------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- | +| `list` | `customer_id` | `query`, `cursor`, `per_page` | Read-only | +| `retrieve` | `customer_id`, `request_id` | — | Read-only | +| `create` | `customer_id`, `name` | `description_html`, `link`, `workitem_ids` | workitem_ids can only be set here; change links afterwards with customer manage_workitems | +| `update` | `customer_id`, `request_id` | `name`, `description_html`, `link` | only the fields you pass are changed | +| `delete` | `customer_id`, `request_id` | — | Destructive | -### Intake +**Notes:** link is a URL associated with the request. workitem_ids is never echoed back -- read the links with `customer list_workitems`. -Triage queue for incoming work items before they enter a project. +### `customer_property` — Customer properties -#### `list_intake_work_items` +Custom properties on customers. -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | +| Action | Required | Optional | Notes | +| ------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | +| `list` | — | `cursor`, `per_page` | Read-only | +| `retrieve` | `property_id` | — | Read-only | +| `create` | `display_name`, `property_type` | `relation_type`, `description`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `display_format`, `external_source`, `external_id` | — | +| `update` | `property_id` | `display_name`, `relation_type`, `description`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `external_source`, `external_id` | only the fields you pass are changed | +| `delete` | `property_id` | — | Destructive | +| `get_values` | `customer_id` | `property_id` | omit property_id to read them all; Read-only | +| `set_values` | `customer_id`, `values` | — | replaces the values of the properties named; others keep theirs | -#### `create_intake_work_item` +**Notes:** display_name is the user-facing label and must be unique in the workspace -- the stored name is derived from it. property_type is one of: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE, FORMULA. relation_type (required for RELATION) is one of: ISSUE, USER, RELEASE, RICH_TEXT. display_format is required by TEXT (single-line, multi-line, readonly) and DATETIME (MMM dd, yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy/MM/dd). options takes a JSON array of {"name", "description", "is_default"} objects. values takes a JSON object of property id to a list of strings, e.g. {"<id>": ["Enterprise"]} -- every value is a string whatever the property type, and a single-item list unless is_multi. -| Parameter | Type | Required | -| ------------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `name` | string | **Yes** | -| `description_html` | string | No | +## Query -#### `retrieve_intake_work_item` / `update_intake_work_item` / `delete_intake_work_item` - -Accept `project_id` and `work_item_id`. - ---- +_Example prompt: “Show the PQL syntax for overdue, in-progress work assigned to the current user.”_ -### Pages +### `get_pql_reference` — PQL reference -Wiki-style documents. Pages can be workspace-scoped or project-scoped. +Plane Query Language (PQL) syntax reference. Call this before composing a `pql` filter for the workitem list, list_archived or count actions. -#### `retrieve_workspace_page` +Takes `detail`: `full` (default) or `brief`. This tool has no `action` parameter. -| Parameter | Type | Required | -| --------- | ----------- | -------- | -| `page_id` | UUID string | **Yes** | +**Notes:** detail 'full' gives operators, functions, common mistakes and worked examples; 'brief' gives the compact field and operator quick reference. -#### `retrieve_project_page` +## Retired tool names -| Parameter | Type | Required | -| ------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `page_id` | UUID string | **Yes** | +Plane MCP server 0.3.0 consolidated 177 per-operation tools into 28 resource tools. The 169 aliases below still resolve, stay hidden from tool listings, and accept their original parameter names. The server logs each alias resolution. Seven retired names cannot be mapped to one action and instead return a message naming the replacement. -#### `create_workspace_page` +### Names without an alias -| Parameter | Type | Required | -| ------------------ | ------ | -------- | -| `name` | string | **Yes** | -| `description_html` | string | No | +| Retired name | Use instead | +| ---------------------------- | ----------------------------------------------------------- | +| `manage_customer_work_items` | `customer manage_workitems` with `link_ids` or `unlink_ids` | +| `manage_cycle_archive` | `cycle archive` or `cycle unarchive` | +| `manage_initiative_projects` | `initiative add_projects` or `initiative remove_projects` | +| `manage_module_archive` | `module archive` or `module unarchive` | +| `manage_project_archive` | `project archive` or `project unarchive` | +| `manage_release_labels` | `release_label attach` or `release_label detach` | +| `manage_release_work_items` | `release manage_workitems` with `add_ids` or `remove_ids` | + +### Alias table + +::: details Show all 169 aliases -#### `create_project_page` +#### `customer` -| Parameter | Type | Required | -| ------------------ | ----------- | -------- | -| `project_id` | UUID string | **Yes** | -| `name` | string | **Yes** | -| `description_html` | string | No | +| Retired name | Now | +| -------------------------- | ------------------------- | +| `list_customers` | `customer list` | +| `retrieve_customer` | `customer retrieve` | +| `create_customer` | `customer create` | +| `update_customer` | `customer update` | +| `delete_customer` | `customer delete` | +| `list_customer_work_items` | `customer list_workitems` | + +#### `customer_property` + +| Retired name | Now | +| ------------------------------ | ------------------------------ | +| `list_customer_properties` | `customer_property list` | +| `retrieve_customer_property` | `customer_property retrieve` | +| `create_customer_property` | `customer_property create` | +| `update_customer_property` | `customer_property update` | +| `delete_customer_property` | `customer_property delete` | +| `get_customer_property_values` | `customer_property get_values` | +| `set_customer_property_values` | `customer_property set_values` | + +#### `customer_request` + +| Retired name | Now | +| --------------------------- | --------------------------- | +| `list_customer_requests` | `customer_request list` | +| `retrieve_customer_request` | `customer_request retrieve` | +| `create_customer_request` | `customer_request create` | +| `update_customer_request` | `customer_request update` | +| `delete_customer_request` | `customer_request delete` | + +#### `cycle` + +| Retired name | Now | +| --------------------------- | -------------------------- | +| `list_cycles` | `cycle list` | +| `retrieve_cycle` | `cycle retrieve` | +| `create_cycle` | `cycle create` | +| `update_cycle` | `cycle update` | +| `delete_cycle` | `cycle delete` | +| `list_cycle_work_items` | `cycle list_workitems` | +| `manage_cycle_work_items` | `cycle manage_workitems` | +| `transfer_cycle_work_items` | `cycle transfer_workitems` | +| `complete_cycle` | `cycle complete` | + +#### `initiative` + +| Retired name | Now | +| -------------------------- | -------------------------- | +| `list_initiatives` | `initiative list` | +| `retrieve_initiative` | `initiative retrieve` | +| `create_initiative` | `initiative create` | +| `update_initiative` | `initiative update` | +| `delete_initiative` | `initiative delete` | +| `list_initiative_projects` | `initiative list_projects` | + +#### `intake` + +| Retired name | Now | +| --------------------------- | ----------------- | +| `list_intake_work_items` | `intake list` | +| `retrieve_intake_work_item` | `intake retrieve` | +| `create_intake_work_item` | `intake create` | +| `update_intake_work_item` | `intake update` | +| `delete_intake_work_item` | `intake delete` | + +#### `label` + +| Retired name | Now | +| ---------------- | ---------------- | +| `list_labels` | `label list` | +| `retrieve_label` | `label retrieve` | +| `create_label` | `label create` | +| `update_label` | `label update` | +| `delete_label` | `label delete` | + +#### `member` + +| Retired name | Now | +| ----------------------- | ----------------------- | +| `get_me` | `member me` | +| `get_workspace_members` | `member list_workspace` | +| `get_project_members` | `member list_project` | +| `list_roles` | `member list_roles` | +| `retrieve_role` | `member retrieve_role` | + +#### `milestone` + +| Retired name | Now | +| ----------------------------- | ---------------------------- | +| `list_milestones` | `milestone list` | +| `retrieve_milestone` | `milestone retrieve` | +| `create_milestone` | `milestone create` | +| `update_milestone` | `milestone update` | +| `delete_milestone` | `milestone delete` | +| `list_milestone_work_items` | `milestone list_workitems` | +| `manage_milestone_work_items` | `milestone manage_workitems` | + +#### `module` + +| Retired name | Now | +| -------------------------- | ------------------------- | +| `list_modules` | `module list` | +| `retrieve_module` | `module retrieve` | +| `create_module` | `module create` | +| `update_module` | `module update` | +| `delete_module` | `module delete` | +| `list_module_work_items` | `module list_workitems` | +| `manage_module_work_items` | `module manage_workitems` | + +#### `page` + +| Retired name | Now | +| ---------------------------- | --------------------------- | +| `list_pages` | `page list` | +| `retrieve_page` | `page retrieve` | +| `create_page` | `page create` | +| `list_work_item_pages` | `page list_workitem_pages` | +| `attach_page_to_work_item` | `page attach_to_workitem` | +| `detach_page_from_work_item` | `page detach_from_workitem` | + +#### `project` + +| Retired name | Now | +| ----------------------------- | ------------------------- | +| `list_projects` | `project list` | +| `retrieve_project` | `project retrieve` | +| `create_project` | `project create` | +| `update_project` | `project update` | +| `delete_project` | `project delete` | +| `get_project_worklog_summary` | `project worklog_summary` | +| `update_project_features` | `project update_features` | + +#### `project_estimate` + +| Retired name | Now | +| -------------------------------- | -------------------------------- | +| `get_project_estimate` | `project_estimate retrieve` | +| `create_project_estimate` | `project_estimate create` | +| `update_project_estimate` | `project_estimate update` | +| `delete_project_estimate` | `project_estimate delete` | +| `link_estimate_to_project` | `project_estimate link` | +| `list_project_estimate_points` | `project_estimate list_points` | +| `create_project_estimate_points` | `project_estimate create_points` | +| `update_project_estimate_point` | `project_estimate update_point` | +| `delete_project_estimate_point` | `project_estimate delete_point` | + +#### `release` + +| Retired name | Now | +| -------------------------- | -------------------------- | +| `list_releases` | `release list` | +| `retrieve_release` | `release retrieve` | +| `create_release` | `release create` | +| `update_release` | `release update` | +| `delete_release` | `release delete` | +| `get_release_changelog` | `release get_changelog` | +| `update_release_changelog` | `release update_changelog` | +| `list_release_work_items` | `release list_workitems` | + +#### `release_label` + +| Retired name | Now | +| ---------------------- | ---------------------- | +| `list_release_labels` | `release_label list` | +| `create_release_label` | `release_label create` | +| `update_release_label` | `release_label update` | +| `delete_release_label` | `release_label delete` | + +#### `release_tag` + +| Retired name | Now | +| ---------------------- | ---------------------- | +| `list_release_tags` | `release_tag list` | +| `retrieve_release_tag` | `release_tag retrieve` | +| `create_release_tag` | `release_tag create` | +| `update_release_tag` | `release_tag update` | +| `delete_release_tag` | `release_tag delete` | + +#### `state` + +| Retired name | Now | +| ---------------- | ---------------- | +| `list_states` | `state list` | +| `retrieve_state` | `state retrieve` | +| `create_state` | `state create` | +| `update_state` | `state update` | +| `delete_state` | `state delete` | + +#### `work_log` + +| Retired name | Now | +| ----------------- | ----------------- | +| `list_work_logs` | `work_log list` | +| `create_work_log` | `work_log create` | +| `update_work_log` | `work_log update` | +| `delete_work_log` | `work_log delete` | + +#### `workitem` + +| Retired name | Now | +| ---------------------------------- | --------------------------------- | +| `list_work_items` | `workitem list` | +| `list_archived_work_items` | `workitem list_archived` | +| `retrieve_work_item` | `workitem retrieve` | +| `retrieve_work_item_by_identifier` | `workitem retrieve_by_identifier` | +| `search_work_items` | `workitem search` | +| `count_work_items` | `workitem count` | +| `create_work_item` | `workitem create` | +| `update_work_item` | `workitem update` | +| `delete_work_item` | `workitem delete` | +| `manage_work_item_archive` | `workitem archive` | +| `manage_work_item_assignee` | `workitem manage_assignee` | +| `manage_work_item_label` | `workitem manage_label` | + +#### `workitem_activity` + +| Retired name | Now | +| ----------------------------- | ---------------------------- | +| `list_work_item_activities` | `workitem_activity list` | +| `retrieve_work_item_activity` | `workitem_activity retrieve` | + +#### `workitem_attachment` + +| Retired name | Now | +| --------------------------------------- | ------------------------------------- | +| `list_work_item_attachments` | `workitem_attachment list` | +| `read_work_item_attachment` | `workitem_attachment read` | +| `get_work_item_attachment_download_url` | `workitem_attachment download_url` | +| `upload_work_item_attachment_from_url` | `workitem_attachment upload_from_url` | +| `delete_work_item_attachment` | `workitem_attachment delete` | + +#### `workitem_comment` + +| Retired name | Now | +| ---------------------------- | --------------------------- | +| `list_work_item_comments` | `workitem_comment list` | +| `retrieve_work_item_comment` | `workitem_comment retrieve` | +| `create_work_item_comment` | `workitem_comment create` | +| `update_work_item_comment` | `workitem_comment update` | +| `delete_work_item_comment` | `workitem_comment delete` | + +#### `workitem_link` + +| Retired name | Now | +| ------------------------- | ------------------------ | +| `list_work_item_links` | `workitem_link list` | +| `retrieve_work_item_link` | `workitem_link retrieve` | +| `create_work_item_link` | `workitem_link create` | +| `update_work_item_link` | `workitem_link update` | +| `delete_work_item_link` | `workitem_link delete` | + +#### `workitem_property` + +| Retired name | Now | +| ------------------------------------ | ------------------------------------------ | +| `list_work_item_properties` | `workitem_property list` | +| `retrieve_work_item_property` | `workitem_property retrieve` | +| `create_work_item_property` | `workitem_property create` | +| `update_work_item_property` | `workitem_property update` | +| `delete_work_item_property` | `workitem_property delete` | +| `manage_work_item_type_properties` | `workitem_property manage_type_properties` | +| `list_work_item_property_options` | `workitem_property list_options` | +| `retrieve_work_item_property_option` | `workitem_property retrieve_option` | +| `create_work_item_property_option` | `workitem_property create_option` | +| `update_work_item_property_option` | `workitem_property update_option` | +| `delete_work_item_property_option` | `workitem_property delete_option` | +| `get_work_item_property_value` | `workitem_property get_value` | +| `set_work_item_property_value` | `workitem_property set_value` | +| `delete_work_item_property_value` | `workitem_property delete_value` | + +#### `workitem_relation` + +| Retired name | Now | +| -------------------------------------- | ------------------------------------- | +| `list_work_item_relations` | `workitem_relation list` | +| `create_work_item_relation` | `workitem_relation create` | +| `remove_work_item_relation` | `workitem_relation delete` | +| `list_work_item_relation_definitions` | `workitem_relation list_definitions` | +| `create_work_item_relation_definition` | `workitem_relation create_definition` | +| `update_work_item_relation_definition` | `workitem_relation update_definition` | +| `delete_work_item_relation_definition` | `workitem_relation delete_definition` | + +#### `workitem_type` + +| Retired name | Now | +| ----------------------------------- | --------------------------------- | +| `list_work_item_types` | `workitem_type list` | +| `retrieve_work_item_type` | `workitem_type retrieve` | +| `resolve_work_item_type` | `workitem_type resolve` | +| `create_work_item_type` | `workitem_type create` | +| `update_work_item_type` | `workitem_type update` | +| `delete_work_item_type` | `workitem_type delete` | +| `import_work_item_types_to_project` | `workitem_type import_to_project` | + +#### `workspace` + +| Retired name | Now | +| --------------------------- | --------------------------- | +| `get_features` | `workspace get_features` | +| `update_workspace_features` | `workspace update_features` | + +::: + +## See also + +- [Set up the MCP server](/dev-tools/mcp-server) +- [Self-host the MCP server](/dev-tools/mcp-server-self-host) +- [Tool architecture and extension guide](https://github.com/makeplane/plane-mcp-server/blob/v0.3.0/plane_mcp/tools/README.md) diff --git a/docs/dev-tools/mcp-server.md b/docs/dev-tools/mcp-server.md index 6e9e55c6..0d95eb0d 100644 --- a/docs/dev-tools/mcp-server.md +++ b/docs/dev-tools/mcp-server.md @@ -1,458 +1,509 @@ --- title: MCP server -description: Connect Cursor, VS Code, Claude, Windsurf, and Zed to your Plane workspace. Create work items, manage cycles, search across projects — all through natural language. -keywords: plane, developer tools, integrations, extensions, mcp server, protocol, integration +description: Connect Claude, ChatGPT, Codex, Cursor, VS Code, Windsurf, Zed, and Antigravity to Plane over MCP. Endpoints, OAuth and access-token auth, per-client setup, security, and troubleshooting. +keywords: plane mcp server, model context protocol, plane ai tools, claude plane, cursor plane, chatgpt plane, codex plane, mcp oauth, mcp access token --- # MCP server -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that defines how AI applications discover and call external tools. Any client that speaks MCP can talk to any server that speaks MCP. +Use Plane from the AI tool you already work in to create work items, plan cycles, and query projects in natural +language. The server is [open source](https://github.com/makeplane/plane-mcp-server) under the MIT license. -The Plane MCP Server is a bridge that lets AI models interact with Plane. It exposes Plane's full API surface as MCP tools, so your AI tool can create work items, manage sprints, track time, and organise work without you leaving your editor or chat interface. - -## Transport modes +::: tip Hosted server +Connect to `https://mcp.plane.so/http/mcp` and sign in with your Plane account. +::: -The server supports four transport modes. The right one depends on your deployment and usecase. +::: tip +Just want to connect your AI tool? Use the [short setup guide](https://docs.plane.so/ai/mcp-server). +::: -| Transport | For | Auth method | How to start | -| ------------------- | --------------------------------- | -------------------------- | ------------------------ | -| HTTP with OAuth | Plane Cloud users, simplest setup | Browser-based OAuth flow | `plane-mcp-server http` | -| HTTP with PAT Token | Automated workflows, CI/CD | API key in request headers | `plane-mcp-server http` | -| Local Stdio | Local dev, self-hosted Plane | Environment variables | `plane-mcp-server stdio` | -| SSE (Legacy) | Existing integrations | Browser-based OAuth flow | `plane-mcp-server http` | +## How it works -## Authentication model +[Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard for how AI clients discover and call +external tools. The Plane MCP server sits between your client and Plane's REST API, then acts as the signed-in user. -The server has three authentication mechanisms, one per transport variant. +Version 0.3.0 exposes **28 tools, one per resource, covering 183 actions**. Pass `action` to select an operation: -### OAuth auth (HTTP with OAuth, SSE) +```python +workitem(action="create", project_id=..., name="Fix login") +workitem(action="list", project_id=..., pql='stateGroup = "started"') +cycle(action="archive", project_id=..., cycle_id=...) +``` -For cloud deployments, the server acts as an **OAuth proxy** to Plane's OAuth system: +Every tool description lists its actions and marks parameters as required or optional. Tools also carry MCP +`readOnlyHint` and `destructiveHint` annotations derived from their actions. -1. The MCP client redirects the user to the Plane OAuth authorization page -2. The user logs into Plane and grants access -3. Plane returns an OAuth token which the server validates by calling `/api/v1/users/me/` -4. Subsequent MCP requests carry this token, from which the server extracts the workspace slug +### Hosted or self-hosted -The server supports OAuth redirect URIs for all major MCP clients: -`cursor://`, `vscode://`, `vscode-insiders://`, `windsurf://`, `claude://` +Plane Cloud users can connect to `mcp.plane.so`. For self-hosted Plane, run locally with `PLANE_BASE_URL` set to your +instance, or [deploy your own server](/dev-tools/mcp-server-self-host). -### Header auth (HTTP with PAT Token) +## What you can do -For automated workflows, the MCP client sends two headers with every request: +- [Work items](/dev-tools/mcp-server-tools#work-items): create, update, search, comment, attach, link, relate, nest, and + log time. +- [Types, properties, and estimates](/dev-tools/mcp-server-tools#types-properties-and-estimates): manage types, + custom properties, and estimates. +- [Planning](/dev-tools/mcp-server-tools#planning): plan cycles, modules, milestones, and initiatives. +- [Releases](/dev-tools/mcp-server-tools#releases): manage tags, labels, work items, and changelogs. +- [Projects and workspace](/dev-tools/mcp-server-tools#projects-and-workspace): manage projects, states, labels, members, + pages, features, and intake. +- [Customers](/dev-tools/mcp-server-tools#customers): manage customers, requests, properties, and linked work. +- [Query](/dev-tools/mcp-server-tools#query): retrieve the PQL language reference before composing filters. -- `x-api-key` - a Plane API token -- `x-workspace-slug` - the workspace identifier +### Query with PQL -The server validates the API key against Plane's `/api/v1/users/me/` endpoint on each request. No browser interaction required. +`workitem list`, `workitem list_archived`, `workitem count`, `cycle list_workitems`, and `module list_workitems` accept +`pql`. UUID-backed fields require UUIDs, so resolve names first. Call `get_pql_reference` with `detail="brief"` or +`detail="full"`; see [Plane Query Language](https://docs.plane.so/core-concepts/issues/plane-query-language). -### Environment variable auth (stdio) +There are no separate epic tools. Follow the [epics recipe](/dev-tools/mcp-server-tools#epics). -For stdio mode, credentials are read from environment variables at startup: +## Endpoints and authentication -- `PLANE_API_KEY` - your Plane API token -- `PLANE_WORKSPACE_SLUG` - your workspace identifier -- `PLANE_BASE_URL` - API URL for self-hosted instances (defaults to `https://api.plane.so`) +| Endpoint | Auth | Use it for | +| --------------------------------------- | --------------------- | ---------------------------------------------------- | +| `https://mcp.plane.so/http/mcp` | OAuth | Streamable HTTP; recommended for interactive use | +| `https://mcp.plane.so/http/api-key/mcp` | PAT headers | Automations, CI, headless agents, shared team setups | +| `uvx plane-mcp-server stdio` | Environment variables | Self-hosted Plane and local or offline development | +| `https://mcp.plane.so/sse` | OAuth | Deprecated clients that still require HTTP+SSE | -## Identifier system +### OAuth -Plane uses two kinds of identifiers for work items. +Your client redirects you to Plane, where you sign in and choose a workspace. The server validates the resulting +token with `/api/v1/users/me/`, and the connection stays bound to that workspace. -- **Readable identifier** - human-friendly, e.g., `ENG-42` - - Composed of the project identifier (`ENG`) and a sequence number (`42`) - - Used in URLs, UI, and team communication +The default redirect allowlist covers Cursor, VS Code, Antigravity, Claude.ai, ChatGPT, and localhost callbacks. A +self-hosted server can add other clients through `PLANE_OAUTH_ALLOWED_REDIRECT_URIS`. -- **UUID** - machine-friendly, e.g., `3fa85f64-5717-4562-b3fc-2c963f66afa6` - - Used by all other tools for `project_id`, `work_item_id`, `cycle_id`, etc. - - Returned by every API response +Re-authenticate from your client's connector controls. In Claude Code, run `/mcp`; with `mcp-remote`, clear its cache: ---- +```bash +rm -rf ~/.mcp-auth +``` -## How-to guides +### Personal access token -Plane hosts the MCP server for you at **`https://mcp.plane.so`**. If you run your own instance of the MCP server, replace `https://mcp.plane.so` with your own server's public URL (e.g., `https://mcp.yourcompany.com`) in all client config examples below. +Send both headers on every request to the PAT endpoint: -### Prerequisites +| Header | Value | +| ------------------ | ------------------ | +| `Authorization` | `Bearer ` | +| `x-workspace-slug` | `` | -**For all modes:** +::: warning Changed +Earlier versions of this page showed an `x-api-key` header. The server reads the standard `Authorization: Bearer` +header; update existing configs. +::: -- A Plane account with access to at least one workspace +#### Get a token -**For stdio mode (local and self-hosted deployments)** +Create a personal access token under **Profile settings → Personal access tokens** and copy it when shown. For +automations, you can instead create a workspace access token under **Workspace settings → Access tokens**. -- Python 3.10+ installed (`python --version`) -- `uv` package manager (recommended). See [Installing uv](https://docs.astral.sh/uv/getting-started/installation/) +#### Find your workspace slug -| Variable | Required | Description | -| ---------------------- | -------- | --------------------------------------------------------------------- | -| `PLANE_API_KEY` | Yes | API key from your workspace settings | -| `PLANE_WORKSPACE_SLUG` | Yes | Your workspace slug | -| `PLANE_BASE_URL` | No | API URL for self-hosted instances. Defaults to `https://api.plane.so` | +The slug is the segment after `app.plane.so/` in your Plane URL. In `https://app.plane.so/acme-corp/`, it is +`acme-corp`. -#### Get your API key (required for stdio and PAT token modes) +### Local (stdio) -1. Open Plane and go to your workspace. -2. Generate a token. You can use either: - - **Personal Access Token** - go to **Profile Settings → API Tokens**. - - **Workspace Access Token** - go to **Workspace Settings → Access Tokens**. -3. Click **Add access token**, name it (e.g., "MCP Server"), click **Generate token**. -4. Copy the token as it will not be shown again. +Local mode requires Python 3.10+ and [`uv`](https://docs.astral.sh/uv/). On macOS or Linux: -#### Get your workspace slug +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` -The slug is the short identifier in your Plane URL. For: +On Windows: +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -https://app.plane.so/acme-corp/ -``` - -the slug is `acme-corp`. -::: info Clients that don't support custom headers -**Claude Desktop** — use Stdio instead.
-**Claude.ai** — use OAuth instead (the UI doesn't expose arbitrary headers). -::: +| Variable | Required | Description | +| ---------------------- | -------- | --------------------------------------------------------------------------------- | +| `PLANE_API_KEY` | Yes | Your Plane personal or workspace access token | +| `PLANE_WORKSPACE_SLUG` | Yes | The workspace slug | +| `PLANE_BASE_URL` | No | Defaults to `https://api.plane.so`; set it to your self-hosted Plane instance URL | -### Claude Desktop +Prefer stdio when the client runs on the same machine, you need a self-hosted or private Plane instance, or you do +not want to expose an MCP HTTP service. -Config file: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). +### SSE (deprecated) -Quit Claude Desktop before editing, then relaunch and click the hammer icon (🔨) to confirm Plane tools are listed. +The MCP specification deprecated the older HTTP+SSE transport. Keep `https://mcp.plane.so/sse` only for an existing +client that cannot use Streamable HTTP, and migrate when that client supports it. -#### Stdio +## Connect a client -Spawns the server as a local subprocess. Credentials come from environment variables. +Replace `mcp.plane.so` with your own host if you self-host the server. Tabs stay in sync across this page. -```json -{ - "mcpServers": { - "plane": { - "command": "uvx", - "args": ["plane-mcp-server", "stdio"], - "env": { - "PLANE_API_KEY": "your_api_key_here", - "PLANE_WORKSPACE_SLUG": "your-workspace-slug", - "PLANE_BASE_URL": "https://plane.yourcompany.com" - } - } - } -} -``` +### General -#### HTTP with OAuth (via mcp-remote) +These are the common shapes. Some clients use `serverUrl`, `servers`, or `context_servers`; use the client-specific +schema below. -Claude Desktop doesn't support remote HTTP natively. Use `mcp-remote` — a local proxy that bridges Claude Desktop to Plane's cloud server. **Requires Node.js 18+.** +:::tabs key:mcp-auth +== OAuth {#general-oauth} ```json { "mcpServers": { "plane": { - "command": "npx", - "args": ["mcp-remote@latest", "https://mcp.plane.so/http/mcp"] + "url": "https://mcp.plane.so/http/mcp" } } } ``` -On first launch, `mcp-remote` opens a browser for the Plane OAuth flow. - -::: tip No Node.js? -Use the SSE fallback instead: `"url": "https://mcp.plane.so/sse", "type": "sse"`. -::: - -#### HTTP with PAT Token - -Connects to the PAT endpoint using API key headers. No browser interaction required - suitable for shared team setups where users authenticate via their own API key. +== Access token {#general-token} ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/api-key/mcp", - "type": "http", "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" + "Authorization": "Bearer ", + "x-workspace-slug": "" } } } } ``` -#### SSE (Legacy) - -For existing integrations already using the SSE transport. +== Local (stdio) {#general-stdio} ```json { "mcpServers": { "plane": { - "url": "https://mcp.plane.so/sse", - "type": "sse" + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` ---- - -### Claude Code (CLI) - -Claude Code manages MCP servers via `claude mcp add` or `claude mcp add-json`. MCP configs are stored in `~/.claude.json` (user scope) or `.mcp.json` in your repo root (project scope) — **not** `.claude/settings.json`. - -#### Stdio - -```bash -claude mcp add plane \ - -e PLANE_API_KEY=your_api_key_here \ - -e PLANE_WORKSPACE_SLUG=your-workspace-slug \ - -e PLANE_BASE_URL=https://plane.yourcompany.com \ - -- uvx plane-mcp-server stdio -``` +::: -Add `--scope project` to write to `.mcp.json` (shared via git with your team) instead of `~/.claude.json` (your local copy). +### Claude -#### HTTP with OAuth +:::tabs key:mcp-auth +== OAuth {#claude-oauth} +On Claude Desktop or claude.ai: -```bash -claude mcp add --transport http plane https://mcp.plane.so/http/mcp -``` +1. Open **Settings → Connectors → Add custom connector**. +2. Paste `https://mcp.plane.so/http/mcp`, select **Add**, then **Connect**. +3. Sign in to Plane. In a chat, choose **+ → Connectors** to enable Plane. -Claude Code will open a browser for the Plane OAuth flow. Run `/mcp` inside a session to re-authenticate if needed. +Free plans allow one custom connector. On Team or Enterprise, an Owner adds it under +**Organization settings → Connectors**, then members select **Connect**. -#### HTTP with PAT Token +== Access token {#claude-token} +Desktop users who need a token instead of OAuth can bridge with `mcp-remote` (Node.js 22+ recommended): -```bash -claude mcp add-json plane '{ - "type": "http", - "url": "https://mcp.plane.so/http/api-key/mcp", - "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" +```json +{ + "mcpServers": { + "plane": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https://mcp.plane.so/http/api-key/mcp", + "--header", + "Authorization: Bearer ${PLANE_PAT}", + "--header", + "x-workspace-slug: ${PLANE_WORKSPACE_SLUG}" + ], + "env": { + "PLANE_PAT": "", + "PLANE_WORKSPACE_SLUG": "" + } + } } -}' -``` - -#### SSE (Legacy) - -```bash -claude mcp add plane \ - --transport sse \ - --url https://mcp.plane.so/sse +} ``` -Settings file equivalent: +== Local (stdio) {#claude-stdio} +Use **Settings → Developer → Edit Config**, or edit `~/Library/Application Support/Claude/claude_desktop_config.json` +on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows: ```json { "mcpServers": { "plane": { - "url": "https://mcp.plane.so/sse", - "type": "sse" + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` -Verify any configuration with: +Quit and relaunch Claude Desktop. This file supports stdio only: never put `url` or `type: http` in it. + +::: + +### Claude Code + +:::tabs key:mcp-auth +== OAuth {#claude-code-oauth} ```bash +claude mcp add --transport http plane https://mcp.plane.so/http/mcp +# In a session, run /mcp and authenticate (or run: claude mcp login plane). claude mcp list ``` -#### Using Plane in Claude Code sessions +== Access token {#claude-code-token} +Put `--header` after the URL: ```bash -claude - -> Look up work item ENG-42 and implement what it describes. +claude mcp add --transport http plane https://mcp.plane.so/http/api-key/mcp \ + --header "Authorization: Bearer " \ + --header "x-workspace-slug: " +``` -> After fixing the bug, mark ENG-42 as done and log 90 minutes of work. +== Local (stdio) {#claude-code-stdio} -> Create work items for each TODO in src/auth.ts and add them to the current sprint. +```bash +claude mcp add --transport stdio plane \ + --env PLANE_API_KEY= \ + --env PLANE_WORKSPACE_SLUG= \ + -- uvx plane-mcp-server stdio ``` ---- - -### Claude.ai / Claude Chat (Web) +::: -Claude.ai supports remote MCP servers for eligible plans. Because it runs in a browser it cannot spawn local processes, stdio is not available here. +Use `--scope local|project|user`; project scope writes a shareable `.mcp.json` with `mcpServers`, `type: "http"`, and +`url`. PAT entries add `headers`, and `${PLANE_PAT}` expands from the environment. Claude Code's SSE transport is deprecated. -#### HTTP with OAuth +### ChatGPT -**Pro / Max:** +ChatGPT supports OAuth on Plus, Pro, Business, Enterprise, and Edu plans: -1. Go to **Customize → Connectors** in Claude.ai. -2. Click **Add custom connector**. -3. Enter the server URL: `https://mcp.plane.so/http/mcp` -4. Claude.ai redirects you through the Plane OAuth flow. +1. Open **Settings → Security and login** and turn on **Developer mode**. Business, Enterprise, and Edu workspaces + require an admin to allow it. +2. Open **chatgpt.com/plugins**, select **+**, name the connection "Plane", enter + `https://mcp.plane.so/http/mcp` under **Connection**, select **Create**, then sign in to Plane. +3. In a chat, open **+ → Developer mode** and enable Plane. -**Team / Enterprise** (admins only): Go to **Organization settings → Connectors → Add custom connector** and use the same URL. +The exact menu names may differ by workspace. ChatGPT does not accept custom headers, so use OAuth. -#### HTTP with PAT Token +### Codex -If your Claude.ai plan supports custom headers in integrations: +The CLI, IDE extension, and ChatGPT desktop app share `~/.codex/config.toml`. -- URL: `https://mcp.plane.so/http/api-key/mcp` -- Headers: `x-api-key: your_api_key_here`, `x-workspace-slug: your-workspace-slug` +:::tabs key:mcp-auth +== OAuth {#codex-oauth} -#### SSE (Legacy) +```bash +codex mcp add plane --url https://mcp.plane.so/http/mcp +codex mcp login plane +codex mcp list +``` -- URL: `https://mcp.plane.so/sse` +You can also run `/mcp` inside Codex. No experimental flag is required. ---- +== Access token {#codex-token} -### Cursor +```toml +[mcp_servers.plane] +url = "https://mcp.plane.so/http/api-key/mcp" +bearer_token_env_var = "PLANE_PAT" +http_headers = { "x-workspace-slug" = "" } +``` -Config file: `~/.cursor/mcp.json` +`bearer_token_env_var` sends `Authorization: Bearer $PLANE_PAT`. The CLI supports +`codex mcp add … --bearer-token-env-var PLANE_PAT`; arbitrary headers are config-file only. -Open Cursor → **Settings** → search **MCP** → open the config file. Restart Cursor (`Cmd/Ctrl + Shift + P → Reload Window`) after saving. +== Local (stdio) {#codex-stdio} -#### Stdio +```toml +[mcp_servers.plane] +command = "uvx" +args = ["plane-mcp-server", "stdio"] -```json -{ - "mcpServers": { - "plane": { - "command": "uvx", - "args": ["plane-mcp-server", "stdio"], - "env": { - "PLANE_API_KEY": "your_api_key_here", - "PLANE_WORKSPACE_SLUG": "your-workspace-slug", - "PLANE_BASE_URL": "https://plane.yourcompany.com" - } - } - } -} +[mcp_servers.plane.env] +PLANE_API_KEY = "" +PLANE_WORKSPACE_SLUG = "" ``` -#### HTTP with OAuth +::: + +### Cursor + +Use `~/.cursor/mcp.json` globally or `.cursor/mcp.json` in a project. + +:::tabs key:mcp-auth +== OAuth {#cursor-oauth} +[![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=plane&config=eyJ1cmwiOiJodHRwczovL21jcC5wbGFuZS5zby9odHRwL21jcCJ9) -The `cursor://` redirect URI is registered natively in the OAuth provider. +Or add the server manually: ```json { "mcpServers": { "plane": { - "url": "https://mcp.plane.so/http/mcp", - "type": "http" + "url": "https://mcp.plane.so/http/mcp" } } } ``` -#### HTTP with PAT Token +Cursor shows **Login** or **Needs authentication** and completes OAuth. Manage servers from **Customize**. + +== Access token {#cursor-token} ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/api-key/mcp", - "type": "http", "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" + "Authorization": "Bearer ${env:PLANE_PAT}", + "x-workspace-slug": "" } } } } ``` -#### SSE (Legacy) +== Local (stdio) {#cursor-stdio} ```json { "mcpServers": { "plane": { - "url": "https://mcp.plane.so/sse", - "type": "sse" + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "${env:PLANE_API_KEY}", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` ---- +::: + +Remote entries use `url` and must not include a `type` key. ### VS Code -VS Code supports MCP through GitHub Copilot (requires a Copilot subscription). Open the Copilot chat panel (`Ctrl+Alt+I`), switch to **Agent** mode. The `vscode://` and `vscode-insiders://` redirect URIs are registered in the OAuth provider. +Use `.vscode/mcp.json` for a workspace, or run **MCP: Open User Configuration** for the user file. -Config can be set at workspace level (`.vscode/mcp.json`) or user level (VS Code `settings.json` under the `"mcp"` key). Examples below use `.vscode/mcp.json`. +:::tabs key:mcp-auth +== OAuth {#vs-code-oauth} +[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=plane&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D) -#### Stdio +[Install in VS Code Insiders](https://insiders.vscode.dev/redirect/mcp/install?name=plane&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D&quality=insiders), or add it from the CLI: + +```bash +code --add-mcp '{"name":"plane","type":"http","url":"https://mcp.plane.so/http/mcp"}' +``` + +Trust the server on first start, verify it with **MCP: List Servers**, and use Copilot Chat in **Agent** mode. +Copilot Business and Enterprise organizations must enable the "MCP servers in Copilot" policy. + +== Access token {#vs-code-token} ```json { + "inputs": [ + { + "type": "promptString", + "id": "plane-pat", + "description": "Plane personal access token", + "password": true + }, + { + "type": "promptString", + "id": "plane-slug", + "description": "Plane workspace slug" + } + ], "servers": { "plane": { - "command": "uvx", - "args": ["plane-mcp-server", "stdio"], - "env": { - "PLANE_API_KEY": "your_api_key_here", - "PLANE_WORKSPACE_SLUG": "your-workspace-slug", - "PLANE_BASE_URL": "https://plane.yourcompany.com" + "type": "http", + "url": "https://mcp.plane.so/http/api-key/mcp", + "headers": { + "Authorization": "Bearer ${input:plane-pat}", + "x-workspace-slug": "${input:plane-slug}" } } } } ``` -#### HTTP with OAuth +== Local (stdio) {#vs-code-stdio} ```json { "servers": { "plane": { - "url": "https://mcp.plane.so/http/mcp", - "type": "http" + "type": "stdio", + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` -#### HTTP with PAT Token +::: + +### Windsurf + +Current vendor docs call Windsurf **Devin Desktop**. Its configuration remains at +`~/.codeium/windsurf/mcp_config.json`; open Cascade's **MCPs → Manage MCPs** or +**Settings → Cascade → MCP Servers**. + +:::tabs key:mcp-auth +== OAuth {#windsurf-oauth} ```json { - "servers": { + "mcpServers": { "plane": { - "url": "https://mcp.plane.so/http/api-key/mcp", - "type": "http", - "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" - } + "serverUrl": "https://mcp.plane.so/http/mcp" } } } ``` -#### SSE (Legacy) +If the OAuth sign-in does not complete, use the access-token configuration instead. + +== Access token {#windsurf-token} ```json { - "servers": { + "mcpServers": { "plane": { - "url": "https://mcp.plane.so/sse", - "type": "sse" + "serverUrl": "https://mcp.plane.so/http/api-key/mcp", + "headers": { + "Authorization": "Bearer ${env:PLANE_PAT}", + "x-workspace-slug": "" + } } } } ``` ---- - -### Windsurf - -Config file: `~/.codeium/windsurf/mcp_config.json` - -Restart Windsurf after saving, then open the Cascade panel. The `windsurf://` redirect URI is registered in the OAuth provider. - -::: warning -Windsurf uses `"serverUrl"` (not `"url"`) for remote HTTP servers. -::: - -#### Stdio +== Local (stdio) {#windsurf-stdio} ```json { @@ -461,282 +512,355 @@ Windsurf uses `"serverUrl"` (not `"url"`) for remote HTTP servers. "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { - "PLANE_API_KEY": "your_api_key_here", - "PLANE_WORKSPACE_SLUG": "your-workspace-slug", - "PLANE_BASE_URL": "https://plane.yourcompany.com" + "PLANE_API_KEY": "${env:PLANE_API_KEY}", + "PLANE_WORKSPACE_SLUG": "" } } } } ``` -#### HTTP with OAuth +::: + +Remote entries use `serverUrl`. Refresh the server list after saving. + +### Zed + +Use **Settings → AI → MCP Servers → Add Server**, or edit `~/.config/zed/settings.json`. + +:::tabs key:mcp-auth +== OAuth {#zed-oauth} ```json { - "mcpServers": { + "context_servers": { "plane": { - "serverUrl": "https://mcp.plane.so/http/mcp" + "url": "https://mcp.plane.so/http/mcp" } } } ``` -#### HTTP with PAT Token +Zed prompts for OAuth through an allowlisted loopback callback. + +== Access token {#zed-token} ```json { - "mcpServers": { + "context_servers": { "plane": { - "serverUrl": "https://mcp.plane.so/http/api-key/mcp", + "url": "https://mcp.plane.so/http/api-key/mcp", "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" + "Authorization": "Bearer ", + "x-workspace-slug": "" } } } } ``` -#### SSE (Legacy) +== Local (stdio) {#zed-stdio} ```json { - "mcpServers": { + "context_servers": { "plane": { - "url": "https://mcp.plane.so/sse", - "type": "sse" + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` ---- +::: -### Zed +Zed uses this flat schema; the old nested `command.path` and `source: custom` shape is outdated. + +### Antigravity -Config file: `~/.config/zed/settings.json` under `"context_servers"`. Zed uses a different schema from other clients - the stdio command goes inside a `"command"` object with `"path"` instead of `"command"`. +The IDE and CLI share `~/.gemini/config/mcp_config.json` globally or `.agents/mcp_config.json` in a workspace. -#### Stdio +:::tabs key:mcp-auth +== OAuth {#antigravity-oauth} ```json { - "context_servers": { - "plane-mcp-server": { - "command": { - "path": "uvx", - "args": ["plane-mcp-server", "stdio"], - "env": { - "PLANE_API_KEY": "your_api_key_here", - "PLANE_WORKSPACE_SLUG": "your-workspace-slug", - "PLANE_BASE_URL": "https://plane.yourcompany.com" - } - }, - "settings": {} + "mcpServers": { + "plane": { + "serverUrl": "https://mcp.plane.so/http/mcp" } } } ``` -#### HTTP with OAuth +OAuth is automatic. In the IDE, open **… → MCP Servers → Manage MCP Servers**. In Antigravity 2.0, use +**Settings → Customizations → Installed MCP Servers → Add MCP**; in the CLI, run `/mcp`. + +== Access token {#antigravity-token} ```json { - "context_servers": { - "plane-mcp-server": { - "url": "https://mcp.plane.so/http/mcp", - "settings": {} + "mcpServers": { + "plane": { + "serverUrl": "https://mcp.plane.so/http/api-key/mcp", + "headers": { + "Authorization": "Bearer ${env:PLANE_PAT}", + "x-workspace-slug": "" + } } } } ``` -#### HTTP with PAT Token +== Local (stdio) {#antigravity-stdio} ```json { - "context_servers": { - "plane-mcp-server": { - "url": "https://mcp.plane.so/http/api-key/mcp", - "headers": { - "x-api-key": "your_api_key_here", - "x-workspace-slug": "your-workspace-slug" - }, - "settings": {} + "mcpServers": { + "plane": { + "command": "uvx", + "args": ["plane-mcp-server", "stdio"], + "env": { + "PLANE_API_KEY": "${env:PLANE_API_KEY}", + "PLANE_WORKSPACE_SLUG": "" + } } } } ``` -#### SSE (Legacy) +::: + +Remote entries require `serverUrl`; `url` and `httpUrl` are unsupported. + +### Other clients + +For a stdio-only client, use `mcp-remote` with Node.js 22+ recommended. A client with native remote-MCP support only +needs the OAuth URL. + +:::tabs key:mcp-auth +== OAuth {#other-clients-oauth} ```json { - "context_servers": { - "plane-mcp-server": { - "url": "https://mcp.plane.so/sse", - "settings": {} + "mcpServers": { + "plane": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://mcp.plane.so/http/mcp"] } } } ``` -Open the AI panel (`Cmd + Shift + A`) to use Plane tools in conversation. - ---- - -### Other clients (mcp-remote bridge) - -Any MCP client that supports stdio but not remote HTTP can use `mcp-remote` as a proxy. It runs locally as a subprocess and forwards requests to `https://mcp.plane.so/http/mcp`, handling the OAuth flow on first run. **Requires Node.js 18+.** +== Access token {#other-clients-token} ```json { - "command": "npx", - "args": ["mcp-remote@latest", "https://mcp.plane.so/http/mcp"] + "mcpServers": { + "plane": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https://mcp.plane.so/http/api-key/mcp", + "--header", + "Authorization: Bearer ${PLANE_PAT}", + "--header", + "x-workspace-slug: ${PLANE_WORKSPACE_SLUG}" + ], + "env": { + "PLANE_PAT": "", + "PLANE_WORKSPACE_SLUG": "" + } + } + } } ``` -## Self-hosted Plane deployments - -Set `PLANE_BASE_URL` to the public URL of your Plane instance (e.g., https://plane.yourcompany.com). This is used for user-facing OAuth redirects and API calls in stdio mode. +::: -In HTTP/SSE mode, the server also makes internal server-to-server calls to Plane for token validation. If your infrastructure routes internal traffic differently from public traffic (e.g., via a private network, service mesh, or internal load balancer), set `PLANE_INTERNAL_BASE_URL` to the internal address. When set, all server-to-server calls use this URL and only OAuth redirects use `PLANE_BASE_URL`. +`mcp-remote` reads headers from its `--header` arguments; a `headers` key on this stdio entry is ignored. To reset +cached OAuth state, remove `~/.mcp-auth`. -If `PLANE_INTERNAL_BASE_URL` is not set, it falls back to PLANE_BASE_URL for all calls. +## Common workflows -Before connecting a client, verify your credentials reach the instance: +**What's on my plate** -```bash -curl -H "x-api-key: YOUR_API_KEY" \ - "https://plane.yourcompany.com/api/v1/users/me/" +```text +List work items assigned to me that are in progress or overdue, grouped by project. ``` -A `200` response confirms the API key and URL are correct. +_Trace: `member me` → `workitem list` without `project_id`, using +`pql='assignee = currentUser() AND (stateGroup = "started" OR isOverdue())'`._ -::: tip Running your own MCP server? -You can skip `mcp.plane.so` entirely and deploy `plane-mcp-server` yourself — Docker Compose, Helm, OAuth app setup: [Self-host MCP Server](/dev-tools/mcp-server-self-host). -::: +**File a bug** ---- +```text +Create a high-priority bug in ENG called "Login times out on Safari 17". Description: the OAuth callback lands on a blank page. Assign it to me and add the "auth" label. +``` -## Common workflows +_Trace: `project list` → `member me` → `label list` → `workitem create`._ -### Look up a work item by ID +**Roll over a sprint** -``` -What is work item ENG-42 about? +```text +Create Sprint 15 in ENG from June 2 to June 15, move everything unfinished from Sprint 14 into it, and give me a count by priority. ``` -Model calls `retrieve_work_item_by_identifier` with `project_identifier="ENG"` and `work_item_identifier="42"`. +_Trace: `cycle create` with `owned_by` → `cycle list` to find Sprint 14 → `cycle transfer_workitems` → +`workitem count` with `pql` and `group_by="priority"`._ -### Create a work item +**Close the loop** -``` -Create a high-priority bug in the ENG project called "Login times out on Safari". -Description: The OAuth callback redirects to a blank page on Safari 17+. -Assign it to me. +```text +Log 90 minutes on ENG-42 with the note "Implemented retry logic", mark it Done, and comment "Fixed in abc1234, needs QA". ``` -Model calls `list_projects` → `retrieve_work_item_by_identifier` (or `get_me` to resolve "me") → `create_work_item`. +_Trace: `workitem retrieve_by_identifier` → `work_log create` → `state list` → `workitem update` → +`workitem_comment create`._ -### Update work item state +## Permissions and sessions -``` -Mark ENG-88 as done and add a comment: "Fixed in commit abc1234, needs QA." -``` +- The server acts as the authenticated user. Plane enforces workspace and project roles, so a Guest cannot do more + through MCP. +- OAuth requests `read` and `write` scopes. The workspace chosen at consent binds that connection; reconnect to + switch workspaces. +- A PAT connection is scoped by `x-workspace-slug`. +- Hosted OAuth tokens are stored server-side in Redis or Valkey. A self-hosted server without Redis falls back to + in-memory storage. +- Revoke access by disconnecting the connector in your client, deleting a PAT in Plane, or clearing the + `mcp-remote` cache. -Model resolves the UUID, calls `list_states` to find the Done state UUID, calls `update_work_item` and `create_work_item_comment`. +## Security best practices -### Sprint planning +- Use only `https://mcp.plane.so` or your own trusted host, and check the URL on Plane's consent screen. +- Treat work item titles, descriptions, comments, and attachments as untrusted model input. Prefer clients that + confirm writes; destructive actions are flagged with `destructiveHint`. +- Keep PATs out of shared or committed configs. Use environment variables or `${input:...}`, and never commit a + token in a project-scoped `.mcp.json`. +- Use a workspace access token with the minimum role needed for automations. +- Revoke tokens in Plane settings and audit API token events in the workspace audit log. +- Server logs are structured JSON with tool name, duration, status, opaque user ID, and workspace slug. Display + names are logged only when `LOG_USER_INFO=true`, because they are PII. -``` -Create a cycle called "Sprint 15" in ENG starting 2025-06-02, ending 2025-06-15. -Then move all incomplete issues from Sprint 14 into it. -``` +## Self-hosted Plane -Model calls `create_cycle` then `list_cycles` to find Sprint 14's UUID, then `transfer_cycle_work_items`. +The hosted `mcp.plane.so` service cannot reach private Plane instances. In stdio mode, set `PLANE_BASE_URL` to your +instance URL, then test the token against Plane's REST API: -### Log time - -``` -Log 90 minutes on ENG-42: "Implemented retry logic for the upload endpoint." +```bash +curl -H "x-api-key: YOUR_API_KEY" \ + "https://plane.yourcompany.com/api/v1/users/me/" ``` -### Search across the workspace +A `200` response confirms the key and URL. That header is the Plane REST API header, not the MCP PAT header. -``` -Show me all high-priority bugs assigned to me that are still in progress. -``` +::: tip Running your own MCP server? +Follow the [self-hosting guide](/dev-tools/mcp-server-self-host) for Docker, Helm, OAuth, storage, and operations. +::: -Model calls `list_work_items` with filters `priorities=["high"]`, `state_groups=["started"]`, and the current user's UUID as `assignee_ids`. +## Upgrading -### Manage a module +### From per-operation tools (0.2.x → 0.3.0) -``` -Add ENG-55, ENG-56, and ENG-57 to the "Checkout Redesign" module. -``` +The 177 per-operation tools became 28 resource tools. Of the retired names, 169 still resolve as hidden aliases and +keep their original parameter names, so saved prompts and scripts continue to work. Seven names cannot be mapped and +return a message naming their replacement; see [retired tool names](/dev-tools/mcp-server-tools#retired-tool-names). -Model calls `list_modules` to find the UUID, then `add_work_items_to_module` with the resolved work item UUIDs. +`project list` is now paginated by default. Follow `next_cursor` or pass `per_page`. Archive actions now return an +explicit status object. ---- +### From the Node.js server -## Troubleshooting +The `@makeplane/plane-mcp-server` npm package is deprecated. Update environment variables, then use the stdio +configuration shown above: -The server propagates errors from the Plane SDK as MCP tool errors. +| Node.js server | Python server | +| ---------------------- | ---------------------- | +| `PLANE_API_KEY` | `PLANE_API_KEY` | +| `PLANE_API_HOST_URL` | `PLANE_BASE_URL` | +| `PLANE_WORKSPACE_SLUG` | `PLANE_WORKSPACE_SLUG` | -| Scenario | HTTP Status | Cause | Resolution | -| ----------------------------------- | -------------------- | ----------------------------------------------- | --------------------------------------------------- | -| Invalid API key | 401 | `PLANE_API_KEY` is wrong or revoked | Regenerate the token in Plane settings | -| Invalid OAuth token | 401 | Token expired or revoked | Re-authorise through OAuth flow | -| Missing `x-workspace-slug` header | - | Header auth missing workspace | Include `x-workspace-slug` header | -| Wrong workspace slug | 404 | Slug doesn't exist | Check the exact slug in your Plane URL | -| Insufficient permissions | 403 | User role too low | Check your role in the workspace/project | -| Resource not found | 404 | UUID or identifier doesn't exist | Verify the ID; check if resource was deleted | -| Validation error | 400 | Required field missing or invalid value | Check required fields and value constraints | -| Redis unavailable | - | Token storage down | Set `REDIS_HOST`/`REDIS_PORT` or omit for in-memory | -| Network error | - | Cannot reach Plane API | Verify `PLANE_BASE_URL` and connectivity | -| Server not listed in Claude Desktop | Wrong transport type | Claude Desktop doesn't support `"type": "http"` | Use `npx mcp-remote@latest` or SSE transport | -| Server config skipped | JSON syntax error | Config file ignored silently | Validate JSON — check for trailing commas | +Replace the old Node.js `command` and `args` with `uvx plane-mcp-server stdio`. + +## Troubleshooting -**Verify connectivity (stdio/PAT):** +| Symptom | Cause | Fix | +| ----------------------------------- | ------------------------------------------- | -------------------------------------------------------------------- | +| 401 with PAT | Token is wrong, revoked, or uses old header | Use `Authorization: Bearer ` instead of `x-api-key` | +| 401 with OAuth | Token expired | Re-authenticate from the client | +| "workspace slug missing" | PAT config omits the workspace header | Add `x-workspace-slug` | +| 404 | Workspace slug or resource ID is wrong | Check the slug or ID | +| 403 | Your Plane role is too low | Ask for the required workspace or project role | +| 400 | An argument is missing or invalid | Read the error; permitted enum values are in the tool description | +| "not available on your plan" or 402 | The Plane plan does not include the feature | Enable the feature or use an available action | +| `mcp-remote` fails to start | Node.js is too old | Use Node.js 22+ and run `npx -y mcp-remote@latest` | +| Server is not listed | JSON or client schema is invalid | Remove trailing commas; apply the client-specific schema notes above | +| Only the first page of projects | `project list` is paginated | Follow `next_cursor` or pass `per_page` | +| Tools look stale or out of order | Pinned tool order or client cache is stale | Restart the client after upgrades | + +For a server that is not listed, remember that Claude Desktop's JSON file cannot contain `url`, +Windsurf and Antigravity require `serverUrl`, and a Cursor remote entry must not contain `type`. + +Debug with: ```bash -curl -H "x-api-key: YOUR_KEY" \ - "https://api.plane.so/api/v1/users/me/" -``` +claude --debug +claude mcp list -**Run stdio mode manually to debug startup:** +PLANE_API_KEY= PLANE_WORKSPACE_SLUG= uvx plane-mcp-server stdio -```bash -PLANE_API_KEY=your_key PLANE_WORKSPACE_SLUG=your-slug plane-mcp-server stdio +curl -X POST http://localhost:8211/http/mcp + +rm -rf ~/.mcp-auth ``` -**Test the HTTP server is running:** +The local HTTP request should return either `401` or an MCP response. -```bash -curl http://localhost:8211/http/mcp -# Should return MCP protocol response or 401 -``` +## FAQ -**Claude Code: enable debug logging:** +::: details Which Plane plans work? +The server follows your Plane plan and role. A plan-gated action returns a message naming the unavailable feature. +::: -```bash -claude --mcp-debug -``` +::: details Is the server free? +The MIT-licensed server is free to use. The Plane features it can access follow your Plane plan. +::: -**Claude Code: re-authenticate OAuth:** +::: details Does it work with self-hosted Plane? +Yes. Use stdio with `PLANE_BASE_URL`, or [deploy your own MCP server](/dev-tools/mcp-server-self-host). +::: -```bash -rm -rf ~/.mcp-auth -``` +::: details Is there a read-only mode? +There is no separate read-only endpoint. Use your client's tool allow-list; read-only tools are annotated with +`readOnlyHint`. +::: -Restart Claude Code and run `/mcp` to authenticate again. +::: details Can I limit which tools are available? +Yes. Use the client's tool allow-list or deny-list. +::: -## See also +::: details How do epics work? +An epic is a work item whose type is "Epic". Follow the [epics recipe](/dev-tools/mcp-server-tools#epics). +::: + +::: details Does it use Plane AI credits? +No. The MCP server calls Plane's API directly; the AI model belongs to your MCP client. +::: -- [Self-host MCP Server](/dev-tools/mcp-server-self-host) -- [Tool Reference](/dev-tools/mcp-server-tools) +::: details Where does my data go? +The hosted server proxies requests to `api.plane.so`. Self-host the MCP server if you need full infrastructure +control. +::: ---- +## See also -_Plane MCP Server is open source and licensed under MIT. Source at [github.com/makeplane/plane-mcp-server](https://github.com/makeplane/plane-mcp-server)._ +- [Tool reference](/dev-tools/mcp-server-tools) +- [Self-host the MCP server](/dev-tools/mcp-server-self-host) +- [Short setup guide](https://docs.plane.so/ai/mcp-server) +- [Plane MCP server on GitHub](https://github.com/makeplane/plane-mcp-server) +- [Plane Query Language](https://docs.plane.so/core-concepts/issues/plane-query-language) diff --git a/docs/index.md b/docs/index.md index c10dbb1a..2e82c85f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,7 +51,7 @@ Build custom integrations using OAuth 2.0. Complete guides for app registration, -Integrate Plane with AI agents using Model Context Protocol for intelligent project management automation. +Connect Claude, ChatGPT, Cursor, VS Code, and other AI tools to Plane. 28 tools covering work items, cycles, releases, customers, and more. diff --git a/vercel.json b/vercel.json index f1bdea82..1f764538 100644 --- a/vercel.json +++ b/vercel.json @@ -118,7 +118,7 @@ }, { "source": "/dev-tools/mcp-server-claude-code", - "destination": "/dev-tools/mcp-server#claude-code-cli" + "destination": "/dev-tools/mcp-server#claude-code" } ], "rewrites": [ From 2b90461c07e7d29d25212589c128c6c72f4c94e6 Mon Sep 17 00:00:00 2001 From: vihar Date: Sat, 15 Aug 2026 19:54:46 +0530 Subject: [PATCH 2/2] docs: address review feedback on MCP server pages - Note that OAuth app registration is Cloud/Commercial Edition only and point Community Edition users to stdio mode - Use the Windows-safe mcp-remote header format (no spaces around the colon, header value in env) and mention MCP_REMOTE_CONFIG_DIR before clearing ~/.mcp-auth - Reconcile the retired-name counts (169 aliases + get_pql_reference + 7 unmappable = 177) - Keep the API key out of the curl command line - Add privacy guidance for the opaque user ID and workspace slug in logs - Serve the Cursor and VS Code install badges from /images/mcp/ --- docs/dev-tools/mcp-server-self-host.md | 9 ++++- docs/dev-tools/mcp-server-tools.md | 2 +- docs/dev-tools/mcp-server.md | 40 ++++++++++++-------- docs/public/images/mcp/install-in-cursor.svg | 16 ++++++++ docs/public/images/mcp/install-in-vscode.svg | 1 + 5 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 docs/public/images/mcp/install-in-cursor.svg create mode 100644 docs/public/images/mcp/install-in-vscode.svg diff --git a/docs/dev-tools/mcp-server-self-host.md b/docs/dev-tools/mcp-server-self-host.md index 34e5b43d..bc60f14c 100644 --- a/docs/dev-tools/mcp-server-self-host.md +++ b/docs/dev-tools/mcp-server-self-host.md @@ -15,7 +15,10 @@ If you're a Plane Cloud user connecting to `mcp.plane.so`, you don't need this. ## Prerequisites -- A running **Plane instance** (self-hosted or Cloud) with workspace admin access +- 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](/dev-tools/mcp-server#local-stdio) 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 @@ -284,6 +287,10 @@ The server emits structured JSON logs with the tool name, duration, status, opaq `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: diff --git a/docs/dev-tools/mcp-server-tools.md b/docs/dev-tools/mcp-server-tools.md index 1b575f52..46e3c9ed 100644 --- a/docs/dev-tools/mcp-server-tools.md +++ b/docs/dev-tools/mcp-server-tools.md @@ -524,7 +524,7 @@ Takes `detail`: `full` (default) or `brief`. This tool has no `action` parameter ## Retired tool names -Plane MCP server 0.3.0 consolidated 177 per-operation tools into 28 resource tools. The 169 aliases below still resolve, stay hidden from tool listings, and accept their original parameter names. The server logs each alias resolution. Seven retired names cannot be mapped to one action and instead return a message naming the replacement. +Plane MCP server 0.3.0 consolidated 177 per-operation tools into 28 resource tools. Of those 177 names, the 169 aliases below still resolve, stay hidden from tool listings, and accept their original parameter names; `get_pql_reference` is unchanged; and seven cannot be mapped to one action and instead return a message naming the replacement. The server logs each alias resolution. ### Names without an alias diff --git a/docs/dev-tools/mcp-server.md b/docs/dev-tools/mcp-server.md index 0d95eb0d..60b21a00 100644 --- a/docs/dev-tools/mcp-server.md +++ b/docs/dev-tools/mcp-server.md @@ -82,6 +82,9 @@ Re-authenticate from your client's connector controls. In Claude Code, run `/mcp rm -rf ~/.mcp-auth ``` +This removes cached OAuth credentials for every `mcp-remote` server, not only Plane. To keep Plane's cache separate, +set `MCP_REMOTE_CONFIG_DIR` in that server's `env` and remove that directory instead. + ### Personal access token Send both headers on every request to the PAT endpoint: @@ -217,12 +220,12 @@ Desktop users who need a token instead of OAuth can bridge with `mcp-remote` (No "mcp-remote", "https://mcp.plane.so/http/api-key/mcp", "--header", - "Authorization: Bearer ${PLANE_PAT}", + "Authorization:${PLANE_AUTH_HEADER}", "--header", - "x-workspace-slug: ${PLANE_WORKSPACE_SLUG}" + "x-workspace-slug:${PLANE_WORKSPACE_SLUG}" ], "env": { - "PLANE_PAT": "", + "PLANE_AUTH_HEADER": "Bearer ", "PLANE_WORKSPACE_SLUG": "" } } @@ -346,7 +349,7 @@ Use `~/.cursor/mcp.json` globally or `.cursor/mcp.json` in a project. :::tabs key:mcp-auth == OAuth {#cursor-oauth} -[![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=plane&config=eyJ1cmwiOiJodHRwczovL21jcC5wbGFuZS5zby9odHRwL21jcCJ9) +[![Install in Cursor](/images/mcp/install-in-cursor.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=plane&config=eyJ1cmwiOiJodHRwczovL21jcC5wbGFuZS5zby9odHRwL21jcCJ9) Or add the server manually: @@ -405,7 +408,7 @@ Use `.vscode/mcp.json` for a workspace, or run **MCP: Open User Configuration** :::tabs key:mcp-auth == OAuth {#vs-code-oauth} -[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=plane&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D) +[![Install in VS Code](/images/mcp/install-in-vscode.svg)](https://vscode.dev/redirect/mcp/install?name=plane&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect/mcp/install?name=plane&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D&quality=insiders), or add it from the CLI: @@ -668,12 +671,12 @@ needs the OAuth URL. "mcp-remote", "https://mcp.plane.so/http/api-key/mcp", "--header", - "Authorization: Bearer ${PLANE_PAT}", + "Authorization:${PLANE_AUTH_HEADER}", "--header", - "x-workspace-slug: ${PLANE_WORKSPACE_SLUG}" + "x-workspace-slug:${PLANE_WORKSPACE_SLUG}" ], "env": { - "PLANE_PAT": "", + "PLANE_AUTH_HEADER": "Bearer ", "PLANE_WORKSPACE_SLUG": "" } } @@ -683,8 +686,10 @@ needs the OAuth URL. ::: -`mcp-remote` reads headers from its `--header` arguments; a `headers` key on this stdio entry is ignored. To reset -cached OAuth state, remove `~/.mcp-auth`. +`mcp-remote` reads headers from its `--header` arguments; a `headers` key on this stdio entry is ignored. Keep the +header values in `env` and write the arguments without spaces around the colon: on Windows, spaces inside `args` can be +mangled by some clients. To reset cached OAuth state, remove `~/.mcp-auth` (or the directory `MCP_REMOTE_CONFIG_DIR` +points to). ## Common workflows @@ -750,10 +755,12 @@ _Trace: `workitem retrieve_by_identifier` → `work_log create` → `state list` ## Self-hosted Plane The hosted `mcp.plane.so` service cannot reach private Plane instances. In stdio mode, set `PLANE_BASE_URL` to your -instance URL, then test the token against Plane's REST API: +instance URL, then test the token against Plane's REST API. Read the key into a shell variable first so it stays out +of your command history: ```bash -curl -H "x-api-key: YOUR_API_KEY" \ +read -rs PLANE_API_KEY # paste the key and press Enter; nothing is echoed +curl -H "x-api-key: $PLANE_API_KEY" \ "https://plane.yourcompany.com/api/v1/users/me/" ``` @@ -761,15 +768,18 @@ A `200` response confirms the key and URL. That header is the Plane REST API hea ::: tip Running your own MCP server? Follow the [self-hosting guide](/dev-tools/mcp-server-self-host) for Docker, Helm, OAuth, storage, and operations. +The OAuth transport needs Plane's OAuth application registration, which is available on Plane Cloud and Plane +Commercial Edition; on Community Edition, use stdio mode. ::: ## Upgrading ### From per-operation tools (0.2.x → 0.3.0) -The 177 per-operation tools became 28 resource tools. Of the retired names, 169 still resolve as hidden aliases and -keep their original parameter names, so saved prompts and scripts continue to work. Seven names cannot be mapped and -return a message naming their replacement; see [retired tool names](/dev-tools/mcp-server-tools#retired-tool-names). +The 177 per-operation tools became 28 resource tools. Of the 177 names, 169 still resolve as hidden aliases and keep +their original parameter names, so saved prompts and scripts continue to work; `get_pql_reference` is unchanged; and +seven cannot be mapped and return a message naming their replacement. See +[retired tool names](/dev-tools/mcp-server-tools#retired-tool-names). `project list` is now paginated by default. Follow `next_cursor` or pass `per_page`. Archive actions now return an explicit status object. diff --git a/docs/public/images/mcp/install-in-cursor.svg b/docs/public/images/mcp/install-in-cursor.svg new file mode 100644 index 00000000..3dacb7f1 --- /dev/null +++ b/docs/public/images/mcp/install-in-cursor.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/public/images/mcp/install-in-vscode.svg b/docs/public/images/mcp/install-in-vscode.svg new file mode 100644 index 00000000..0c1f164e --- /dev/null +++ b/docs/public/images/mcp/install-in-vscode.svg @@ -0,0 +1 @@ +VS Code: Install ServerVS CodeInstall Server \ No newline at end of file