Skip to content

Conversation

@1nk1
Copy link

@1nk1 1nk1 commented Jan 28, 2026

Summary

Adds Pages/Wiki API support to Plane MCP server.

New Tools

Tool Description
`list_project_pages` List all pages in a project
`list_workspace_pages` List all workspace pages
`search_pages` Search pages by query
`update_page` Update an existing page
`delete_page` Delete a page

Technical Details

  • Uses `plane-sdk.pages` module from Plane SDK v0.2.2
  • Adds missing `list_pages` endpoints (official server only has `retrieve_page`)
  • Adds `update_page` and `delete_page` (official server only has `create_page`)
  • Adds `search_pages` tool with `workspace_level` parameter

Testing

Tested with:

  • `PLANE_API_KEY=...`
  • `PLANE_WORKSPACE_SLUG=qantum-ai`

Related

  • Enhances official `makeplane/plane-mcp-server`
  • Maintains all existing tools compatibility

Summary by CodeRabbit

Release Notes

  • New Features
    • Added pagination support for listing project and workspace pages with cursor-based navigation.
    • Added ability to update existing pages with customizable fields including name, description, access level, and color.
    • Added page deletion functionality.
    • Added page search capability with workspace and project-level options.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add list_project_pages, list_workspace_pages tools
- Add search_pages tool
- Add update_page, delete_page tools
- Use plane-sdk pages module

Adds full Pages/Wiki API support to Plane MCP server.
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

The pages tool module has been refactored to replace single-page retrieval methods with paginated list operations (list_project_pages, list_workspace_pages), and introduces new CRUD operations (update_page, delete_page, search_pages) for comprehensive page management with dynamic field updates and optional workspace-level search.

Changes

Cohort / File(s) Summary
List-based pagination
plane_mcp/tools/pages.py
Replaced retrieve_workspace_page and retrieve_project_page with list_project_pages(project_id, per_page, cursor) and list_workspace_pages(per_page, cursor) for pagination support using client list endpoints.
CRUD operations
plane_mcp/tools/pages.py
Added update_page() with dynamic payload building from optional fields (name, description_html, access, color, is_locked, parent_page, archived_at, view_props, logo_props, external_id, external_source). Added delete_page() for page deletion. Added search_pages(query, workspace_level) with conditional workspace vs. project-level search logic.
Type and return adjustments
plane_mcp/tools/pages.py
Updated imports to include Optional and standardized all method returns to JSON strings for consistency with new tool outputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Pages now list with grace and pagination's dance,
Update them, delete them, give workflows a chance!
Dynamic fields flutter like carrots in spring,
Search through the workspace—oh what joy this will bring!
hops around celebrating ✨📄

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add Pages/Wiki API support' directly matches the PR objective of adding Pages/Wiki API support with new tools for listing, updating, deleting, and searching pages.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

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

🤖 Fix all issues with AI agents
In `@plane_mcp/tools/pages.py`:
- Around line 164-191: The search_pages function currently defaults to
project-level (workspace_level=False) but returns a hardcoded error because it
lacks project_id; update the signature of search_pages to accept project_id: str
| None = None and adjust logic in search_pages (and any calls to
get_plane_client_context if needed) to: if workspace_level is True call
client.pages.list with workspace_slug and query; else if project_id is provided
call client.pages.list with project_id and query; else return a clear error
prompting the caller to supply project_id. Ensure references to the function
name search_pages and the parameter workspace_level are updated where used and
validate/handle project_id None appropriately before calling client.pages.list.
🧹 Nitpick comments (4)
plane_mcp/tools/pages.py (4)

3-8: Remove unused imports.

Optional is imported but not used (the code uses | None syntax instead). Similarly, CreatePage and Page from plane.models.pages are imported but never referenced in the code.

♻️ Proposed fix
-from typing import Any, Optional
+from typing import Any

 from fastmcp import FastMCP
 from plane_mcp.client import get_plane_client_context

-from plane.models.pages import CreatePage, Page
-

42-42: Redundant str() wrapper around model_dump_json().

model_dump_json() already returns a string, so the str() call is unnecessary. This pattern appears in all functions (lines 42, 69, 140, 162, 191).

♻️ Proposed fix (apply to all occurrences)
-        return str(result.model_dump_json())
+        return result.model_dump_json()

103-104: Docstring return type is inconsistent with actual return.

The docstring states "Returns: Updated Page object" but the function returns a JSON string. Update the docstring for accuracy.

♻️ Proposed fix
         Returns:
-            Updated Page object
+            JSON string with updated page data
         """

152-153: Docstring return type is inconsistent with actual return.

The docstring states "Returns: Success message" but the function returns result.model_dump_json(). Update the docstring to accurately describe the return value.

♻️ Proposed fix
         Returns:
-            Success message
+            JSON string with deletion result
         """

Comment on lines +164 to +191
@mcp.tool()
def search_pages(
query: str,
workspace_level: bool = False,
) -> str:
"""
Search for pages by query string.

Args:
query: Search query
workspace_level: Search workspace pages (True) or project pages (False)

Returns:
JSON string with search results
"""
client, workspace_slug = get_plane_client_context()

# Determine search endpoint based on level
if workspace_level:
result = client.pages.list(
workspace_slug=workspace_slug,
query=query,
)
else:
# For project-level, we'd need project_id
# This is a limitation - let's return error for now
return '{"error": "Project page search requires project_id. Use workspace_level=True for workspace-wide search."}'

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete implementation: project-level search defaults to error.

The workspace_level parameter defaults to False, but the project-level search path (lines 187-190) returns a hardcoded error because project_id is not accepted as a parameter. This is confusing UX since the default behavior returns an error.

Consider one of these approaches:

  1. Add project_id: str | None = None parameter to support project-level search
  2. Change the default to workspace_level: bool = True so the working path is the default
  3. Remove the workspace_level parameter entirely if project-level search isn't planned
♻️ Option 1: Add project_id parameter
     `@mcp.tool`()
     def search_pages(
         query: str,
+        project_id: str | None = None,
         workspace_level: bool = False,
     ) -> str:
         """
         Search for pages by query string.
         
         Args:
             query: Search query
+            project_id: UUID of the project (required when workspace_level=False)
             workspace_level: Search workspace pages (True) or project pages (False)
         
         Returns:
             JSON string with search results
         """
         client, workspace_slug = get_plane_client_context()
         
         # Determine search endpoint based on level
         if workspace_level:
             result = client.pages.list(
                 workspace_slug=workspace_slug,
                 query=query,
             )
+        elif project_id:
+            result = client.projects.pages.list(
+                workspace_slug=workspace_slug,
+                project_id=project_id,
+                query=query,
+            )
         else:
-            # For project-level, we'd need project_id
-            # This is a limitation - let's return error for now
             return '{"error": "Project page search requires project_id. Use workspace_level=True for workspace-wide search."}'
         
         return result.model_dump_json()
♻️ Option 2: Default to workspace-level search
     def search_pages(
         query: str,
-        workspace_level: bool = False,
+        workspace_level: bool = True,
     ) -> str:
📝 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.

Suggested change
@mcp.tool()
def search_pages(
query: str,
workspace_level: bool = False,
) -> str:
"""
Search for pages by query string.
Args:
query: Search query
workspace_level: Search workspace pages (True) or project pages (False)
Returns:
JSON string with search results
"""
client, workspace_slug = get_plane_client_context()
# Determine search endpoint based on level
if workspace_level:
result = client.pages.list(
workspace_slug=workspace_slug,
query=query,
)
else:
# For project-level, we'd need project_id
# This is a limitation - let's return error for now
return '{"error": "Project page search requires project_id. Use workspace_level=True for workspace-wide search."}'
`@mcp.tool`()
def search_pages(
query: str,
workspace_level: bool = True,
) -> str:
"""
Search for pages by query string.
Args:
query: Search query
workspace_level: Search workspace pages (True) or project pages (False)
Returns:
JSON string with search results
"""
client, workspace_slug = get_plane_client_context()
# Determine search endpoint based on level
if workspace_level:
result = client.pages.list(
workspace_slug=workspace_slug,
query=query,
)
else:
# For project-level, we'd need project_id
# This is a limitation - let's return error for now
return '{"error": "Project page search requires project_id. Use workspace_level=True for workspace-wide search."}'
🤖 Prompt for AI Agents
In `@plane_mcp/tools/pages.py` around lines 164 - 191, The search_pages function
currently defaults to project-level (workspace_level=False) but returns a
hardcoded error because it lacks project_id; update the signature of
search_pages to accept project_id: str | None = None and adjust logic in
search_pages (and any calls to get_plane_client_context if needed) to: if
workspace_level is True call client.pages.list with workspace_slug and query;
else if project_id is provided call client.pages.list with project_id and query;
else return a clear error prompting the caller to supply project_id. Ensure
references to the function name search_pages and the parameter workspace_level
are updated where used and validate/handle project_id None appropriately before
calling client.pages.list.

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