plane-mcp
by bhflm
README.md
# plane-mcp
Local MCP server for a self-hosted Plane instance. Exposes tickets
(work items), cycles, modules, labels, and states as MCP tools, plus a
small set of mutation tools (label add/remove, state changes, ticket
create/update, module attach).
The Plane API key is read only from the `PLANE_API_KEY` environment
variable. It is never logged, never printed, and never included in tool
output — errors are redacted before being returned.
## Setup
```bash
npm install
npm run build
export PLANE_BASE_URL="https://your-plane-instance.example.com"
export PLANE_API_KEY="..."
export PLANE_DEFAULT_WORKSPACE="..." # optional
export PLANE_DEFAULT_PROJECT_ID="..." # optional
export PLANE_DEFAULT_PROJECT_IDENTIFIER="..." # optional
export PLANE_ENABLE_WRITES="false" # optional; must be true for mutations
```
Copy `.env.example` to `.env` for reference (placeholders only — never
commit a real `.env`).
## Run
```bash
npm start
# or during development:
npm run dev
```
The server speaks MCP over stdio.
## MCP client config (stdio)
```json
{
"mcpServers": {
"plane": {
"command": "node",
"args": ["/absolute/path/to/plane-mcp/dist/src/index.js"],
"env": {
"PLANE_BASE_URL": "https://your-plane-instance.example.com",
"PLANE_API_KEY": "${PLANE_API_KEY}",
"PLANE_DEFAULT_WORKSPACE": "${PLANE_DEFAULT_WORKSPACE}",
"PLANE_DEFAULT_PROJECT_ID": "${PLANE_DEFAULT_PROJECT_ID}",
"PLANE_ENABLE_WRITES": "false"
}
}
}
}
```
## Exposed tools
| Tool | Purpose |
|---|---|
| `plane_list_workspaces` | List accessible workspaces |
| `plane_list_projects` | List projects in a workspace |
| `plane_resolve_project` | Resolve a project identifier (e.g. `SDK`) to its UUID |
| `plane_search_tickets` | Search work items by query/key |
| `plane_get_ticket` | Fetch a full ticket by UUID or key (e.g. `SDK-250`) |
| `plane_list_cycles` | List cycles (sprints/milestones) |
| `plane_get_current_cycle` | Fetch the current active cycle |
| `plane_list_cycle_tickets` | List tickets in a cycle |
| `plane_list_current_cycle_tickets` | One-shot "what's in the current cycle"; link summaries by default |
| `plane_list_modules` | List modules (epics/milestones) |
| `plane_list_module_tickets` | List tickets in a module |
| `plane_list_labels` | List labels for a project |
| `plane_add_labels_to_ticket` | Add labels without clobbering existing ones |
| `plane_remove_labels_from_ticket` | Remove labels from a ticket |
| `plane_update_ticket` | Patch basic ticket fields |
| `plane_add_comment` | Add a comment to a ticket/work item |
| `plane_list_states` | List states/statuses |
| `plane_set_ticket_state` | Move a ticket to a state by name or id |
| `plane_create_ticket` | Create a ticket, optionally attach to cycle/module |
| `plane_add_ticket_to_module` | Attach an existing ticket to a module |
| `plane_get_ticket_children` | List child tickets by filtering on `parent` |
Tools return normalized, allowlisted summaries rather than raw Plane API
records. Mutating tools accept `dry_run: boolean` for previews. Real writes
are disabled unless `PLANE_ENABLE_WRITES=true` is set, and each mutating call
must also pass `confirm: true`; successful ticket updates are verified by
re-fetching the record afterward, while comment creation returns the created
comment from Plane.
`plane_add_comment` accepts either `comment_text` or `comment_html`.
`comment_text` is escaped and converted to simple paragraph HTML before it is
sent to Plane. Comments default to `access: "INTERNAL"` unless supplied.
## Testing
```bash
npm test
```
Unit tests mock `fetch` — no real Plane API key is required. Coverage
includes: auth header presence without leaking the key, redaction of
secrets from errors and result payloads, safe path construction, bounded
cursor pagination, schema limits, ticket-key normalization, and label-union
(non-clobbering) behavior.
## Smoke test
With real credentials exported (see Setup), you can drive the server
directly with the MCP inspector, or manually via any MCP-compatible
client pointed at `node dist/src/index.js`. A minimal manual check:
```bash
node --input-type=module -e "
import { PlaneClient } from './dist/src/planeClient.js';
const client = new PlaneClient({
baseUrl: process.env.PLANE_BASE_URL,
apiKey: process.env.PLANE_API_KEY,
defaultWorkspace: process.env.PLANE_DEFAULT_WORKSPACE,
});
console.log(await client.paginate('/api/v1/workspaces/'));
"
```
## Known quirks (self-hosted Plane)
- `GET /api/v1/workspaces/` may 404 (no cross-workspace listing endpoint on
some self-hosted deployments). Set `PLANE_DEFAULT_WORKSPACE` and use
workspace-scoped tools directly instead of relying on
`plane_list_workspaces`.
- Advanced search may return no results when `project_id` is included;
the client retries workspace-wide and filters client-side.
- Cycle-issue and module-issue list endpoints may return link objects
(`{ issue: <uuid> }`) rather than full ticket records. Current-cycle tools
keep link summaries by default; pass `include_full_tickets: true` to join
normalized ticket summaries.
## Security
- Credentials are read only from environment variables (`PLANE_API_KEY`,
`PLANE_BASE_URL`).
- The API key is never printed, logged, or embedded in responses.
- Errors (HTTP failures, network failures, JSON parse failures) are
passed through a redactor before being surfaced to the MCP client.
- Tool arguments used in Plane URL paths are validated and encoded before
request construction.
- Tool outputs are allowlisted and redacted; raw Plane API records are not
returned to MCP clients.
- Writes are off by default and require both `PLANE_ENABLE_WRITES=true` and
`confirm: true` on each mutating call.
- `.env` is gitignored; only `.env.example` with placeholders is committed.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues