Skip to content

feat: add tools for reading work item property values#117

Open
151813125 wants to merge 2 commits intomakeplane:mainfrom
151813125:feat/work-item-property-values
Open

feat: add tools for reading work item property values#117
151813125 wants to merge 2 commits intomakeplane:mainfrom
151813125:feat/work-item-property-values

Conversation

@151813125
Copy link
Copy Markdown

@151813125 151813125 commented Apr 28, 2026

Summary

Add two read-only MCP tools that expose work item custom property values (e.g. Responsible, Review, or any other RELATION/OPTION/TEXT… property). Both tools wrap the existing client.work_item_properties.values resource that already lives in plane-sdk but was previously not exposed through MCP.

  • list_work_item_property_values(project_id, work_item_id, include_unset=False) — high-level helper. Resolves the work item's type_id internally, lists all active property definitions for that type, and returns a flat list of {property_id, display_name, name, property_type, relation_type, is_multi, value, value_record_ids}. By default only properties with a set value are returned; pass include_unset=True to also list active properties without values.
  • retrieve_work_item_property_value(project_id, work_item_id, property_id) — granular retrieve. Returns a dict mirroring WorkItemPropertyValueDetail for single-value properties, a list of dicts for multi-value properties, and None if the value is not set (Plane returns 404 \"Property value not set\" in that case, and we trap it).

No write tools (set / delete) are added in this PR — kept intentionally minimal.

Motivation

Member-type custom fields (RELATION/USER) are not returned by retrieve_work_item under any value of expand (property_values, properties, issue_properties, custom_properties were all tried — none populate the field on the response). Until now, MCP clients had no way to read these properties at all, even though the underlying Plane API and the SDK already support it (/work-items/{id}/work-item-properties/{property_id}/values/).

Implementation notes

The tools return plain dict[str, Any] rather than the SDK Pydantic models. This is intentional and avoids inheriting the FastMCP output-validation issue described in #78: WorkItemProperty declares display_name: str and property_type: PropertyType as required, but the backend can legitimately return null in adjacent fields (description, external_source, external_id, …), which makes FastMCP's auto-generated JSON Schema reject otherwise-valid responses with Output validation error: None is not of type 'string'. By returning hand-built dicts, the new tools sidestep that path entirely.

Related: #78 (same root cause; this PR does not fix it but demonstrates a workaround that future PRs can apply more broadly).

Test plan

  • python -m py_compile and ruff check clean on the modified file.
  • Smoke-imported the package and registered the new tools through a fake MCP context.
  • Verified against a self-hosted Plane (v1.3.0) workspace:
    • list_work_item_property_values returns the correct UUIDs for Responsible and Review (RELATION/USER properties on a work item that has them set).
    • include_unset=True adds an entry for an unset OPTION property with value: null.
    • retrieve_work_item_property_value returns the expected WorkItemPropertyValueDetail payload for a property with a value, and None for a property without one (404 from the API is trapped).
  • Confirmed the tools are visible alongside the existing *_work_item_property CRUD tools after register_work_item_property_tools(mcp).

Summary by CodeRabbit

  • New Features
    • Added ability to list all custom property values for a work item, with optional support for including unset properties
    • Added ability to retrieve the value for a specific work item property
    • Enhanced property data formatting and normalization

Add `list_work_item_property_values` and `retrieve_work_item_property_value`
MCP tools that wrap the existing `client.work_item_properties.values` SDK
resource. The aggregating tool resolves the work item type internally and
returns a flat list of properties currently set on the item, including
member-type custom fields (e.g. "Responsible", "Review") that are not
exposed via `retrieve_work_item` or any `expand` value.

The tools return plain dicts so that the MCP output schema does not
inherit Pydantic models with required fields that may be null in some
backend states.

Read-only — no create/update/delete tools are added.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Warning

Rate limit exceeded

@151813125 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 25 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2953044f-2cec-4f90-80f4-fba417ce00b4

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff1e8e and 11f8200.

📒 Files selected for processing (1)
  • plane_mcp/tools/work_item_properties.py
📝 Walkthrough

Walkthrough

Two new MCP tools are added to retrieve work-item custom property values. One lists all properties for a work item with their values, handling multiple API calls and normalizing enum fields. The other retrieves a specific property's value, supporting both single and list return shapes, with a utility function to convert enum-like fields to primitives.

Changes

Cohort / File(s) Summary
Work Item Property Retrieval Tools
plane_mcp/tools/work_item_properties.py
Added list_work_item_property_values() to fetch all custom properties for a work item by retrieving type ID, listing property definitions, and fetching individual property values with error suppression. Added retrieve_work_item_property_value() to get a specific property's value(s). Added _enum_value() helper to normalize enum fields.

Sequence Diagram

sequenceDiagram
    participant MCP as MCP Tool
    participant API as Plane API
    participant Response as Response Handler

    MCP->>API: Get work item type ID
    API-->>MCP: type_id
    
    MCP->>API: List property definitions for type
    API-->>MCP: property definitions list
    
    loop For each property definition
        MCP->>API: Fetch property value (may 404)
        API-->>MCP: property value or 404
        MCP->>Response: Suppress 404, normalize result
        Response-->>MCP: normalized property data
    end
    
    MCP->>Response: Convert enums, aggregate results
    Response-->>MCP: finalized property list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop, skip, leap through properties untold,
New tools to fetch values, both new and bold!
Custom fields dancing in lists so neat,
Where enums grow humble and work-items compete.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add tools for reading work item property values' directly and accurately describes the main change: adding two new MCP-exposed tools for reading work item property values.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plane_mcp/tools/work_item_properties.py`:
- Around line 280-285: The function list_work_item_property_values currently
returns plain dict/list types; change its signature and implementation to return
the appropriate plane-sdk Pydantic model(s) (e.g., replace list[dict[str, Any]]
with list[WorkItemPropertyValue] or Optional[WorkItemPropertyValue] as
appropriate), import the WorkItemPropertyValue model from plane-sdk, and convert
returned dicts into model instances using WorkItemPropertyValue.parse_obj(...)
(or construct) before returning; make the same adjustment for other functions in
this module that currently return raw dict/list/None so all tool functions
comply with the "Tool functions must return Pydantic models from plane-sdk"
contract.
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00ae7cb8-9c2d-45d9-b55e-f72740cc8c94

📥 Commits

Reviewing files that changed from the base of the PR and between bfd0603 and 7ff1e8e.

📒 Files selected for processing (1)
  • plane_mcp/tools/work_item_properties.py

Comment thread plane_mcp/tools/work_item_properties.py
Comment thread plane_mcp/tools/work_item_properties.py Outdated
Comment on lines +347 to +350
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.

* `list_work_item_property_values` now returns
  `list[WorkItemPropertyValueDetail]` and `retrieve_work_item_property_value`
  returns `WorkItemPropertyValueDetail | list[WorkItemPropertyValueDetail]`,
  satisfying the "tool functions must return plane-sdk Pydantic models"
  contract. The aggregating tool no longer attempts to merge property
  metadata into a custom dict shape — callers that need `display_name` can
  pair the result with `list_work_item_properties(project_id, type_id)`.
* `retrieve_work_item_property_value` no longer swallows 404. The Plane API
  returns 404 both for unset values and for invalid `work_item_id` /
  `property_id`, so silently mapping it to `None` would hide real errors;
  the underlying `HttpError` is now propagated unchanged. The granular tool's
  docstring documents this and points callers at
  `list_work_item_property_values` when they need a 404-free enumeration.
* `list_work_item_property_values` keeps the 404→skip behaviour but only
  because the IDs it forwards to `values.retrieve` are guaranteed valid by
  construction (the work item was just retrieved successfully and the
  property comes from the authoritative type-bound listing). The reasoning
  is now spelled out in an inline comment so future readers do not have to
  re-derive the invariant.
* Drop the no-longer-needed `_enum_value` helper and the `include_unset`
  parameter (which had no meaning once metadata fields are no longer in the
  returned shape).
@151813125
Copy link
Copy Markdown
Author

Thanks for the review! Pushed 11f8200 addressing both findings.

1. Pydantic return types. Both tools now return plane-sdk models:

  • list_work_item_property_valueslist[WorkItemPropertyValueDetail]
  • retrieve_work_item_property_valueWorkItemPropertyValueDetail | list[WorkItemPropertyValueDetail]

Removed the dropping of property metadata into custom dict shape (along with the now-unused _enum_value helper and the include_unset parameter, which had no meaning once display_name / property_type are no longer in the returned shape). Callers that need the human-readable name can pair the result with list_work_item_properties(project_id, type_id); the docstring spells this out.

2. 404 handling.

  • retrieve_work_item_property_value — no longer swallows 404. The underlying HttpError is propagated unchanged; the docstring documents that the Plane API does not distinguish "value not set" from "invalid id" at this endpoint and points callers at list_work_item_property_values when they want a 404-free enumeration.
  • list_work_item_property_values — kept 404 → continue, but only because the IDs it forwards to values.retrieve are guaranteed valid by construction: the work item was just retrieved successfully and the property comes from the authoritative type-bound listing returned by client.work_item_properties.list(type_id). An inline comment now states that invariant.

A note on the empirical behaviour I observed on Plane self-hosted v1.3.0 (in case it's useful): when the property has no value, the API returns HTTP 404 with body {"error":"Property value not set for this work item"} — i.e. values.retrieve does not return 200 OK with a null body for unset values. That's why a strict "remove the 404 catch entirely" in the aggregating tool would mean the loop aborts on the first unset property and never reaches the rest. Happy to revisit if the upstream contract is meant to be different.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant