Skip to content

Plane MCP is live: three operational gotchas you only feel after using it

Adityo Guni Waluyo

No workspace delete via MCP, CORS reused as CSRF, and an API token sitting in .mcp.json next to the code.

TL;DR

Workspace deletion isn't exposed via API and requires double confirmation in the UI to prevent irreversible wipes by AI agents. CORS and CSRF share the single CORS_ALLOWED_ORIGINS variable, so adding an origin requires recreating all backend containers, not the proxy. API tokens sit in plaintext in .mcp.json, so you must gitignore the file and use expiring tokens.

Right after the install finished, I tried what should be the easiest thing: deleting the test workspace I had created minutes earlier. I sent the command and got nothing back. The workspace was still there. I went through the tool list one by one, across the 30 tools and 204 operations advertised in the plane-mcp-server README [1]. Not a single one can delete a workspace.

My first guess was wrong. I assumed the official server would expose every operation. In fact, the upstream workspace.py file only defines three actions: retrieve, get_features, and update_features [3]. Delete simply does not exist.

Workspace deletion is UI-only, and that is the right call

A workspace is the tenant root. Every project, work item, and bit of activity history hangs off it. Deleting it through one mistyped API call would be an unrecoverable disaster. So the deletion path is deliberately layered inside the web UI: open Workspace Settings, type the workspace name exactly (case-sensitive), then type the phrase delete my workspace into a second confirmation field. Only then does the button activate.

This layer matters more now that MCP tools get called by autonomous AI agents instead of humans who think twice. If deleting a workspace were just another action, one small hallucination from an agent would be enough to wipe a production tenant. That exact typed phrase is the guardrail. This is not an MCP limitation, it is a sensible security decision. Once I understood the flow, I stopped looking for an API path.

CORS and CSRF turn out to be one variable

The second need showed up when I opened the dashboard from a different origin. The browser blocked it with a CORS error. The standard fix: add the new origin to CORS_ALLOWED_ORIGINS in .env, following Plane's self-hosting environment variables reference [5]. I did exactly that, restarted the reverse proxy, and the error stayed.

My assumption was that CORS and CSRF were two separate configs. Reading the source told a different story: CSRF_TRUSTED_ORIGINS = cors_allowed_origins [2]. Plane's Django layer has no CSRF variable of its own; it reuses the same origin list.

The practical consequence: editing .env alone is not enough. Environment variables are read at container start, so the api, worker, beat, and migrator containers have to be recreated with docker compose up -d --force-recreate, not just the proxy. My way of confirming the fix stuck: a hard refresh of the dashboard. If the CSRF error still shows, some container was not recreated. It is a bit more ceremony, but I will take it: one source of truth for origins is safer than two variables that can drift apart.

The API token lives in .mcp.json, right next to the code

Plane API keys are sent through the X-API-Key header, and the official API documentation is explicit that the key must be treated like a password, with an optional expiry set at creation [4]. In reality, the Claude Code client config stores that token as plain text in .mcp.json at the repo root.

The habit that saves you: .mcp.json goes into .gitignore, and git status gets checked before every commit. If this one file leaks, the workspace token goes public in git history. The alternative I chose for Hermes: credentials stay in the main config that never travels with a repo, not in a side file that is easy to sweep up by git add ..

One small practice I also follow: tokens used by AI clients always have an expiry, never a permanent one. If the config file ever lands somewhere it should not, the damage has a time limit, and rotating means creating a new token and changing one config line.

None of these three things show up while the install is succeeding. They only surface once the server is actually used: one operation that does not exist, one variable playing two roles, one file quietly holding a credential. The lesson is one line: when the docs and the real behavior disagree, open the source. Source code rarely lies.

Related articles