Skip to content
Open
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
27 changes: 27 additions & 0 deletions plane_mcp/tools/milestones.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Milestone-related tools for Plane MCP Server."""

from functools import wraps
from typing import Any

from fastmcp import FastMCP
Expand All @@ -15,10 +16,30 @@
from plane_mcp.client import get_plane_client_context


def _handle_not_available_on_self_hosted(func):
"""Decorator to handle milestones not being available on Plane self-hosted."""

@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
error_str = str(e)
if "404" in error_str or "Page not found" in error_str:
raise ValueError(
"Milestones are not available on Plane self-hosted instances. "
"This feature is only available on Plane Cloud (app.plane.so)."
) from e
Comment on lines +26 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the SDK/client exception shapes and existing 404 handling before tightening this condition.
rg -n -C4 --type py 'Page not found|status_code|response\.status|HTTP.*404|404' plane_mcp tests || true
rg -n -C4 --type py 'client\.milestones\.(retrieve|update|delete|list|create|list_work_items|add_work_items|remove_work_items)' plane_mcp tests || true

Repository: makeplane/plane-mcp-server

Length of output: 20954


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the milestones tool implementation and any nearby HTTP error handling patterns.
wc -l plane_mcp/tools/milestones.py
cat -n plane_mcp/tools/milestones.py | sed -n '1,260p'

printf '\n--- matching error-handling patterns ---\n'
rg -n -C3 'except (Exception|HttpError)|status_code == 404|Page not found|self-hosted instances|not available on Plane self-hosted' plane_mcp || true

Repository: makeplane/plane-mcp-server

Length of output: 20292


Avoid treating every milestone 404 as a self-hosted-only error.

Matching on "404" here will also catch legitimate cloud-side not-found cases (for example, invalid project_id or milestone_id) and misreport them as a Plane Cloud limitation. Restrict this to the specific self-hosted endpoint-missing response, or use a generic 404 message when the cause is ambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane_mcp/tools/milestones.py` around lines 26 - 32, The milestone error
handling in the `except Exception as e` block is too broad because it treats any
404-like failure as a self-hosted-only limitation. Update the logic in
`Milestones` to only raise the Plane self-hosted message when the response
clearly indicates the milestones endpoint is missing on self-hosted, and leave
ambiguous or cloud-side 404s as generic not-found errors. Use the existing
`error_str` check in `tools/milestones.py` to narrow the match to the specific
endpoint-missing response instead of `"404"` alone.

raise

return wrapper


def register_milestone_tools(mcp: FastMCP) -> None:
"""Register all milestone-related tools with the MCP server."""

@mcp.tool()
@_handle_not_available_on_self_hosted
def list_milestones(
project_id: str,
params: dict[str, Any] | None = None,
Expand All @@ -40,6 +61,7 @@ def list_milestones(
return response.results

@mcp.tool()
@_handle_not_available_on_self_hosted
def create_milestone(
project_id: str,
title: str,
Expand Down Expand Up @@ -72,6 +94,7 @@ def create_milestone(
return client.milestones.create(workspace_slug=workspace_slug, project_id=project_id, data=data)

@mcp.tool()
@_handle_not_available_on_self_hosted
def retrieve_milestone(project_id: str, milestone_id: str) -> Milestone:
"""
Retrieve a milestone by ID.
Expand All @@ -89,6 +112,7 @@ def retrieve_milestone(project_id: str, milestone_id: str) -> Milestone:
)

@mcp.tool()
@_handle_not_available_on_self_hosted
def update_milestone(
project_id: str,
milestone_id: str,
Expand Down Expand Up @@ -128,6 +152,7 @@ def update_milestone(
)

@mcp.tool()
@_handle_not_available_on_self_hosted
def delete_milestone(project_id: str, milestone_id: str) -> None:
"""
Delete a milestone by ID.
Expand All @@ -140,6 +165,7 @@ def delete_milestone(project_id: str, milestone_id: str) -> None:
client.milestones.delete(workspace_slug=workspace_slug, project_id=project_id, milestone_id=milestone_id)

@mcp.tool()
@_handle_not_available_on_self_hosted
def manage_milestone_work_items(
project_id: str,
milestone_id: str,
Expand Down Expand Up @@ -176,6 +202,7 @@ def manage_milestone_work_items(
)

@mcp.tool()
@_handle_not_available_on_self_hosted
def list_milestone_work_items(
project_id: str,
milestone_id: str,
Expand Down