Metadata-Version: 2.5
Name: plane-mcp-oss
Version: 0.1.1
Summary: MCP server that exposes the Plane project management API to AI clients, built with FastMCP.
Project-URL: Homepage, https://github.com/abelsr/plane-mcp-oss
Project-URL: Repository, https://github.com/abelsr/plane-mcp-oss
Project-URL: Issues, https://github.com/abelsr/plane-mcp-oss/issues
Project-URL: Changelog, https://github.com/abelsr/plane-mcp-oss/blob/main/CHANGELOG.md
Project-URL: Security, https://github.com/abelsr/plane-mcp-oss/security/policy
Author-email: Abel Santillan Rodriguez <abelsantillanrdz@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,fastmcp,mcp,model-context-protocol,plane,project-management
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: fastmcp>=2.0
Requires-Dist: httpx>=0.27
Requires-Dist: python-dotenv>=1.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# Plane MCP Server

[![CI](https://github.com/abelsr/plane-mcp-oss/actions/workflows/ci.yml/badge.svg)](https://github.com/abelsr/plane-mcp-oss/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/plane-mcp-oss.svg)](https://pypi.org/project/plane-mcp-oss/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
[![Built with FastMCP](https://img.shields.io/badge/built%20with-FastMCP-blueviolet.svg)](https://gofastmcp.com)
[![MCP](https://img.shields.io/badge/protocol-MCP-6f42c1.svg)](https://modelcontextprotocol.io)

An [MCP](https://modelcontextprotocol.io) server that exposes the
[Plane](https://plane.so) REST API to AI clients (Claude, Cursor, VS Code,
Codex, …). Built with [FastMCP](https://gofastmcp.com).

It authenticates as a Plane personal access token and acts on a single
workspace, giving the model tools to read and manage projects, work items,
states, labels, cycles, modules, members and comments.

## Requirements

- Python 3.10+
- A Plane account and a **personal access token**:
  *Plane → Profile settings → Personal access tokens → Add personal access token*
- Your **workspace slug** — the segment in your Plane URL:
  `https://app.plane.so/<workspace-slug>/projects/`

## Install

From PyPI — package `plane-mcp-oss`:

```bash
uvx plane-mcp-oss            # run without installing
# or
pip install plane-mcp-oss    # installs the `plane-mcp` and `plane-mcp-oss` commands
```

From source:

```bash
uv sync            # installs fastmcp + httpx into .venv
# or, without uv:
pip install -e .
```

## Configure

The server targets one Plane **instance** and one **workspace**. Settings are
resolved with the following precedence (highest wins):

1. CLI flags (`--base-url`, `--workspace`, `--api-key`)
2. Process environment variables
3. A `.env` file in the working directory (loaded automatically)
4. Built-in defaults

| Variable | Alias | Required | Default | Purpose |
|---|---|---|---|---|
| `PLANE_API_KEY` | `PLANE_TOKEN` | yes* | — | Personal access token, sent as `X-API-Key`. |
| `PLANE_OAUTH_TOKEN` | — | yes* | — | OAuth access token, sent as `Authorization: Bearer …`. |
| `PLANE_WORKSPACE_SLUG` | `PLANE_WORKSPACE` | yes | — | Target workspace slug. |
| `PLANE_BASE_URL` | `PLANE_URL` | no | `https://api.plane.so` | Plane instance URL. |
| `PLANE_TIMEOUT` | — | no | `30` | Request timeout (seconds). |

\* One of `PLANE_API_KEY` / `PLANE_OAUTH_TOKEN` is required.

### Pointing at a self-hosted instance

`PLANE_BASE_URL` accepts whatever you copy from your browser. The `/api/v1`
suffix is added automatically when needed:

```bash
PLANE_BASE_URL=https://api.plane.so            # Plane Cloud (default)
PLANE_BASE_URL=https://plane.example.com       # self-hosted
PLANE_BASE_URL=https://example.com/plane       # self-hosted behind a subpath
PLANE_BASE_URL=https://plane.example.com/api/v1  # already versioned
```

### CLI flags

```bash
plane-mcp --base-url https://plane.example.com/plane \
          --workspace my-team \
          --api-key plane_api_xxxx
```

Check what the server resolved — without leaking the token:

```bash
$ plane-mcp --show-config --base-url https://plane.example.com/plane --workspace my-team --api-key xxx
{
  "base_url": "https://plane.example.com/plane/api/v1",
  "workspace_slug": "my-team",
  "auth": "api_key",
  "timeout": 30.0
}
```

Copy `.env.example` for a template; a `.env` file is loaded from the working
directory (change it with `--env-file`, or pass `--env-file ''` to skip).


## Run

```bash
# stdio — how MCP clients launch it locally
PLANE_API_KEY=... PLANE_WORKSPACE_SLUG=my-team uv run plane-mcp

# streamable HTTP
PLANE_API_KEY=... PLANE_WORKSPACE_SLUG=my-team uv run plane-mcp --transport http --port 8000
```

`python -m plane_mcp` and `python main.py` are equivalent entry points.

## Client setup

Every example below runs the published package with `uvx plane-mcp-oss`, so
nothing needs to be installed first. For a **self-hosted** instance, add
`PLANE_BASE_URL` to the same environment (`https://plane.example.com`).

### Claude Code

```bash
claude mcp add plane \
  -e PLANE_API_KEY=plane_api_xxxxxxxx \
  -e PLANE_WORKSPACE_SLUG=my-team \
  -- uvx plane-mcp-oss

# add --scope user to make it available in every project (default is "local")
claude mcp list
```

Or commit a project-scoped `.mcp.json` so the whole team gets it:

```json
{
  "mcpServers": {
    "plane": {
      "command": "uvx",
      "args": ["plane-mcp-oss"],
      "env": {
        "PLANE_API_KEY": "plane_api_xxxxxxxx",
        "PLANE_WORKSPACE_SLUG": "my-team"
      }
    }
  }
}
```

### OpenAI Codex

```bash
codex mcp add plane \
  --env PLANE_API_KEY=plane_api_xxxxxxxx \
  --env PLANE_WORKSPACE_SLUG=my-team \
  -- uvx plane-mcp-oss

codex mcp list
```

Codex writes this to `~/.codex/config.toml`:

```toml
[mcp_servers.plane]
command = "uvx"
args = ["plane-mcp-oss"]

[mcp_servers.plane.env]
PLANE_API_KEY = "plane_api_xxxxxxxx"
PLANE_WORKSPACE_SLUG = "my-team"
```

### phoson-cli

Add an entry to `~/.phoson/mcps.json` under `mcpServers`:

```json
{
  "mcpServers": {
    "plane": {
      "command": "uvx",
      "args": ["plane-mcp-oss"],
      "env": {
        "PLANE_API_KEY": "plane_api_xxxxxxxx",
        "PLANE_WORKSPACE_SLUG": "my-team"
      },
      "enabled": true
    }
  }
}
```

### Cursor

`~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):

```json
{
  "mcpServers": {
    "plane": {
      "command": "uvx",
      "args": ["plane-mcp-oss"],
      "env": {
        "PLANE_API_KEY": "plane_api_xxxxxxxx",
        "PLANE_WORKSPACE_SLUG": "my-team"
      }
    }
  }
}
```

### VS Code

`.vscode/mcp.json` (note the `servers` key and `type`):

```json
{
  "servers": {
    "plane": {
      "type": "stdio",
      "command": "uvx",
      "args": ["plane-mcp-oss"],
      "env": {
        "PLANE_API_KEY": "plane_api_xxxxxxxx",
        "PLANE_WORKSPACE_SLUG": "my-team"
      }
    }
  }
}
```

VS Code can prompt for the token instead of storing it — add an `inputs` entry
and reference it as `${input:plane_api_key}`.

### Claude Desktop

`claude_desktop_config.json` (Settings → Developer → Edit Config):

```json
{
  "mcpServers": {
    "plane": {
      "command": "uvx",
      "args": ["plane-mcp-oss"],
      "env": {
        "PLANE_API_KEY": "plane_api_xxxxxxxx",
        "PLANE_WORKSPACE_SLUG": "my-team"
      }
    }
  }
}
```

### Remote / HTTP transport

Any client that supports remote MCP servers can connect over HTTP instead of
spawning a process:

```bash
PLANE_API_KEY=... PLANE_WORKSPACE_SLUG=my-team plane-mcp-oss --transport http --port 8000
# endpoint: http://127.0.0.1:8000/mcp
```

Clients without native remote support can bridge to it with `mcp-remote`:

```json
{
  "mcpServers": {
    "plane": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:8000/mcp"]
    }
  }
}
```

> Keep tokens out of version control: prefer the client's secret input
> mechanism, or reference an environment variable your client expands, instead
> of committing a real key to `.mcp.json`.


## Tools

| Tool | What it does |
|---|---|
| `get_current_user` | Profile of the token's user. |
| `list_workspace_members` | Workspace members (to resolve assignee UUIDs). |
| `list_projects` / `get_project` | Browse projects (paginated). |
| `create_project` / `update_project` | Create or edit a project. |
| `list_work_items` / `get_work_item` | Browse work items in a project (paginated). |
| `get_work_item_by_identifier` | Look up e.g. `PROJ-123` directly. |
| `search_work_items` | Text search across names/identifiers (works everywhere). |
| `advanced_search_work_items` | Filter-based search; permission-gated, may return 403. |
| `create_work_item` / `update_work_item` / `delete_work_item` | Manage work items. |
| `list_states` | Workflow states — get the UUID before setting `state`. |
| `list_labels` / `create_label` | Project labels. |
| `list_cycles` / `list_modules` | Sprints and modules. |
| `list_comments` / `add_comment` / `update_comment` / `delete_comment` | Work item comments. |
| `list_pages` / `get_page` / `create_page` / `update_page` | Pages — workspace wiki or project (omit/ pass `project_id`). |
| `archive_page` / `restore_page` / `delete_page` | Page lifecycle; delete requires archiving first. |

List tools return `{results, count, total_results, next_cursor}`; pass
`next_cursor` back to page through results.

**Resources:** `plane://me`, `plane://projects`, `plane://projects/{id}/states`.
**Prompts:** `triage_work_items`.

### Notes on the API

- Work item `state`, `assignees` and `labels` take **UUIDs**, not names. Call
  `list_states` / `list_labels` / `list_workspace_members` first.
- `description` is plain text and is converted to the `description_html` the
  API expects; `description_html` overrides it when supplied.
- Priority is one of `urgent`, `high`, `medium`, `low`, `none`.
- The API allows 60 requests/minute per key; the client surfaces `429` as a
  tool error so the model can retry.

## Architecture

```
src/plane_mcp/
├── config.py   # env-driven Settings + validation
├── client.py   # async httpx wrapper: auth, URLs, error translation, pagination
├── server.py   # FastMCP instance, tool/resource/prompt definitions, CLI
└── __main__.py # `python -m plane_mcp`
tests/          # offline: httpx.MockTransport + in-memory FastMCP client
```

`client.py` has no FastMCP dependency, so it is reusable and easy to test; the
server layer only maps tools to client calls and turns `PlaneAPIError` into
`ToolError` for clean MCP error messages.

## Known limitations

- **Pages are Plane Cloud only.** The public Pages REST API is **not part of the
  open-source Community Edition** — it is absent from the API URL routing at
  `v1.3.1`, `v1.4.2` *and* `master` (`apps/api/plane/api/urls/` registers
  asset, cycle, intake, label, member, module, project, state, user, work_item,
  invite and sticky — no pages). On a self-hosted instance, pages exist in the
  UI behind an internal session API (`/api/…`) that rejects `X-API-Key` and
  Bearer tokens, so the page tools will 404 there. They work against Plane
  Cloud, where the documented `/api/v1/…/pages/` routes exist. The page tools
  detect this and return an explanatory error rather than a bare 404.
- **`advanced_search_work_items` is permission-gated** on some workspaces and
  editions and can return `403`. Use `search_work_items` or `list_work_items`
  as a fallback.
- **Not implemented yet:** work item links, attachments, activity feed, and
  custom properties/types, though the Plane API supports them.


## Development

```bash
uv run pytest        # 12 offline tests, no credentials needed
```

## Extending

Add a method to `PlaneClient` for the endpoint you need (see the
[API reference](https://developers.plane.so/api-reference/introduction)), then
register a tool in `server.py`:

```python
@mcp.tool
async def list_pages(project_id: str) -> dict[str, Any]:
    """List a project's pages."""
    client = get_client()
    data = await _call(client.request("GET", client._workspace("projects", project_id, "pages")))
    return summarize_paginated(data)
```

## Contributing

Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the
development setup, how to add a tool, and commit/PR guidelines. This project
follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).

## Security

Please report vulnerabilities privately — see [SECURITY.md](SECURITY.md). Never
commit real credentials; `.env` is git-ignored.

## License

Released under the [MIT License](LICENSE). © 2026 Abel Santillan Rodriguez.

See [CHANGELOG.md](CHANGELOG.md) for release history.
