fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE#173
fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE#173refract99 wants to merge 2 commits into
Conversation
…osted CE list_projects, list_cycles, list_modules, and get_workspace_members call the *-lite endpoints added in 0.2.10 (projects-lite, cycles-lite, modules-lite, members-lite) for Cloud performance. Self-hosted Plane Community Edition instances don't register these routes, so all four tools 404 even though the workspace, auth, and the non-lite endpoints work fine. Add a lite_or_fallback() helper that retries via the full endpoint on a 404 and reshapes its response into the lite envelope the tool advertises, then wire it into the four affected tools. Fixes makeplane#172, makeplane#170, makeplane#169, makeplane#126 Co-Authored-By: Claude Sonnet 5 <[email protected]>
📝 WalkthroughWalkthroughAdds a shared 404 fallback utility for lite endpoints, normalizes full responses into lite models, and applies the behavior to cycle, module, project, and workspace member listing tools with unit coverage. ChangesLite listing compatibility
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ListingTool
participant lite_or_fallback
participant LiteEndpoint
participant FullEndpoint
ListingTool->>lite_or_fallback: call lite listing
lite_or_fallback->>LiteEndpoint: request lite response
LiteEndpoint-->>lite_or_fallback: 404 error
lite_or_fallback->>FullEndpoint: retry full listing
FullEndpoint-->>lite_or_fallback: full response
lite_or_fallback-->>ListingTool: normalized lite response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
plane_mcp/tools/modules.py (1)
62-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent params passing:
.model_dump(exclude_none=True)vs direct object.The full-path lambda calls
PaginatedQueryParams(...).model_dump(exclude_none=True)while the lite-path lambda passes theLiteListQueryParamsobject directly. The other tool integrations (cycles, projects) pass params objects directly toclient.*.list(). If the SDK accepts both, this is purely cosmetic; if not, one path may silently drop params. Consider aligning all four tools to use the same style.🤖 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/modules.py` around lines 62 - 73, Align parameter passing in the lite and full lambdas within the modules listing flow, using the same representation as the corresponding cycles and projects integrations. Update the full-path call in lite_or_fallback and preserve all pagination, ordering, and filtering values without introducing inconsistent serialization.tests/test_lite_fallback.py (1)
45-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding tests for
full_callraising and empty results.The suite covers lite success, both fallback shapes, and non-404 re-raise. Two gaps worth adding:
full_call()itself raisesHttpError(e.g., 500) — should propagate, not be swallowed.- Full endpoint returns empty results — should produce an empty lite response without errors.
🧪 Suggested additional tests
def test_propagates_error_from_full_call_on_404(): def lite_call(): raise HttpError("Not Found", status_code=404, response={"error": "Page not found."}) def full_call(): raise HttpError("Server Error", status_code=500, response={"error": "boom"}) with pytest.raises(HttpError) as exc_info: lite_or_fallback(lite_call, full_call, _LiteItem, _LiteResponse) assert exc_info.value.status_code == 500 def test_falls_back_to_empty_bare_list_on_404(): def lite_call(): raise HttpError("Not Found", status_code=404, response={"error": "Page not found."}) result = lite_or_fallback(lite_call, lambda: [], _LiteItem, _LiteResponse) assert isinstance(result, _LiteResponse) assert result.results == [] assert result.total_count == 0🤖 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_lite_fallback.py` around lines 45 - 118, Add tests covering both fallback edge cases in lite_or_fallback: verify an HttpError raised by full_call after a 404 from lite_call propagates with status code 500, and verify an empty full-response list produces an empty _LiteResponse with total_count 0 and no errors.plane_mcp/lite_fallback.py (1)
45-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using
model_validatefor the bare-list branch for consistency.The paginated branch (line 58) uses
lite_response_cls.model_validate(...), but the bare-list branch constructs the response directly with hardcoded field names. If a lite response model adds a required field or renames one, the direct constructor call breaks whilemodel_validatewould be more forgiving. Usingmodel_validatein both branches would also reduce coupling to specific field names.♻️ Optional refactor for consistency
if isinstance(full, list): - return lite_response_cls( + return lite_response_cls.model_validate( results=results, total_count=len(results), next_cursor="", prev_cursor="", next_page_results=False, prev_page_results=False, count=len(results), total_pages=1, total_results=len(results), )🤖 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/lite_fallback.py` around lines 45 - 56, Update the bare-list branch in the fallback response flow to build its payload and pass it through lite_response_cls.model_validate, matching the paginated branch’s validation path. Preserve the existing list-derived values and pagination defaults while avoiding direct construction tied to specific response field names.
🤖 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/projects.py`:
- Around line 57-67: Update the fallback parameter construction in list_projects
to preserve include_archived=False from ProjectLiteListQueryParams, using a
compatible params class if PaginatedQueryParams does not support it; also update
get_workspace_members so its fallback MemberQueryParams preserves cursor and
per_page from MemberListQueryParams. Apply the changes in
plane_mcp/tools/projects.py lines 57-67 and plane_mcp/tools/workspaces.py lines
56-73.
---
Nitpick comments:
In `@plane_mcp/lite_fallback.py`:
- Around line 45-56: Update the bare-list branch in the fallback response flow
to build its payload and pass it through lite_response_cls.model_validate,
matching the paginated branch’s validation path. Preserve the existing
list-derived values and pagination defaults while avoiding direct construction
tied to specific response field names.
In `@plane_mcp/tools/modules.py`:
- Around line 62-73: Align parameter passing in the lite and full lambdas within
the modules listing flow, using the same representation as the corresponding
cycles and projects integrations. Update the full-path call in lite_or_fallback
and preserve all pagination, ordering, and filtering values without introducing
inconsistent serialization.
In `@tests/test_lite_fallback.py`:
- Around line 45-118: Add tests covering both fallback edge cases in
lite_or_fallback: verify an HttpError raised by full_call after a 404 from
lite_call propagates with status code 500, and verify an empty full-response
list produces an empty _LiteResponse with total_count 0 and no errors.
🪄 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: 399b53de-4401-43cb-8da6-349ea0298f67
📒 Files selected for processing (6)
plane_mcp/lite_fallback.pyplane_mcp/tools/cycles.pyplane_mcp/tools/modules.pyplane_mcp/tools/projects.pyplane_mcp/tools/workspaces.pytests/test_lite_fallback.py
| params = ProjectLiteListQueryParams(cursor=cursor, per_page=per_page, order_by=order_by, include_archived=False) | ||
|
|
||
| return client.projects.list_lite(workspace_slug=workspace_slug, params=params) | ||
| return lite_or_fallback( | ||
| lambda: client.projects.list_lite(workspace_slug=workspace_slug, params=params), | ||
| lambda: client.projects.list( | ||
| workspace_slug=workspace_slug, | ||
| params=PaginatedQueryParams(cursor=cursor, per_page=per_page, order_by=order_by), | ||
| ), | ||
| ProjectLite, | ||
| PaginatedProjectLiteResponse, | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fallback paths drop query parameters that the lite paths include. Both the list_projects and get_workspace_members fallback lambdas construct params classes that omit filters/pagination present in the lite path, causing behavior inconsistencies for self-hosted users who trigger the 404 fallback.
plane_mcp/tools/projects.py#L57-L67: The lite path setsinclude_archived=FalseviaProjectLiteListQueryParams, but the full path usesPaginatedQueryParamswithout it. If the full/projects/endpoint defaults to including archived projects, the fallback will surface archived projects that lite users never see. Addinclude_archivedto the fallback params (or use a params class that supports it), or confirm the full endpoint defaults to excluding archived projects.plane_mcp/tools/workspaces.py#L56-L73: The lite path passescursorandper_pageviaMemberListQueryParams, but the full path'sMemberQueryParamsomits both. When the fallback triggers, the user's requested page is lost — the full endpoint may return all members or use a default page size. Addcursorandper_pageto the fallbackMemberQueryParamsif it supports them, or use a params class that does.
📍 Affects 2 files
plane_mcp/tools/projects.py#L57-L67(this comment)plane_mcp/tools/workspaces.py#L56-L73
🤖 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/projects.py` around lines 57 - 67, Update the fallback
parameter construction in list_projects to preserve include_archived=False from
ProjectLiteListQueryParams, using a compatible params class if
PaginatedQueryParams does not support it; also update get_workspace_members so
its fallback MemberQueryParams preserves cursor and per_page from
MemberListQueryParams. Apply the changes in plane_mcp/tools/projects.py lines
57-67 and plane_mcp/tools/workspaces.py lines 56-73.
- projects.py: filter archived projects client-side in the fallback path, since the full /projects/ endpoint's PaginatedQueryParams has no include_archived toggle, matching the lite path's default. - workspaces.py: document why the get_members fallback can't forward cursor/per_page (the endpoint is unpaginated by design). - lite_fallback.py: use model_validate for the bare-list branch, consistent with the paginated branch. - tests: cover full_call raising after a 404 and an empty bare-list fallback. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Plane Community Edition does not register the Cloud-only `*-lite` list endpoints; upstream makeplane#162 switched several tools to them, so on a self-hosted CE instance they return HTTP 404 ("Page not found") and the tools are unusable: list_projects, get_workspace_members, get_project_members, list_cycles, list_modules. Add plane_mcp/ce_compat.py with a lite_or_fallback() helper that prefers the Cloud endpoint but, on a 404, transparently retries the full endpoint and reshapes the response into the same paginated envelope the tool already advertises (so the output schema is unchanged). Any other error propagates. - reshape_paginated(): full paginated -> lite envelope (re-validates each row through the trimmed *Lite model; optional keep= filter mirrors the lite endpoint's include_archived=False for projects). - list_to_paginated(): bare member list -> single-page envelope. Works transparently on both Cloud (uses lite) and CE (uses full). Verified against a local CE instance: all five tools now return real data. Refs makeplane#169, makeplane#170, makeplane#172; borrows the approach of upstream PR makeplane#173 and extends it to the two members tools. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Summary
list_projects,list_cycles,list_modules, andget_workspace_memberscall the-liteendpoints added in 0.2.10 (projects-lite,cycles-lite,modules-lite,members-lite) for Cloud performance on large workspaces. Self-hosted Plane Community Edition instances don't register these routes, so all four tools returnHTTP 404: Not Found— even though the workspace, auth, and the underlying non-lite endpoints work fine.Reproduced live against a self-hosted CE instance:
retrieve_projectcorrectly reportstotal_modules: 6andtotal_members: 3, whilelist_modulesandget_workspace_membersboth 404 on the same project/workspace.This closely tracks the root cause and suggested fix already described in #172.
Approach
Added
plane_mcp/lite_fallback.py: a smalllite_or_fallback()helper that calls the-liteendpoint first, and on anHTTP 404retries against the equivalent full (non-lite) endpoint, reshaping its response (paginated envelope or bare list, depending on the endpoint) into the lite envelope the tool already advertises in its return type. Non-404 errors are re-raised unchanged.Wired into the four affected tools:
list_projects→ falls back fromprojects.list_lite()toprojects.list()list_cycles→ falls back fromcycles.list_lite()tocycles.list()list_modules→ falls back frommodules.list_lite()tomodules.list()get_workspace_members→ falls back fromworkspaces.get_members_lite()toworkspaces.get_members()This preserves the Cloud perf path (lite endpoints are tried first, so large-workspace timeouts are still avoided where the routes exist) while restoring compatibility with self-hosted CE deployments that predate them.
Test plan
tests/test_lite_fallback.py— 4 unit tests covering: lite success (full_call never invoked), 404 fallback with a paginated full response, 404 fallback with a bare-list full response, and non-404 errors re-raising unchanged.ruff check/ruff format --checkclean on all changed files.pytest tests/ --ignore=tests/test_integration.py(56 passed).list_projects,list_cycles,list_modules,get_workspace_membersall 404'd;list_labels,list_pages,list_milestones,retrieve_work_item_by_identifierwere unaffected).🤖 Generated with Claude Code
Summary by CodeRabbit