Skip to content

fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE#173

Open
refract99 wants to merge 2 commits into
makeplane:mainfrom
refract99:fix/lite-endpoint-404-on-self-hosted-ce
Open

fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE#173
refract99 wants to merge 2 commits into
makeplane:mainfrom
refract99:fix/lite-endpoint-404-on-self-hosted-ce

Conversation

@refract99

@refract99 refract99 commented Jul 11, 2026

Copy link
Copy Markdown

Summary

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 on large workspaces. Self-hosted Plane Community Edition instances don't register these routes, so all four tools return HTTP 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_project correctly reports total_modules: 6 and total_members: 3, while list_modules and get_workspace_members both 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 small lite_or_fallback() helper that calls the -lite endpoint first, and on an HTTP 404 retries 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 from projects.list_lite() to projects.list()
  • list_cycles → falls back from cycles.list_lite() to cycles.list()
  • list_modules → falls back from modules.list_lite() to modules.list()
  • get_workspace_members → falls back from workspaces.get_members_lite() to workspaces.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

  • Added 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 --check clean on all changed files.
  • Full non-integration suite passes: pytest tests/ --ignore=tests/test_integration.py (56 passed).
  • Reproduced the original 404s live against a self-hosted CE workspace before the fix (list_projects, list_cycles, list_modules, get_workspace_members all 404'd; list_labels, list_pages, list_milestones, retrieve_work_item_by_identifier were unaffected).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a lightweight-first listing strategy for cycles, modules, projects, and workspace members, automatically retrying with full responses when lightweight endpoints aren’t available.
    • Normalized returned data so item and pagination details stay consistent across lite and full paths.
  • Bug Fixes
    • Ensures only “not found” cases trigger fallback; other errors are still surfaced.
    • Correctly handles full responses returned as raw lists, including empty results.
  • Tests
    • Expanded automated coverage for successful lite responses, fallback behavior, pagination mapping, and error propagation.

…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]>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Lite listing compatibility

Layer / File(s) Summary
Fallback helper and validation
plane_mcp/lite_fallback.py, tests/test_lite_fallback.py
lite_or_fallback retries full calls only for HTTP 404, converts paginated or bare-list responses into lite models, and tests success, fallback, and error propagation paths.
Listing tool integrations
plane_mcp/tools/cycles.py, plane_mcp/tools/modules.py, plane_mcp/tools/projects.py, plane_mcp/tools/workspaces.py
Cycle, module, project, and workspace member listings attempt lite endpoints first and use full endpoint query parameters and response models on 404. get_project_members is reformatted without changing its call.

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
Loading

Possibly related PRs

Suggested reviewers: Prashant-Surya

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses the reported self-hosted 404s by retrying full endpoints and preserving the expected lite response shape.
Out of Scope Changes check ✅ Passed The changes stay focused on the lite-to-full fallback behavior and supporting tests/helpers, with no clear unrelated additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: retrying full list endpoints when lite routes return 404 on self-hosted CE.
✨ 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.

@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: 1

🧹 Nitpick comments (3)
plane_mcp/tools/modules.py (1)

62-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent 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 the LiteListQueryParams object directly. The other tool integrations (cycles, projects) pass params objects directly to client.*.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 win

Consider adding tests for full_call raising and empty results.

The suite covers lite success, both fallback shapes, and non-404 re-raise. Two gaps worth adding:

  1. full_call() itself raises HttpError (e.g., 500) — should propagate, not be swallowed.
  2. 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 value

Consider using model_validate for 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 while model_validate would be more forgiving. Using model_validate in 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

📥 Commits

Reviewing files that changed from the base of the PR and between b0fb136 and 12a9bb7.

📒 Files selected for processing (6)
  • plane_mcp/lite_fallback.py
  • plane_mcp/tools/cycles.py
  • plane_mcp/tools/modules.py
  • plane_mcp/tools/projects.py
  • plane_mcp/tools/workspaces.py
  • tests/test_lite_fallback.py

Comment on lines +57 to +67
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,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 sets include_archived=False via ProjectLiteListQueryParams, but the full path uses PaginatedQueryParams without it. If the full /projects/ endpoint defaults to including archived projects, the fallback will surface archived projects that lite users never see. Add include_archived to 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 passes cursor and per_page via MemberListQueryParams, but the full path's MemberQueryParams omits 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. Add cursor and per_page to the fallback MemberQueryParams if 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]>
tancou added a commit to tancou/plane-mcp-server-ce that referenced this pull request Jul 17, 2026
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant