Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,6 @@ The server provides comprehensive tools for interacting with Plane. All tools us
| `update_work_item_property` | Update a work item property with partial data |
| `delete_work_item_property` | Delete a work item property by ID |

### Epics

| Tool Name | Description |
|-----------|-------------|
| `list_epics` | List all epics in a project |
| `create_epic` | Create a new epic |
| `retrieve_epic` | Retrieve an epic by ID |
| `update_epic` | Update an epic by ID |
| `delete_epic` | Delete an epic by ID |

### Milestones

| Tool Name | Description |
Expand Down Expand Up @@ -294,6 +284,8 @@ The server provides comprehensive tools for interacting with Plane. All tools us
| `retrieve_work_item_type` | Retrieve a work item type by ID |
| `update_work_item_type` | Update a work item type by ID |
| `delete_work_item_type` | Delete a work item type by ID |
| `import_work_item_types_to_project` | Bulk-link workspace-level work item types to a project |
| `resolve_work_item_type` | Find or create a named type for a project, auto-handling workspace vs project scope and import |

### Work Item Relations

Expand Down
45 changes: 45 additions & 0 deletions plane_mcp/instructions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Server-level instructions sent once to MCP clients (FastMCP `instructions` param)."""

WORK_ITEM_TYPE_SCOPING_INSTRUCTIONS = """
## Work item type scoping

To get a usable work item type for a project (e.g. "Epic", "Initiative"), call resolve_work_item_type(project_id, name). It returns the type (its id is the type_id for create_work_item) and handles everything in one step:
- If the workspace owns work item types, it finds or creates the type at the workspace level and imports it into the project (project-level creation is not allowed in this mode).
- Otherwise it finds or creates the type at the project level, enabling the project's work item types feature first if needed.

Prefer this single tool over manually combining get_workspace_features, list_work_item_types, create_work_item_type, and import_work_item_types_to_project — it does all of that deterministically and never creates a duplicate.
"""

EPIC_INSTRUCTIONS = """
## Epics

This server has no dedicated epic tools (no create_epic, list_epics, retrieve_epic, update_epic, delete_epic, list_epic_issues, add_epic_issues). An "epic" is just a work item whose work item type is named "Epic".

1. type = resolve_work_item_type(project_id, "Epic") — see "Work item type scoping".
2. Create: create_work_item(project_id=project_id, type_id=type.id, name=<epic name>).
3. List epics: list_work_items(project_id=project_id, pql='type = "<type id>"') (or pql='isEpic()').
4. Read, update, or delete: retrieve_work_item / update_work_item / delete_work_item, using the epic's work item id.
5. Nest a work item under an epic: create_work_item or update_work_item with parent=<epic work item id>.
6. List an epic's children: list_work_items(project_id=project_id, pql='childOf("<EPIC-IDENTIFIER>")'), where <EPIC-IDENTIFIER> is the epic's human-readable identifier (e.g. "PROJ-12") from retrieve_work_item.
"""

INITIATIVE_INSTRUCTIONS = """
## Initiatives

Call get_workspace_features() first. Pick exactly one path — never mix them.

If initiatives is true — native workspace-level objects (no project_id needed):
- Create: create_initiative(name=...).
- List: list_initiatives().
- Read/update/delete: retrieve_initiative / update_initiative / delete_initiative by initiative id.

If initiatives is false — fall back to an "Initiative" work item type inside a project:
1. If the user has not named a project, ask which project to use before proceeding.
2. type = resolve_work_item_type(project_id, "Initiative") — handles everything: checks if the type is already in the project, finds or creates it at the workspace level if workspace owns types (the common case — "Initiative" is normally a workspace-level type imported into projects), or creates it at the project level if the project owns its own types. Never creates a duplicate.
3. Create: create_work_item(project_id=project_id, type_id=type.id, name=<initiative name>).
4. List: list_work_items(project_id=project_id, pql='type = "<type id>"').
5. Read/update/delete: retrieve_work_item / update_work_item / delete_work_item by work item id.
Use this fallback only when initiatives is false.
"""

SERVER_INSTRUCTIONS = WORK_ITEM_TYPE_SCOPING_INSTRUCTIONS + EPIC_INSTRUCTIONS + INITIATIVE_INSTRUCTIONS
4 changes: 4 additions & 0 deletions plane_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from mcp.types import Icon

from plane_mcp.auth import PlaneHeaderAuthProvider, PlaneOAuthProvider
from plane_mcp.instructions import SERVER_INSTRUCTIONS
from plane_mcp.storage import build_token_store
from plane_mcp.tools import register_tools

Expand All @@ -17,6 +18,7 @@ def get_oauth_mcp(base_path: str = "/") -> FastMCP:
"""Build the FastMCP instance for the OAuth HTTP / SSE transports."""
oauth_mcp = FastMCP(
"Plane MCP Server",
instructions=SERVER_INSTRUCTIONS,
icons=[Icon(src="https://plane.so/favicon.ico", alt="Plane MCP Server")],
website_url="https://plane.so",
auth=PlaneOAuthProvider(
Expand Down Expand Up @@ -53,6 +55,7 @@ def get_oauth_mcp(base_path: str = "/") -> FastMCP:
def get_header_mcp():
header_mcp = FastMCP(
"Plane MCP Server (header-http)",
instructions=SERVER_INSTRUCTIONS,
auth=PlaneHeaderAuthProvider(
required_scopes=["read", "write"],
),
Expand All @@ -65,6 +68,7 @@ def get_header_mcp():
def get_stdio_mcp():
stdio_mcp = FastMCP(
"Plane MCP Server (stdio)",
instructions=SERVER_INSTRUCTIONS,
)
stdio_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True))
register_tools(stdio_mcp)
Expand Down
2 changes: 0 additions & 2 deletions plane_mcp/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from fastmcp import FastMCP

from plane_mcp.tools.cycles import register_cycle_tools
from plane_mcp.tools.epics import register_epic_tools
from plane_mcp.tools.initiatives import register_initiative_tools
from plane_mcp.tools.intake import register_intake_tools
from plane_mcp.tools.labels import register_label_tools
Expand Down Expand Up @@ -45,6 +44,5 @@ def register_tools(mcp: FastMCP) -> None:
register_work_item_type_tools(mcp)
register_state_tools(mcp)
register_workspace_tools(mcp)
register_epic_tools(mcp)
register_milestone_tools(mcp)
register_pql_tools(mcp)
Loading