-
Notifications
You must be signed in to change notification settings - Fork 99
feat: add tools for reading work item property values #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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]]: | ||
| """ | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 fRepository: makeplane/plane-mcp-server Length of output: 102 🏁 Script executed: wc -l plane_mcp/tools/work_item_properties.pyRepository: makeplane/plane-mcp-server Length of output: 114 🏁 Script executed: sed -n '340,415p' plane_mcp/tools/work_item_properties.pyRepository: makeplane/plane-mcp-server Length of output: 2798 🏁 Script executed: rg -i "property value not set" --type pyRepository: makeplane/plane-mcp-server Length of output: 52 🏁 Script executed: rg "work_item_properties.*values" --type py -A 3 -B 3Repository: 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 -100Repository: 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 IDRepository: 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 -20Repository: 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:
💡 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 -150Repository: 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 -20Repository: makeplane/plane-mcp-server Length of output: 52 🌐 Web query:
💡 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 |
||
|
|
||
| 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) | ||
Uh oh!
There was an error while loading. Please reload this page.