Skip to content
Open
Changes from 1 commit
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
144 changes: 144 additions & 0 deletions plane_mcp/tools/work_item_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any

from fastmcp import FastMCP
from plane.errors import HttpError
from plane.models.enums import PropertyType, RelationType
from plane.models.work_item_properties import (
CreateWorkItemProperty,
Expand Down Expand Up @@ -274,3 +275,146 @@ def delete_work_item_property(
type_id=type_id,
work_item_property_id=work_item_property_id,
)

@mcp.tool()
def list_work_item_property_values(
project_id: str,
work_item_id: str,
include_unset: bool = False,
) -> list[dict[str, Any]]:
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
List custom property values currently set on a work item.

Resolves the work item's type, lists all active property definitions for
that type, and fetches the value (if any) for each property. Useful for
retrieving member-type custom fields such as ``Responsible`` or ``Review``
whose values do not appear in ``retrieve_work_item``.

Args:
project_id: UUID of the project
work_item_id: UUID of the work item
include_unset: If True, include properties without a set value
(``value`` will be ``None``). Default False — only properties
with values are returned.

Returns:
List of dicts, one per property. Each dict contains:
- ``property_id``: UUID of the property definition
- ``display_name``: Human-readable name (e.g. "Responsible")
- ``name``: Internal name (slug)
- ``property_type``: TEXT/DATETIME/DECIMAL/BOOLEAN/OPTION/RELATION/URL/EMAIL/FILE
- ``relation_type``: USER/ISSUE for RELATION properties, else None
- ``is_multi``: Whether the property accepts multiple values
- ``value``: Single value or list of values (for multi). For
RELATION/USER it is the user UUID(s).
- ``value_record_ids``: ID(s) of the underlying value records.

Notes:
* Returns an empty list if the work item has no type assigned.
* Properties marked ``is_active=False`` are skipped.
"""
client, workspace_slug = get_plane_client_context()

work_item = client.work_items.retrieve(
workspace_slug=workspace_slug,
project_id=project_id,
work_item_id=work_item_id,
)
type_id = getattr(work_item, "type_id", None)
if not type_id:
return []

properties = client.work_item_properties.list(
workspace_slug=workspace_slug,
project_id=project_id,
type_id=type_id,
)

result: list[dict[str, Any]] = []
for prop in properties:
if not getattr(prop, "is_active", True):
continue

value: Any = None
value_record_ids: Any = None
try:
value_obj = client.work_item_properties.values.retrieve(
workspace_slug=workspace_slug,
project_id=project_id,
work_item_id=work_item_id,
property_id=prop.id,
)
except HttpError as exc:
if exc.status_code != 404:
raise
value_obj = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For the Plane Python SDK, what exact HttpError fields and payload/message are returned by work_item_properties.values.retrieve for (1) value not set and (2) invalid work_item_id/property_id 404s?

💡 Result:

For the Plane Python SDK, work_item_properties.values.retrieve raises an HttpError exception on non-2xx HTTP responses, including 404s for invalid work_item_id or property_id. HttpError fields (from SDK documentation and examples): - message: A string describing the error (e.g., the API error message like "The requested resource doesn't exist." for 404). - status_code: The HTTP status code (404 for both invalid work_item_id and invalid property_id cases). - response: The full HTTP response object, containing the raw API response body (JSON payload). (1) Value not set: No error is raised; returns a successful response (likely 200 OK with empty/null value or empty list for multi-value properties, as the endpoint retrieves values if they exist, but docs show it returns the value object when present. "Value not set" is handled as success with null/empty value, not a 404). (2) Invalid work_item_id or property_id: Raises HttpError with status_code=404. The API returns 404 "Not Found" when the requested resource (work item, property, or value) doesn't exist. Payload typically JSON like {"code": "not_found" or similar error code, "message": "The requested resource doesn't exist.", "details": {}} based on Plane API error format.

Citations:


🏁 Script executed:

cd plane_mcp && find . -name "work_item_properties.py" -type f

Repository: makeplane/plane-mcp-server

Length of output: 102


🏁 Script executed:

wc -l plane_mcp/tools/work_item_properties.py

Repository: makeplane/plane-mcp-server

Length of output: 114


🏁 Script executed:

sed -n '340,415p' plane_mcp/tools/work_item_properties.py

Repository: makeplane/plane-mcp-server

Length of output: 2798


🏁 Script executed:

rg -i "property value not set" --type py

Repository: makeplane/plane-mcp-server

Length of output: 52


🏁 Script executed:

rg "work_item_properties.*values" --type py -A 3 -B 3

Repository: makeplane/plane-mcp-server

Length of output: 1207


🏁 Script executed:

# Search for any API error handling patterns in the codebase
rg "HttpError" plane_mcp/tools/ --type py -B 3 -A 3 | head -100

Repository: makeplane/plane-mcp-server

Length of output: 1598


🏁 Script executed:

# Search for Plane SDK documentation or source about work_item_properties error handling
web_search
Plane Python SDK work_item_properties.values.retrieve API behavior 404 error when property value not set vs invalid ID

Repository: makeplane/plane-mcp-server

Length of output: 165


🏁 Script executed:

# Also check if there are any tests in this repo that show the expected behavior
fd -type f -name "*.py" | xargs rg -l "work_item_properties" | head -20

Repository: makeplane/plane-mcp-server

Length of output: 363


🏁 Script executed:

# Search for test files or examples
fd -type f -name "*test*.py" -o -name "*example*.py" | xargs rg "work_item_properties.values.retrieve" 2>/dev/null || echo "No test files found"

Repository: makeplane/plane-mcp-server

Length of output: 481


🌐 Web query:

Plane Python SDK work_item_properties values retrieve 404 error behavior documentation

💡 Result:

Plane Python SDK: work_item_properties retrieve 404 behavior What I found in authoritative docs/code 1) The Python SDK exposes work_item_properties.retrieve(...), mapping to Plane’s “Retrieve a custom property” endpoint: GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-item-types/{type_id}/work-item-properties/{property_id}/ The docs show this endpoint and its 200 response shape, but they do not document what 404 returns or its exact body schema. 2) Error handling in the SDK: The SDK raises plane.errors.HttpError on non-2xx responses, including the HTTP status_code and response body. So for a 404 from the retrieve endpoint, you should expect an exception (HttpError) containing status_code=404 and the server’s response payload (if any). 3) Plane docs for custom property retrieval do not specify 404 semantics. They only document the request path parameters (including workspace_slug, project_id, type_id, property_id) and the 200 response. Practical interpretation of “why you get 404” Because the docs don’t define 404 behavior, the most evidence-backed conclusion is: - A 404 occurs when the custom property resource identified by (workspace_slug, project_id, type_id, property_id) does not exist or is not addressable under that combination. - The SDK will surface that as an HttpError exception rather than returning None. Example: handling the 404 in the Plane Python SDK try: prop = client.work_item_properties.retrieve( workspace_slug, project_id, work_item_type_id, property_id, ) except HttpError as e: if e.status_code == 404: # property not found / not accessible print("Not found") else: raise If you want, share the exact call signature (values you pass for workspace_slug, project_id, type_id, property_id) and I can pinpoint the most likely mismatch causing the 404.

Citations:


🏁 Script executed:

# Check if there are any integration tests showing expected behavior
cat tests/test_integration.py | head -150

Repository: makeplane/plane-mcp-server

Length of output: 4716


🏁 Script executed:

# Try to find any Plane SDK references or documentation about error messages
rg "Property value not set" . -i 2>/dev/null || echo "Pattern not found in repo"

Repository: makeplane/plane-mcp-server

Length of output: 96


🏁 Script executed:

# Check if there's documentation about API error messages in the repo
find . -name "*.md" -o -name "*.rst" | xargs grep -i "404" 2>/dev/null | head -20

Repository: makeplane/plane-mcp-server

Length of output: 52


🌐 Web query:

Plane backend API work_item_properties values endpoint 404 error response message format

💡 Result:

The Plane backend API (https://api.plane.so/api/v1/) returns a 404 Not Found status code with a JSON error response when the work_item_properties values endpoint is not found. This typically occurs if the requested workspace_slug, project_id, work_item_id, or property_id does not exist. The standard error response format for Plane API 4xx errors, including 404, is a JSON object containing: - code: A short string indicating the specific error code (e.g., a machine-readable identifier). - message or detail: A human-readable message providing more details about the error. - details: An optional object with key-value pairs providing additional information (may be empty {}). Example structure (inferred from Plane documentation patterns): { "code": "not_found", "message": "The requested resource doesn't exist.", "details": {} } No exact 404 example for this specific endpoint was found in the official docs, but this format is consistently described across Plane's error handling documentation. The endpoint likely follows the pattern /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/work-item-properties/{property_id}/values/, and 404 indicates one of the path parameters is invalid.

Citations:


Distinguish between invalid IDs and unset property values.

The code at lines 347-350 and 406-409 treats all 404 errors as "value not set," but the Plane API returns 404 when the work_item_id or property_id is invalid, not when a value is unset. Unset values are returned as 200 OK with a null/empty response. Re-raise 404 errors to surface invalid resource IDs to the caller instead of silently treating them as unset.

The suggested guard pattern checking for "Property value not set" in the error message won't work—the API returns a generic 404 error format without that specific message. Consider instead removing the 404 catch entirely and letting invalid IDs surface as errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/work_item_properties.py` around lines 347 - 350, The except
HttpError blocks that catch 404 and set value_obj = None incorrectly treat
invalid work_item_id/property_id as "unset" — locate the two except HttpError as
exc blocks (currently checking exc.status_code != 404 and setting value_obj =
None) and remove the special-casing for 404 so that HttpError is re-raised for
invalid IDs; instead, only handle non-HttpError/no-error cases where the API
returns 200 with a null/empty body as an unset value. Ensure both occurrences
(the blocks at the shown diff and the similar block later) are updated so 404
errors bubble up rather than being converted to a "value not set" result.


if value_obj is None:
if not include_unset:
continue
elif isinstance(value_obj, list):
value = [item.value for item in value_obj]
value_record_ids = [item.id for item in value_obj]
else:
value = value_obj.value
value_record_ids = value_obj.id

result.append(
{
"property_id": prop.id,
"display_name": prop.display_name,
"name": getattr(prop, "name", None),
"property_type": _enum_value(prop.property_type),
"relation_type": _enum_value(prop.relation_type),
"is_multi": bool(getattr(prop, "is_multi", False)),
"value": value,
"value_record_ids": value_record_ids,
}
)

return result

@mcp.tool()
def retrieve_work_item_property_value(
project_id: str,
work_item_id: str,
property_id: str,
) -> dict[str, Any] | list[dict[str, Any]] | None:
"""
Retrieve the value(s) of a single custom property for a work item.

Args:
project_id: UUID of the project
work_item_id: UUID of the work item
property_id: UUID of the property definition

Returns:
For single-value properties: a dict (or ``None`` if the value is
not set). For multi-value properties: a list of dicts.
Each dict mirrors ``WorkItemPropertyValueDetail`` (id, value,
value_type, property_id, issue_id, created_at, updated_at, ...).
"""
client, workspace_slug = get_plane_client_context()

try:
value_obj = client.work_item_properties.values.retrieve(
workspace_slug=workspace_slug,
project_id=project_id,
work_item_id=work_item_id,
property_id=property_id,
)
except HttpError as exc:
if exc.status_code == 404:
return None
raise

if isinstance(value_obj, list):
return [item.model_dump() for item in value_obj]
return value_obj.model_dump()


def _enum_value(value: Any) -> Any:
"""Convert an enum-like field to its plain value, leave other types alone."""
if value is None:
return None
return getattr(value, "value", value)