Skip to content

feat: replace work item relations with dedicated dependency and custom relation tools#148

Open
akhil-vamshi-konam wants to merge 13 commits into
mainfrom
chore-custom-relations
Open

feat: replace work item relations with dedicated dependency and custom relation tools#148
akhil-vamshi-konam wants to merge 13 commits into
mainfrom
chore-custom-relations

Conversation

@akhil-vamshi-konam

@akhil-vamshi-konam akhil-vamshi-konam commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Description

Introduces dedicated work item relation modules and improves structured logging for observability while tightening PII handling.

Work item relations

  • Replaces the legacy work_item_relations tools with dedicated modules:

    • work_item_relation_definitions (CRUD for workspace relation definitions)
    • work_item_dependencies (manage the six built-in dependency types)
    • work_item_custom_relations (manage definition-based custom relations)
  • Removes the legacy work_item_relations.py implementation backed by the deprecated /relations/ endpoint.

Logging improvements

  • Added PlaneLoggingMiddleware to include the tool name in tools/call log entries.
  • Added workspace_slug to all log records for workspace-level analytics.
  • Added LOG_USER_INFO (default: false) to optionally include user display names in logs while keeping PII disabled by default.
  • stdio logs only workspace_slug; LOG_USER_INFO is a no-op for stdio.

Type of Change

  • Feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Documentation update

Sample Logs

HTTP with user info (LOG_USER_INFO=truedisplay_name included)

{"timestamp": "2026-06-17T10:34:44.498814+00:00", "level": "INFO", "logger": "fastmcp.plane_mcp.auth.plane_oauth_provider", "message": "User verified: (4161e0f8-48a2-49b4-a43a-458465337135) - akhil.vamshi"}

{"timestamp": "2026-06-17T10:34:44.532392+00:00", "level": "INFO", "logger": "fastmcp.middleware.structured_logging", "event": "request_start", "method": "tools/call", "source": "client", "payload": "{\"task\":null,\"_meta\":null,\"name\":\"list_projects\",\"arguments\":{}}", "payload_type": "CallToolRequestParams", "user_id": "4161e0f8-48a2-49b4-a43a-458465337135", "workspace_slug": "bronze", "display_name": "akhil.vamshi"}

{"timestamp": "2026-06-17T10:34:44.630735+00:00", "level": "INFO", "logger": "fastmcp.middleware.structured_logging", "event": "request_success", "method": "tools/call", "source": "client", "duration_ms": 98.26, "tool": "list_projects", "user_id": "4161e0f8-48a2-49b4-a43a-458465337135", "workspace_slug": "bronze", "display_name": "akhil.vamshi"}

HTTP without user info (default → no display_name, user_id only)

{"timestamp": "2026-06-17T11:55:51.043796+00:00", "level": "INFO", "logger": "fastmcp.plane_mcp.auth.plane_oauth_provider", "message": "User verified: (4161e0f8-48a2-49b4-a43a-458465337135)"}

{"timestamp": "2026-06-17T11:55:51.084374+00:00", "level": "INFO", "logger": "fastmcp.middleware.structured_logging", "event": "request_start", "method": "tools/call", "source": "client", "payload": "{\"task\":null,\"_meta\":null,\"name\":\"list_projects\",\"arguments\":{}}", "payload_type": "CallToolRequestParams", "user_id": "4161e0f8-48a2-49b4-a43a-458465337135", "workspace_slug": "bronze"}

{"timestamp": "2026-06-17T11:55:51.175255+00:00", "level": "INFO", "logger": "fastmcp.middleware.structured_logging", "event": "request_success", "method": "tools/call", "source": "client", "duration_ms": 90.76, "tool": "list_projects", "user_id": "4161e0f8-48a2-49b4-a43a-458465337135", "workspace_slug": "bronze"}

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new work_item_relation_definitions.py module exposing four MCP tools for workspace relation definition CRUD. Rewrites work_item_relations.py to split list/create/remove across built-in dependency and custom relation backends. Simplifies SERVER_INSTRUCTIONS to epics-only, updates work_items docstrings, and extends integration tests and README accordingly.

Changes

Work Item Relation Definitions and Dual-Mode Relation Tools

Layer / File(s) Summary
Tool registry wiring
plane_mcp/tools/__init__.py
Imports register_work_item_relation_definition_tools and calls it inside register_tools() alongside the existing relation tools registration.
Work item relation definition CRUD tools
plane_mcp/tools/work_item_relation_definitions.py, tests/test_integration.py, README.md
New module exposes four MCP tools: paginated list with optional is_default/is_active filters returning both built_in_dependencies and custom_definitions; create, update, and delete workspace relation definitions via client.work_item_relation_definitions. Integration tests add the four tool names to EXPECTED_TOOLS; README adds a "Work Item Relation Definitions" subsection.
Dual-mode work item relation tools rewrite
plane_mcp/tools/work_item_relations.py
list_work_item_relations now returns {"dependencies": ..., "custom": ...} from separate client calls. create_work_item_relation supports two mutually exclusive modes: relation_type (validated dependency) or relation_definition_id+relation_definition_label (custom), routing to the respective client endpoint and raising ValueError otherwise. remove_work_item_relation accepts related_work_item_id and is_dependency to choose the deletion endpoint.
SERVER_INSTRUCTIONS simplification, docstring updates, and dependency bump
plane_mcp/instructions.py, plane_mcp/tools/work_items.py, pyproject.toml
SERVER_INSTRUCTIONS is rewritten as a single epics-only inline string, removing prior instruction constants. The count_work_items and search_work_items docstrings are corrected for accuracy. The plane-sdk dependency is bumped from 0.2.16 to 0.2.17.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • makeplane/plane-mcp-server#150: Modifies plane_mcp/instructions.py by rewriting SERVER_INSTRUCTIONS to a single consolidated "Epics"-only instruction string, directly overlapping with this PR's changes to the same variable.
  • makeplane/plane-mcp-server#127: Touches the create_work_item_relation tool in plane_mcp/tools/work_item_relations.py, which this PR rewrites with a dual-mode routing implementation.

Suggested reviewers

  • Prashant-Surya
  • Saurabhkmr98

Poem

🐇 Hop, hop, relations defined!
Custom and built-in, neatly aligned,
Two modes for creating, one flag to remove,
Instructions trimmed down, epics approve.
The rabbit checks tools — all four are there,
A tidy PR handled with care! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing the legacy work_item_relations system with dedicated tools for dependencies and custom relations.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore-custom-relations

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.

@akhil-vamshi-konam akhil-vamshi-konam marked this pull request as ready for review June 15, 2026 06:38
@akhil-vamshi-konam akhil-vamshi-konam marked this pull request as draft June 16, 2026 03:41
@akhil-vamshi-konam akhil-vamshi-konam marked this pull request as ready for review June 16, 2026 06:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plane_mcp/tools/work_item_relation_definitions.py (1)

138-142: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing Returns sections to tool docstrings.

Both modified tool docstrings include Args but omit Returns, which violates the tool docstring standard.

  • plane_mcp/tools/work_item_relation_definitions.py#L138-L142: add a Returns section (even if it is explicit None/ack behavior).
  • plane_mcp/tools/work_item_relations.py#L136-L150: add a Returns section documenting the remove tool’s output contract.
    As per coding guidelines, “Tool docstrings must include Args and Returns sections.”
🤖 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/work_item_relation_definitions.py` around lines 138 - 142,
Add missing `Returns` sections to both tool docstrings to comply with the tool
docstring standard. In `plane_mcp/tools/work_item_relation_definitions.py` lines
138-142, add a `Returns` section to the docstring for the delete method
documenting what it returns (e.g., None or acknowledgment behavior). In
`plane_mcp/tools/work_item_relations.py` lines 136-150, add a `Returns` section
to the docstring for the remove tool documenting its output contract. Both
sections should clearly describe the return value or behavior to ensure complete
documentation per coding guidelines.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@plane_mcp/tools/work_item_relations.py`:
- Around line 96-127: The function currently silently chooses dependency
creation if relation_type is provided, even if custom relation fields
(relation_definition_id and relation_definition_label) are also passed. Since
these modes are mutually exclusive per the API contract, add early validation
before the existing if statements to check if both relation_type and the custom
relation fields are provided together. If this ambiguous combination is
detected, raise a ValueError explaining that these parameters are mutually
exclusive and the caller must choose only one mode.

In `@tests/test_integration.py`:
- Around line 193-200: The extract_result call for the epics_result is
incorrectly assuming the payload is wrapped in a dict with a "results" key, but
list_work_items returns the item list directly. Remove the ["results"] indexing
from the epics assignment where extract_result(epics_result) is called, so that
epics is assigned the extracted list directly rather than attempting to access a
non-existent "results" key within it.
- Around line 142-158: Before creating a new workspace-level Epic type via the
create_work_item_type call, first check if an Epic type already exists in the
workspace_types list that was just retrieved. After extracting the
workspace_types from the result, search through that list for an existing type
with the name "Epic". If found, use that existing type for epic_type instead of
creating a duplicate. Only call create_work_item_type if no Epic type already
exists in the workspace_types. This prevents duplicate-name errors and avoids
unnecessary type creation when the type is already available.

---

Outside diff comments:
In `@plane_mcp/tools/work_item_relation_definitions.py`:
- Around line 138-142: Add missing `Returns` sections to both tool docstrings to
comply with the tool docstring standard. In
`plane_mcp/tools/work_item_relation_definitions.py` lines 138-142, add a
`Returns` section to the docstring for the delete method documenting what it
returns (e.g., None or acknowledgment behavior). In
`plane_mcp/tools/work_item_relations.py` lines 136-150, add a `Returns` section
to the docstring for the remove tool documenting its output contract. Both
sections should clearly describe the return value or behavior to ensure complete
documentation per coding guidelines.
🪄 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: f03418e5-4715-4273-8d89-b804cb6dc997

📥 Commits

Reviewing files that changed from the base of the PR and between 3abec05 and bd920fd.

📒 Files selected for processing (9)
  • README.md
  • plane_mcp/instructions.py
  • plane_mcp/tools/__init__.py
  • plane_mcp/tools/initiatives.py
  • plane_mcp/tools/work_item_relation_definitions.py
  • plane_mcp/tools/work_item_relations.py
  • plane_mcp/tools/work_item_types.py
  • plane_mcp/tools/work_items.py
  • tests/test_integration.py
✅ Files skipped from review due to trivial changes (2)
  • plane_mcp/tools/work_item_types.py
  • README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plane_mcp/tools/work_item_relation_definitions.py (1)

138-142: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing Returns sections to tool docstrings.

Both modified tool docstrings include Args but omit Returns, which violates the tool docstring standard.

  • plane_mcp/tools/work_item_relation_definitions.py#L138-L142: add a Returns section (even if it is explicit None/ack behavior).
  • plane_mcp/tools/work_item_relations.py#L136-L150: add a Returns section documenting the remove tool’s output contract.
    As per coding guidelines, “Tool docstrings must include Args and Returns sections.”
🤖 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/work_item_relation_definitions.py` around lines 138 - 142,
Add missing `Returns` sections to both tool docstrings to comply with the tool
docstring standard. In `plane_mcp/tools/work_item_relation_definitions.py` lines
138-142, add a `Returns` section to the docstring for the delete method
documenting what it returns (e.g., None or acknowledgment behavior). In
`plane_mcp/tools/work_item_relations.py` lines 136-150, add a `Returns` section
to the docstring for the remove tool documenting its output contract. Both
sections should clearly describe the return value or behavior to ensure complete
documentation per coding guidelines.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@plane_mcp/tools/work_item_relations.py`:
- Around line 96-127: The function currently silently chooses dependency
creation if relation_type is provided, even if custom relation fields
(relation_definition_id and relation_definition_label) are also passed. Since
these modes are mutually exclusive per the API contract, add early validation
before the existing if statements to check if both relation_type and the custom
relation fields are provided together. If this ambiguous combination is
detected, raise a ValueError explaining that these parameters are mutually
exclusive and the caller must choose only one mode.

In `@tests/test_integration.py`:
- Around line 193-200: The extract_result call for the epics_result is
incorrectly assuming the payload is wrapped in a dict with a "results" key, but
list_work_items returns the item list directly. Remove the ["results"] indexing
from the epics assignment where extract_result(epics_result) is called, so that
epics is assigned the extracted list directly rather than attempting to access a
non-existent "results" key within it.
- Around line 142-158: Before creating a new workspace-level Epic type via the
create_work_item_type call, first check if an Epic type already exists in the
workspace_types list that was just retrieved. After extracting the
workspace_types from the result, search through that list for an existing type
with the name "Epic". If found, use that existing type for epic_type instead of
creating a duplicate. Only call create_work_item_type if no Epic type already
exists in the workspace_types. This prevents duplicate-name errors and avoids
unnecessary type creation when the type is already available.

---

Outside diff comments:
In `@plane_mcp/tools/work_item_relation_definitions.py`:
- Around line 138-142: Add missing `Returns` sections to both tool docstrings to
comply with the tool docstring standard. In
`plane_mcp/tools/work_item_relation_definitions.py` lines 138-142, add a
`Returns` section to the docstring for the delete method documenting what it
returns (e.g., None or acknowledgment behavior). In
`plane_mcp/tools/work_item_relations.py` lines 136-150, add a `Returns` section
to the docstring for the remove tool documenting its output contract. Both
sections should clearly describe the return value or behavior to ensure complete
documentation per coding guidelines.
🪄 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: f03418e5-4715-4273-8d89-b804cb6dc997

📥 Commits

Reviewing files that changed from the base of the PR and between 3abec05 and bd920fd.

📒 Files selected for processing (9)
  • README.md
  • plane_mcp/instructions.py
  • plane_mcp/tools/__init__.py
  • plane_mcp/tools/initiatives.py
  • plane_mcp/tools/work_item_relation_definitions.py
  • plane_mcp/tools/work_item_relations.py
  • plane_mcp/tools/work_item_types.py
  • plane_mcp/tools/work_items.py
  • tests/test_integration.py
✅ Files skipped from review due to trivial changes (2)
  • plane_mcp/tools/work_item_types.py
  • README.md
🛑 Comments failed to post (3)
plane_mcp/tools/work_item_relations.py (1)

96-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject ambiguous create mode inputs.

If callers pass relation_type and custom relation fields together, dependency creation is chosen silently. This should fail fast because the API contract is mutually exclusive.

Suggested fix
         client, workspace_slug = get_plane_client_context()
+        if relation_type and (relation_definition_id or relation_definition_label):
+            raise ValueError(
+                "Use either relation_type (dependency) or relation_definition_id + "
+                "relation_definition_label (custom), not both."
+            )
         if relation_type:
             if relation_type not in _DEPENDENCY_TYPES:
                 raise ValueError(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if relation_type and (relation_definition_id or relation_definition_label):
            raise ValueError(
                "Use either relation_type (dependency) or relation_definition_id + "
                "relation_definition_label (custom), not both."
            )
        if relation_type:
            if relation_type not in _DEPENDENCY_TYPES:
                raise ValueError(
                    f"relation_type must be one of {list(_DEPENDENCY_TYPES)}. For any "
                    "other relationship, pass relation_definition_id + "
                    "relation_definition_label from list_work_item_relation_definitions."
                )
            return client.work_items.dependencies.create(
                workspace_slug=workspace_slug,
                project_id=project_id,
                work_item_id=work_item_id,
                data=CreateWorkItemDependency(
                    relation_type=relation_type,  # type: ignore[arg-type]
                    work_item_ids=work_item_ids,
                ),
            )
        if relation_definition_id and relation_definition_label:
            return client.work_items.custom_relations.create(
                workspace_slug=workspace_slug,
                project_id=project_id,
                work_item_id=work_item_id,
                data=CreateWorkItemCustomRelation(
                    relation_definition_id=relation_definition_id,
                    relation_definition_type=relation_definition_label,
                    work_item_ids=work_item_ids,
                ),
            )
        raise ValueError(
            "Provide relation_type for a built-in dependency, or "
            "relation_definition_id + relation_definition_label for a custom "
            "relation (call list_work_item_relation_definitions to find one)."
        )
🤖 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/work_item_relations.py` around lines 96 - 127, The function
currently silently chooses dependency creation if relation_type is provided,
even if custom relation fields (relation_definition_id and
relation_definition_label) are also passed. Since these modes are mutually
exclusive per the API contract, add early validation before the existing if
statements to check if both relation_type and the custom relation fields are
provided together. If this ambiguous combination is detected, raise a ValueError
explaining that these parameters are mutually exclusive and the caller must
choose only one mode.
tests/test_integration.py (2)

142-158: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid duplicate “Epic” type creation in workspace scope.

The fallback path creates a new workspace-level Epic whenever workspace_types is non-empty, without first checking whether Epic already exists there. That can trigger duplicate-name errors or unnecessary type churn.

Suggested fix
-            if workspace_types:
-                new_type_result = await client.call_tool("create_work_item_type", {"name": "Epic"})
-                epic_type = extract_result(new_type_result)
-                created_workspace_epic_type = True
-                await client.call_tool(
-                    "import_work_item_types_to_project",
-                    {"project_id": project_id, "work_item_type_ids": [epic_type["id"]]},
-                )
+            if workspace_types:
+                epic_type = next((t for t in workspace_types if t.get("name", "").lower() == "epic"), None)
+                if epic_type is None:
+                    new_type_result = await client.call_tool("create_work_item_type", {"name": "Epic"})
+                    epic_type = extract_result(new_type_result)
+                    created_workspace_epic_type = True
+                await client.call_tool(
+                    "import_work_item_types_to_project",
+                    {"project_id": project_id, "work_item_type_ids": [epic_type["id"]]},
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if epic_type is None:
            workspace_types_result = await client.call_tool("list_work_item_types", {})
            workspace_types = extract_result(workspace_types_result)
            if workspace_types:
                epic_type = next((t for t in workspace_types if t.get("name", "").lower() == "epic"), None)
                if epic_type is None:
                    new_type_result = await client.call_tool("create_work_item_type", {"name": "Epic"})
                    epic_type = extract_result(new_type_result)
                    created_workspace_epic_type = True
                await client.call_tool(
                    "import_work_item_types_to_project",
                    {"project_id": project_id, "work_item_type_ids": [epic_type["id"]]},
                )
            else:
                new_type_result = await client.call_tool(
                    "create_work_item_type", {"name": "Epic", "project_id": project_id}
                )
                epic_type = extract_result(new_type_result)
🤖 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 `@tests/test_integration.py` around lines 142 - 158, Before creating a new
workspace-level Epic type via the create_work_item_type call, first check if an
Epic type already exists in the workspace_types list that was just retrieved.
After extracting the workspace_types from the result, search through that list
for an existing type with the name "Epic". If found, use that existing type for
epic_type instead of creating a duplicate. Only call create_work_item_type if no
Epic type already exists in the workspace_types. This prevents duplicate-name
errors and avoids unnecessary type creation when the type is already available.

193-200: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use list_work_items result as a list, not ["results"].

Line 199 assumes a wrapped payload shape and can fail with TypeError/KeyError when the tool returns the list directly.
Based on learnings, list endpoints should return only response.results (the item list), not pagination metadata wrappers.

Suggested fix
-        epics = extract_result(epics_result)["results"]
+        epics = extract_result(epics_result)
🤖 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 `@tests/test_integration.py` around lines 193 - 200, The extract_result call
for the epics_result is incorrectly assuming the payload is wrapped in a dict
with a "results" key, but list_work_items returns the item list directly. Remove
the ["results"] indexing from the epics assignment where
extract_result(epics_result) is called, so that epics is assigned the extracted
list directly rather than attempting to access a non-existent "results" key
within it.

Source: Learnings

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