Skip to content

feat: add PLANE_TOOLS env var to filter tool groups#57

Closed
CaliLuke wants to merge 2 commits intomakeplane:canaryfrom
CaliLuke:tool-group-filtering
Closed

feat: add PLANE_TOOLS env var to filter tool groups#57
CaliLuke wants to merge 2 commits intomakeplane:canaryfrom
CaliLuke:tool-group-filtering

Conversation

@CaliLuke
Copy link
Copy Markdown

@CaliLuke CaliLuke commented Jan 5, 2026

Summary

Adds environment variable-based filtering to reduce MCP tool token usage by enabling only needed tool groups.

Usage

Inclusion mode - only register specified groups:

PLANE_TOOLS=projects,work_items,users

Exclusion mode - register all except specified groups:

PLANE_TOOLS=!cycles,!modules,!initiatives

Default (no env var): All tools registered (backwards compatible)

Available tool groups

  • projects (9 tools)
  • work_items (6 tools)
  • cycles (12 tools)
  • users (1 tool)
  • modules (11 tools)
  • initiatives (5 tools)
  • intake (5 tools)
  • work_item_properties (5 tools)

Use case

LLM-based MCP clients have limited context windows. Each tool definition consumes tokens (~600-1500 per tool). Users who don't need all features can exclude unused tool groups to free up context for actual conversation.

Example: Excluding cycles, modules, and initiatives reduces tool count from 55 to 27, saving ~18k tokens.


Note

Introduces environment-controlled tool registration to reduce loaded MCP tools.

  • New PLANE_TOOLS env var supports inclusion (projects,work_items,...), exclusion (!cycles,!modules,...), and default (all) modes
  • Refactors register_tools to register only enabled groups; iterates TOOL_GROUPS and applies selection logic
  • Adds immutable TOOL_GROUPS mapping for group→registration function
  • Emits warnings for unknown groups and unprefixed items in exclusion mode

Written by Cursor Bugbot for commit a1bfdc0. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Environment-driven tool registration via PLANE_TOOLS (enable all, include list, or prefix with "!" to exclude); warnings for invalid entries
    • New state tools available for listing project states
  • Refactor

    • Tool registration flow centralized and made immutable
    • Public tool outputs simplified to slimmed summary/full representations (lighter, HTML-stripped descriptions)
  • Documentation

    • Added comprehensive MCP tool design guide (SKILL.md)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

Adds an environment-driven tool registration system (immutable TOOL_GROUPS + register_tools using PLANE_TOOLS) and introduces new MCP models, summaries, state tooling, converted project/work-item tool outputs to slim summaries, a new MCP tool design guide (SKILL.md), minor config and .gitignore updates, and dependency bumps.

Changes

Cohort / File(s) Summary
Tool group registration
plane_mcp/tools/__init__.py
Add immutable _TOOL_GROUPS backing store and public TOOL_GROUPS (MappingProxyType); refactor register_tools() to read PLANE_TOOLS (include/exclude/default), validate entries, log warnings, and invoke registration functions dynamically.
MCP tool design doc
.claude/skills/mcp-tool-design/SKILL.md
Add comprehensive MCP tool design guide covering naming, params, response models, registration patterns, logging, error handling, and checklist for adding tools.
Models (new public DTOs & util)
plane_mcp/models.py
Add slim response models (ProjectSummary, WorkItemSummary, WorkItemFull, AssigneeSummary, LabelSummary, StateSummary), _SlimBase, HTML stripping utility strip_html, and slim() serialization helpers.
Project tools (API shape changes)
plane_mcp/tools/projects.py
Switch public project tool return types to ProjectSummary/slim representations; add internal helper _to_project_summary.
Work-item tools (API shape changes)
plane_mcp/tools/work_items.py
Change list/create/update/retrieve returns to WorkItemSummary/WorkItemFull; add conversion helpers (_to_work_item_summary, _to_work_item_full, _enum_str) and use strip_html for descriptions.
State tools (new)
plane_mcp/tools/states.py
New module registering register_state_tools(mcp) and list_states tool that returns StateSummary slim representations.
Config & packaging
pyproject.toml
Bump fastmcp version and adjust py-key-value-aio[redis] constraint; add VCS source for fastmcp.
Repository ignores
.gitignore
Add .mcp.json, token-use.md, and .claude/settings.local.json entries.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Env as Environment (PLANE_TOOLS)
  participant Registrar as register_tools()
  participant Registry as TOOL_GROUPS
  participant ToolFn as Tool registration fn

  Env->>Registrar: read PLANE_TOOLS
  Registrar->>Registry: evaluate entries (include / exclude / default)
  alt inclusion mode
    Registrar->>Registry: select listed groups
  else exclusion mode
    Registrar->>Registry: select all minus excluded
  else default
    Registrar->>Registry: select all groups
  end
  loop for each selected group
    Registrar->>ToolFn: call registration function
    ToolFn-->>Registrar: registration complete / log
  end
  Registrar-->>Env: finished
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • sriramveeraghanta

Poem

🐰 I hopped through code and env tonight,

Mapped the tools by morning light,
Include or skip with just one line,
Slimmed the models, tidy and fine,
A rabbit’s hop — systems set right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly summarizes the main feature: adding the PLANE_TOOLS environment variable for filtering tool groups, which aligns perfectly with the primary changeset objective.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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: 0

🧹 Nitpick comments (2)
plane_mcp/tools/__init__.py (2)

17-27: Consider making TOOL_GROUPS immutable.

Since TOOL_GROUPS is a new public export, consider making it immutable to prevent unintended modifications by external code. This ensures the tool registry remains stable.

🔎 Proposed fix using MappingProxyType
+from types import MappingProxyType
+
 from fastmcp import FastMCP

 from plane_mcp.tools.cycles import register_cycle_tools
 # Map of tool group names to their registration functions
-TOOL_GROUPS: dict[str, Callable[[FastMCP], None]] = {
+_TOOL_GROUPS: dict[str, Callable[[FastMCP], None]] = {
     "projects": register_project_tools,
     "work_items": register_work_item_tools,
     "cycles": register_cycle_tools,
     "users": register_user_tools,
     "modules": register_module_tools,
     "initiatives": register_initiative_tools,
     "intake": register_intake_tools,
     "work_item_properties": register_work_item_property_tools,
 }
+
+TOOL_GROUPS = MappingProxyType(_TOOL_GROUPS)

52-58: Consider validating group names to catch typos.

Invalid group names in PLANE_TOOLS are silently ignored, which could lead to confusion if users misspell a group name. Consider adding validation to warn users about unrecognized groups.

🔎 Proposed enhancement with validation
 def register_tools(mcp: FastMCP) -> None:
     """Register tools with the MCP server based on PLANE_TOOLS configuration.
 
     The PLANE_TOOLS environment variable controls which tool groups are registered:
     - Not set or empty: Register all tools (default)
     - Comma-separated list: Register only specified groups
       Example: PLANE_TOOLS=projects,work_items,users
     - Exclusion mode (prefix with !): Register all except specified groups
       Example: PLANE_TOOLS=!cycles,!modules,!initiatives
 
     Available tool groups: projects, work_items, cycles, users, modules,
     initiatives, intake, work_item_properties
     """
     tools_config = os.getenv("PLANE_TOOLS", "").strip()
 
     if not tools_config:
         # Default: register all tools
         enabled_groups = set(TOOL_GROUPS.keys())
     elif tools_config.startswith("!"):
         # Exclusion mode: start with all, remove specified groups
         excluded = {t.strip().lstrip("!") for t in tools_config.split(",")}
+        # Validate excluded group names
+        invalid = excluded - set(TOOL_GROUPS.keys()) - {""}
+        if invalid:
+            import logging
+            logging.warning(f"Unknown tool groups in PLANE_TOOLS will be ignored: {', '.join(invalid)}")
         enabled_groups = set(TOOL_GROUPS.keys()) - excluded
     else:
         # Inclusion mode: only register specified groups
         enabled_groups = {t.strip() for t in tools_config.split(",")}
+        # Validate included group names
+        invalid = enabled_groups - set(TOOL_GROUPS.keys()) - {""}
+        if invalid:
+            import logging
+            logging.warning(f"Unknown tool groups in PLANE_TOOLS will be ignored: {', '.join(invalid)}")
 
     for name, register_fn in TOOL_GROUPS.items():
         if name in enabled_groups:
             register_fn(mcp)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 754faa9 and 9b19501.

📒 Files selected for processing (1)
  • plane_mcp/tools/__init__.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (2)
plane_mcp/tools/__init__.py (2)

3-4: LGTM!

The imports are correctly added to support environment variable access and type hints for the new filtering logic.


30-58: Well-structured implementation!

The environment variable parsing logic correctly handles all documented use cases:

  • Default mode (empty) registers all tools
  • Inclusion mode registers only specified groups
  • Exclusion mode registers all except specified groups

The logic properly handles whitespace, trailing commas, and empty strings. The docstring provides clear examples for users.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Comment thread plane_mcp/tools/__init__.py Outdated
@CaliLuke CaliLuke force-pushed the tool-group-filtering branch from 9b19501 to 12eafa4 Compare January 5, 2026 21:45
Allows users to reduce context usage by enabling only needed tool groups.

Usage:
- PLANE_TOOLS=projects,work_items,users (inclusion mode)
- PLANE_TOOLS=!cycles,!modules,!initiatives (exclusion mode)

Available groups: projects, work_items, cycles, users, modules,
initiatives, intake, work_item_properties

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@CaliLuke CaliLuke force-pushed the tool-group-filtering branch from 12eafa4 to a1bfdc0 Compare January 5, 2026 21:48
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: 0

🧹 Nitpick comments (2)
plane_mcp/tools/__init__.py (2)

19-30: LGTM!

The pattern of using a private _TOOL_GROUPS dict with a public immutable TOOL_GROUPS proxy is excellent for preventing accidental modifications. The mapping is clean and well-structured.

Optional: Add explicit type annotation for clarity

Adding an explicit type annotation would improve code documentation:

 }
-TOOL_GROUPS = MappingProxyType(_TOOL_GROUPS)
+TOOL_GROUPS: MappingProxyType[str, Callable[[FastMCP], None]] = MappingProxyType(_TOOL_GROUPS)

60-63: Consider clarifying the warning message.

The warning message "Unprefixed items in exclusion mode will be ignored" could be misinterpreted. Users might think these groups won't be registered at all, when actually they will be registered (because they're not excluded).

Suggested improvement for clarity
             logging.warning(
-                f"Unprefixed items in exclusion mode will be ignored: {unprefixed_str}. "
+                f"Unprefixed items in exclusion mode will not be excluded (groups will still be registered): {unprefixed_str}. "
                 f"Did you mean: {suggested}?"
             )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12eafa4 and a1bfdc0.

📒 Files selected for processing (1)
  • plane_mcp/tools/__init__.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
plane_mcp/tools/__init__.py (3)

3-6: LGTM!

The new imports are appropriate and necessary for the environment-based configuration and immutable tool groups mapping.


34-45: LGTM!

The docstring is comprehensive and provides clear examples for each mode. The listing of available tool groups is particularly helpful for users.


71-83: LGTM!

The inclusion mode parsing and registration loop are correctly implemented. Empty strings from malformed input (trailing/double commas) are silently ignored, which is reasonable behavior. The validation warnings will help users catch typos in group names.

# Exclusion mode: start with all, remove specified groups
# Only items prefixed with ! are excluded
items = [t.strip() for t in tools_config.split(",")]
excluded = {t[1:] for t in items if t.startswith("!")}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Whitespace after ! prefix not stripped in exclusion

In exclusion mode, when extracting group names with t[1:], whitespace after the ! prefix is preserved. If a user writes PLANE_TOOLS="! cycles" (with a space after !), the excluded set contains " cycles" (with leading space), which doesn't match "cycles" in TOOL_GROUPS. This causes the exclusion to silently fail—all groups remain enabled. A warning is logged about unknown groups, but the behavior is unexpected compared to inclusion mode which properly strips each item.

Fix in Cursor Fix in Web

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: 5

🧹 Nitpick comments (5)
.gitignore (1)

52-55: .mcp.json is slightly miscategorized

.mcp.json is a tool/IDE configuration file (MCP server config), not an environment-variables file. Consider moving it to the # IDE section, or grouping it with the Cursor AI rules block under a broader "# AI tool configuration" heading for clarity.

♻️ Proposed reorganization
-# Environment variables
-.env
-.env.local
-.mcp.json

+# Environment variables
+.env
+.env.local
+
+# AI tool configuration
+.cursor/rules/codacy.mdc
+.mcp.json
+.claude/settings.local.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 52 - 55, The .mcp.json entry is currently listed
under the environment variables block but is an IDE/tool config (MCP server
config); move the ".mcp.json" line out of the "# Environment variables" group
and place it under the "# IDE" section (or into a new "# AI tool configuration"
/ Cursor AI grouping) so .mcp.json is clearly categorized with other IDE/tool
config entries.
plane_mcp/tools/work_items.py (2)

388-410: _to_work_item_full accesses detail.assignees and detail.labels attributes directly, while _to_work_item_summary uses getattr.

In _to_work_item_summary (Line 373-374), you use getattr(item, "assignees", None) defensively, but in _to_work_item_full (Lines 389, 393), you access detail.assignees and detail.labels directly. If WorkItemDetail always guarantees these attributes, that's fine. Otherwise, align the access pattern for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/work_items.py` around lines 388 - 410, The function
_to_work_item_full currently accesses detail.assignees and detail.labels
directly which can raise AttributeError if those fields are missing; change
those accesses to use getattr(detail, "assignees", []) and getattr(detail,
"labels", []) (matching the defensive pattern used in _to_work_item_summary) so
the list comprehensions for AssigneeSummary and LabelSummary safely iterate an
empty list when the attributes are absent.

372-385: _to_work_item_summary — consider adding type hints for the item parameter.

The item parameter is untyped, making it unclear what duck-typed interface is expected. Since this function handles both raw API response objects (with .assignees as objects) and potentially other shapes (assignees as strings), a brief type hint or docstring would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/work_items.py` around lines 372 - 385, The
_to_work_item_summary function's item parameter is untyped; add a concise type
hint (e.g., a Protocol/TypedDict or Union) that documents the expected
attributes (id, sequence_id, name, priority, state, assignees, labels) and
import typing.Protocol or typing.TypedDict as appropriate, then annotate the
signature as def _to_work_item_summary(item: WorkItemLike) -> WorkItemSummary so
callers and linters know the duck-typed interface; reference the WorkItemSummary
return type and ensure the Protocol/TypedDict matches how assignees/labels can
be str or objects with .id.
plane_mcp/models.py (1)

21-42: _HTMLStripper collapses all structure (paragraphs, line breaks) into a single space-separated string.

Tags like <p>, <br>, <li> won't produce any whitespace separation — "<p>First paragraph</p><p>Second paragraph</p>" becomes "First paragraph Second paragraph". For token-efficient MCP responses this may be acceptable, but descriptions with multiple paragraphs or lists will lose readability. Consider inserting a newline for block-level tags if plain-text fidelity matters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/models.py` around lines 21 - 42, The current _HTMLStripper
collapses all tags into a single space which loses paragraph/list structure;
update _HTMLStripper (methods handle_starttag/handle_endtag and existing
handle_data/get_text used by strip_html) to insert explicit newline markers when
encountering block-level tags (e.g., p, br, li, div, h1..h6, ul, ol, table, tr)
and preserve spaces for inline data, then normalize consecutive newlines and
trim in get_text so strip_html returns readable plain text with paragraph/line
breaks preserved.
plane_mcp/tools/states.py (1)

10-13: _enum_str is duplicated across states.py and work_items.py.

This exact function exists in both plane_mcp/tools/states.py (Lines 10-13) and plane_mcp/tools/work_items.py (Lines 366-369). Consider extracting it to a shared utility (e.g., plane_mcp/models.py or a new plane_mcp/utils.py) to keep it DRY.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/states.py` around lines 10 - 13, The helper function
_enum_str is duplicated; extract it into a single shared module (e.g., create
plane_mcp/utils.py with def _enum_str(v) -> str | None: ...) and then replace
the local definitions in both _enum_str occurrences with an import from that new
module (update states and work_items to from plane_mcp.utils import _enum_str),
removing the duplicated implementations so both modules use the single shared
function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/mcp-tool-design/SKILL.md:
- Around line 6-8: The SKILL.md is copy-pasted from Auto-K and must be fully
rewritten to reflect Plane MCP; remove any references to "Auto-K server",
graph-based APIs (search_graph, create_nodes, get_node_details), and nonexistent
classes like ToolError. Replace examples and directory paths with the real
package root and modules (plane_mcp/, and tools cycles.py, initiatives.py,
intake.py, modules.py, projects.py, states.py, users.py, work_items.py) and use
real entities (ProjectSummary, WorkItemSummary, StateSummary) in code examples
and patterns (flat params, token efficiency, progressive disclosure). Update the
checklist to reference actual docs/tests locations or remove invalid paths
(docs/agent-tools.md, src/tests/integration/mcp/), and use concrete snippets
from existing tool implementations to illustrate best practices and boilerplate
for tool wrappers and summaries.

In `@plane_mcp/tools/projects.py`:
- Around line 37-38: The docstring in the function returning projects
incorrectly lists fields ("description, timezone, etc.") that are not present on
the ProjectSummary dataclass; update the docstring in
plane_mcp/tools/projects.py to accurately state that the return is a list of
ProjectSummary objects containing only id, name, and identifier (or explicitly
reference ProjectSummary for field details), or alternatively add the missing
fields to the ProjectSummary model in plane_mcp/models.py (Lines around the
ProjectSummary definition) if those fields are intended to exist—pick one option
and make the docstring and model consistent.

In `@plane_mcp/tools/states.py`:
- Around line 23-33: The docstring in plane_mcp/tools/states.py claims returned
StateSummary objects include color and sequence, but the actual model class
StateSummary in plane_mcp/models.py only defines id, name, group, and default;
update the code so documentation and model agree by either (A) editing the
docstring in the function in plane_mcp/tools/states.py to list only id, name,
group, and default, or (B) if color and sequence are intended, add those fields
to the StateSummary model (and any serialization/creation paths) in
plane_mcp/models.py; pick one approach and make sure the docstring and
StateSummary definition are consistent.

In `@plane_mcp/tools/work_items.py`:
- Line 35: Several functions in work_items.py (and similarly in projects.py and
states.py) declare return types like "-> list[WorkItemSummary]" but call
".slim()" and thus return dicts; remove the ".slim()" calls and return the
Pydantic/Model instances directly so the runtime return types match the
annotations and FastMCP can handle serialization. Locate methods that return
list[WorkItemSummary] or single WorkItemSummary (and analogous Project/State
model returners) and replace "return something.slim()" with "return something"
(or ensure list elements are model instances), keeping the existing type
annotations unchanged; run type checks and unit tests to confirm no regressions.

In `@pyproject.toml`:
- Line 15: The pyproject.toml currently pins fastmcp==2.14.5 but
[tool.uv.sources] overrides it to v3.0.0rc2 (pre-release), causing
inconsistency; update pyproject.toml so declared dependency and uv source agree
— either change the declared dependency to fastmcp>=3.0.0rc2 (or
fastmcp==3.0.0rc2) to match the git source, or remove the uv override to use the
stable 2.14.5, and if you intentionally require the RC add a clear comment next
to [tool.uv.sources] documenting why v3.0.0rc2 is required and its production
implications.

---

Duplicate comments:
In `@plane_mcp/tools/projects.py`:
- Line 28: The functions list_projects, create_project, retrieve_project, and
update_project currently annotate returns as ProjectSummary (or
list[ProjectSummary]) but call .slim() and actually return dicts; fix by either
changing the return type annotations to list[dict] / dict as appropriate or by
converting the slim() dicts into ProjectSummary instances before returning
(e.g., map ProjectSummary(**data) or ProjectSummary.parse_obj(data) for each
item returned by list_projects). Update the return annotations to match the
chosen approach and ensure callers expect the correct type.

---

Nitpick comments:
In @.gitignore:
- Around line 52-55: The .mcp.json entry is currently listed under the
environment variables block but is an IDE/tool config (MCP server config); move
the ".mcp.json" line out of the "# Environment variables" group and place it
under the "# IDE" section (or into a new "# AI tool configuration" / Cursor AI
grouping) so .mcp.json is clearly categorized with other IDE/tool config
entries.

In `@plane_mcp/models.py`:
- Around line 21-42: The current _HTMLStripper collapses all tags into a single
space which loses paragraph/list structure; update _HTMLStripper (methods
handle_starttag/handle_endtag and existing handle_data/get_text used by
strip_html) to insert explicit newline markers when encountering block-level
tags (e.g., p, br, li, div, h1..h6, ul, ol, table, tr) and preserve spaces for
inline data, then normalize consecutive newlines and trim in get_text so
strip_html returns readable plain text with paragraph/line breaks preserved.

In `@plane_mcp/tools/states.py`:
- Around line 10-13: The helper function _enum_str is duplicated; extract it
into a single shared module (e.g., create plane_mcp/utils.py with def
_enum_str(v) -> str | None: ...) and then replace the local definitions in both
_enum_str occurrences with an import from that new module (update states and
work_items to from plane_mcp.utils import _enum_str), removing the duplicated
implementations so both modules use the single shared function.

In `@plane_mcp/tools/work_items.py`:
- Around line 388-410: The function _to_work_item_full currently accesses
detail.assignees and detail.labels directly which can raise AttributeError if
those fields are missing; change those accesses to use getattr(detail,
"assignees", []) and getattr(detail, "labels", []) (matching the defensive
pattern used in _to_work_item_summary) so the list comprehensions for
AssigneeSummary and LabelSummary safely iterate an empty list when the
attributes are absent.
- Around line 372-385: The _to_work_item_summary function's item parameter is
untyped; add a concise type hint (e.g., a Protocol/TypedDict or Union) that
documents the expected attributes (id, sequence_id, name, priority, state,
assignees, labels) and import typing.Protocol or typing.TypedDict as
appropriate, then annotate the signature as def _to_work_item_summary(item:
WorkItemLike) -> WorkItemSummary so callers and linters know the duck-typed
interface; reference the WorkItemSummary return type and ensure the
Protocol/TypedDict matches how assignees/labels can be str or objects with .id.

Comment on lines +6 to +8
# MCP Tool Design Guide for Auto-K

Reference for designing, implementing, and reviewing MCP tools exposed to AI agents.
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

🏁 Script executed:

#!/bin/bash
# Check actual project structure
echo "=== Project directory listing (top level) ==="
ls -la | grep -E '^d' | head -20

echo ""
echo "=== Check for plane_mcp directory ==="
find . -maxdepth 2 -type d -name 'plane_mcp' 2>/dev/null

echo ""
echo "=== Check for app/features/mcp directory ==="
find . -path '*/app/features/mcp' -type d 2>/dev/null || echo "Not found"

echo ""
echo "=== Look for ToolError class ==="
rg -rn "class ToolError" --type py 2>/dev/null || echo "Not found"

echo ""
echo "=== Look for search_graph, create_nodes, get_node_details ==="
rg -rn "def (search_graph|create_nodes|get_node_details)" --type py 2>/dev/null || echo "Not found"

echo ""
echo "=== Check SKILL.md file content (first 50 lines) ==="
head -50 .claude/skills/mcp-tool-design/SKILL.md 2>/dev/null || echo "File not found"

Repository: makeplane/plane-mcp-server

Length of output: 3224


🏁 Script executed:

#!/bin/bash
# Read full SKILL.md to identify all Auto-K references
echo "=== Full SKILL.md content ==="
wc -l .claude/skills/mcp-tool-design/SKILL.md

echo ""
echo "=== Searching for path references in SKILL.md ==="
grep -n "app/features/mcp\|src/tests\|docs/agent-tools" .claude/skills/mcp-tool-design/SKILL.md || echo "Checking next..."

echo ""
echo "=== Lines mentioning app/features or Auto-K ==="
rg -n "app/features|Auto-K|auto-k" .claude/skills/mcp-tool-design/SKILL.md

echo ""
echo "=== Actual Plane MCP package structure ==="
find ./plane_mcp -type f -name "*.py" | head -30

echo ""
echo "=== Look for actual MCP tools in plane_mcp ==="
find ./plane_mcp -type f -name "*.py" -path "*/tools/*" 2>/dev/null | head -20

echo ""
echo "=== Check what entities Plane MCP actually handles ==="
rg -n "class|def " plane_mcp/models.py 2>/dev/null | head -40

Repository: makeplane/plane-mcp-server

Length of output: 2639


🏁 Script executed:

#!/bin/bash
# Check if referenced documentation/test paths exist
echo "=== Check for docs/agent-tools.md ==="
find . -name "agent-tools.md" -o -path "*/docs/agent-tools.md" 2>/dev/null || echo "Not found"

echo ""
echo "=== Check for src/tests/integration/mcp/ ==="
find . -path "*/tests/integration/mcp" -type d 2>/dev/null || echo "Not found"

echo ""
echo "=== Check actual test structure ==="
find . -type d -name "tests" 2>/dev/null

echo ""
echo "=== Sample actual tool implementation (projects.py) ==="
head -100 plane_mcp/tools/projects.py

echo ""
echo "=== Check for actual error handling patterns ==="
rg -n "Error|Exception" plane_mcp/tools/projects.py | head -20

Repository: makeplane/plane-mcp-server

Length of output: 3708


Skill file is copy-pasted from a different project ("Auto-K") and needs complete rewriting.

This document references "Auto-K server" (lines 3, 6), graph-based operations (search_graph, create_nodes, get_node_details), and incorrect directory structures. The actual Plane MCP project uses:

  • Package root: plane_mcp/ (not app/features/mcp/)
  • Actual tools: cycles.py, initiatives.py, intake.py, modules.py, projects.py, states.py, users.py, work_items.py
  • Real entities: ProjectSummary, WorkItemSummary, StateSummary (not nodes/edges/graphs)
  • No ToolError class exists in this project

The checklist (lines 398–413) references paths that don't exist in this repository:

  • docs/agent-tools.md — not found
  • src/tests/integration/mcp/ — not found

The general design principles (token efficiency, flat parameters, progressive disclosure) are sound, but all specific examples, paths, and entities mislead developers about the actual structure of this codebase. This skill file should be rewritten to document actual Plane MCP tool patterns, using real tool implementations as examples.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/mcp-tool-design/SKILL.md around lines 6 - 8, The SKILL.md is
copy-pasted from Auto-K and must be fully rewritten to reflect Plane MCP; remove
any references to "Auto-K server", graph-based APIs (search_graph, create_nodes,
get_node_details), and nonexistent classes like ToolError. Replace examples and
directory paths with the real package root and modules (plane_mcp/, and tools
cycles.py, initiatives.py, intake.py, modules.py, projects.py, states.py,
users.py, work_items.py) and use real entities (ProjectSummary, WorkItemSummary,
StateSummary) in code examples and patterns (flat params, token efficiency,
progressive disclosure). Update the checklist to reference actual docs/tests
locations or remove invalid paths (docs/agent-tools.md,
src/tests/integration/mcp/), and use concrete snippets from existing tool
implementations to illustrate best practices and boilerplate for tool wrappers
and summaries.

Comment on lines 37 to +38
Returns:
List of Project objects
List of ProjectSummary objects with id, name, identifier, description, timezone, etc.
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 | 🟡 Minor

Docstring mentions fields not present in ProjectSummary.

Line 38 claims the return includes "description, timezone, etc." but ProjectSummary (in plane_mcp/models.py, Lines 50-55) only has id, name, and identifier. This is misleading — especially for AI agents relying on docstrings to understand the API.

📝 Proposed fix
         Returns:
-            List of ProjectSummary objects with id, name, identifier, description, timezone, etc.
+            List of ProjectSummary objects with id, name, and identifier.
📝 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
Returns:
List of Project objects
List of ProjectSummary objects with id, name, identifier, description, timezone, etc.
Returns:
List of ProjectSummary objects with id, name, and identifier.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/projects.py` around lines 37 - 38, The docstring in the
function returning projects incorrectly lists fields ("description, timezone,
etc.") that are not present on the ProjectSummary dataclass; update the
docstring in plane_mcp/tools/projects.py to accurately state that the return is
a list of ProjectSummary objects containing only id, name, and identifier (or
explicitly reference ProjectSummary for field details), or alternatively add the
missing fields to the ProjectSummary model in plane_mcp/models.py (Lines around
the ProjectSummary definition) if those fields are intended to exist—pick one
option and make the docstring and model consistent.

Comment thread plane_mcp/tools/states.py
Comment on lines +23 to +33
"""
List all states for a project.

Use this to get state UUIDs needed when creating or updating work items.

Args:
project_id: UUID of the project

Returns:
List of StateSummary objects containing id, name, group, color, default, sequence.
"""
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 | 🟡 Minor

Docstring mentions fields not present in StateSummary.

The docstring on Line 32 claims the return includes color and sequence, but the StateSummary model (in plane_mcp/models.py, Line 107-113) only has id, name, group, and default. This will mislead callers (especially AI agents) into expecting fields that are never returned.

📝 Proposed fix
         Returns:
-            List of StateSummary objects containing id, name, group, color, default, sequence.
+            List of StateSummary objects containing id, name, group, and default.
📝 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
"""
List all states for a project.
Use this to get state UUIDs needed when creating or updating work items.
Args:
project_id: UUID of the project
Returns:
List of StateSummary objects containing id, name, group, color, default, sequence.
"""
"""
List all states for a project.
Use this to get state UUIDs needed when creating or updating work items.
Args:
project_id: UUID of the project
Returns:
List of StateSummary objects containing id, name, group, and default.
"""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/states.py` around lines 23 - 33, The docstring in
plane_mcp/tools/states.py claims returned StateSummary objects include color and
sequence, but the actual model class StateSummary in plane_mcp/models.py only
defines id, name, group, and default; update the code so documentation and model
agree by either (A) editing the docstring in the function in
plane_mcp/tools/states.py to list only id, name, group, and default, or (B) if
color and sequence are intended, add those fields to the StateSummary model (and
any serialization/creation paths) in plane_mcp/models.py; pick one approach and
make sure the docstring and StateSummary definition are consistent.

external_id: str | None = None,
external_source: str | None = None,
) -> list[WorkItem]:
) -> list[WorkItemSummary]:
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

🏁 Script executed:

#!/bin/bash
# Find the slim() method definition
echo "=== Finding slim() method definition ==="
rg -n "def slim" plane_mcp/models.py -A 5

echo ""
echo "=== Checking actual return types in work_items.py ==="
# Look at function signatures and their return statements
rg -n "def (list_work_items|create_work_item|retrieve_work_item|retrieve_by_identifier|update_work_item)" plane_mcp/tools/work_items.py -A 1

echo ""
echo "=== Checking return statements with .slim() ==="
rg -n "return.*\.slim\(\)" plane_mcp/tools/work_items.py -B 2

echo ""
echo "=== Checking other tool files ==="
rg -n "return.*\.slim\(\)" plane_mcp/tools/projects.py plane_mcp/tools/states.py

echo ""
echo "=== Looking for fastmcp tool decorator usage ==="
rg -n "@mcp\.tool" plane_mcp/tools/work_items.py -B 1 -A 5 | head -50

Repository: makeplane/plane-mcp-server

Length of output: 2970


🏁 Script executed:

#!/bin/bash
# Check fastmcp documentation or handling of return types
echo "=== Checking fastmcp imports and usage ==="
rg -n "import.*mcp|from.*mcp" plane_mcp/tools/work_items.py

echo ""
echo "=== Check if there's any mcp configuration ==="
find plane_mcp -name "*.py" -type f -exec rg -l "mcp" {} \; | head -20

Repository: makeplane/plane-mcp-server

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Get full function signatures with return types
echo "=== Full function signatures with return types in work_items.py ==="
rg -n "def (list_work_items|create_work_item|retrieve_work_item|retrieve_work_item_by_identifier|update_work_item)" plane_mcp/tools/work_items.py -A 8 | grep -E "def |-> "

echo ""
echo "=== Check projects.py ==="
rg -n "def (list_projects|get_project)" plane_mcp/tools/projects.py -A 8 | grep -E "def |-> "

echo ""
echo "=== Check states.py ==="
rg -n "def " plane_mcp/tools/states.py | head -20

echo ""
echo "=== Look at fastmcp documentation or usage in server.py ==="
rg -n "@mcp\.tool\|def |return " plane_mcp/server.py -B 2 -A 2 | head -40

Repository: makeplane/plane-mcp-server

Length of output: 1116


🏁 Script executed:

#!/bin/bash
# Check if fastmcp is a known library and how it handles models
echo "=== Check requirements/dependencies for fastmcp version ==="
find plane_mcp -name "*.txt" -o -name "*.toml" -o -name "*.cfg" | xargs cat 2>/dev/null | grep -i fastmcp

echo ""
echo "=== Look for any serialization or model handling in the codebase ==="
rg -n "exclude_none|model_dump" plane_mcp/

Repository: makeplane/plane-mcp-server

Length of output: 272


🏁 Script executed:

#!/bin/bash
# Get exact return types for all functions in work_items.py
echo "=== All function return types in work_items.py ==="
rg -n "^    def |    \) -> " plane_mcp/tools/work_items.py | paste - -

echo ""
echo "=== All function return types in projects.py ==="
rg -n "^    def |    \) -> " plane_mcp/tools/projects.py | paste - -

echo ""
echo "=== All function return types in states.py ==="
rg -n "^    def |    \) -> " plane_mcp/tools/states.py | paste - -

echo ""
echo "=== Check if fastmcp is in pyproject.toml or requirements ==="
find . -maxdepth 2 -name "pyproject.toml" -o -name "requirements*.txt" | xargs cat 2>/dev/null | head -50

Repository: makeplane/plane-mcp-server

Length of output: 1508


🏁 Script executed:

#!/bin/bash
# Check if there's documentation about fastmcp or how it serializes returns
echo "=== Check imports and any type hints for `@mcp.tool`() ==="
rg -n "from fastmcp|import fastmcp" plane_mcp/ -A 3

echo ""
echo "=== Look for any middleware or serialization logic ==="
rg -n "json|serial|JSON" plane_mcp/server.py

Repository: makeplane/plane-mcp-server

Length of output: 5975


🏁 Script executed:

#!/bin/bash
# Try to understand how fastmcp handles return types
# Check for any custom JSON encoder or serialization
echo "=== Check for any JSON encoding or custom serializers ==="
rg -n "JSONEncoder|dumps|json" plane_mcp/tools/work_items.py

echo ""
echo "=== Check if there are any tests that show the expected return types ==="
find . -name "*test*.py" -type f | head -10

echo ""
echo "=== Look at the actual model_dump call and what slim() returns ==="
cat plane_mcp/models.py | head -30

Repository: makeplane/plane-mcp-server

Length of output: 1053


🌐 Web query:

fastmcp 2.14.5 Pydantic model serialization automatic

💡 Result:

In FastMCP v2.14.5 there isn’t a new/changed “automatic Pydantic serialization” feature—2.14.5 is a patch release focused on a memory-leak fix (pydocket bump). [2]

What you’re looking for is FastMCP’s Structured Output behavior (added earlier, new in v2.10.0): if a tool returns an object-like value—including a Pydantic BaseModel—FastMCP automatically emits structuredContent (machine-readable JSON) alongside the usual human-readable content. [1]

Key rules:

  • dict / dataclass / Pydantic model → always becomes structuredContent (even without an explicit output schema). [1]
  • Primitives/collections (int, str, list, etc.) → only become structuredContent if you provide a return type annotation / output schema; otherwise you’ll only get content. [1]
  • If you need a specific text format (YAML/Markdown/etc.), return a ToolResult or configure a server tool_serializer. [1][3]

Sources: FastMCP tools/structured output docs [1], FastMCP v2.14.5 release notes [2], FastMCP server serialization docs [3].


🏁 Script executed:

#!/bin/bash
# Check if any code depends on the .slim() calls returning dicts
echo "=== Search for any code that accesses the return value of these functions ==="
rg -n "list_work_items|create_work_item|retrieve_work_item|update_work_item" plane_mcp/ --type py -B 1 -A 3 | grep -v "def " | head -50

echo ""
echo "=== Check if _to_work_item_summary and _to_work_item_full are used elsewhere ==="
rg -n "_to_work_item_summary|_to_work_item_full" plane_mcp/ --type py

echo ""
echo "=== Verify function signatures one more time with context ==="
sed -n '28,35p' plane_mcp/tools/work_items.py
echo "---"
sed -n '69,76p' plane_mcp/tools/work_items.py
echo "---"
sed -n '146,154p' plane_mcp/tools/work_items.py
echo "---"
sed -n '189,197p' plane_mcp/tools/work_items.py
echo "---"
sed -n '232,240p' plane_mcp/tools/work_items.py

Repository: makeplane/plane-mcp-server

Length of output: 5166


Return type annotations don't match actual returns — calling .slim() is unnecessary and violates type contracts.

Functions declare return types like -> list[WorkItemSummary] but actually return list[dict] (via .slim() which calls model_dump(exclude_none=True)). While this doesn't break at runtime—FastMCP 2.14.5 automatically serializes both Pydantic models and dicts to structuredContent—it's still incorrect:

  • Type checkers will flag the mismatch
  • The .slim() calls are redundant (FastMCP handles serialization automatically)
  • Return values should match their annotations

Better approach: Remove all .slim() calls and return the model instances directly. FastMCP will serialize them automatically, type annotations will be correct, and code will be simpler.

This pattern appears across work_items.py, projects.py, and states.py.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/tools/work_items.py` at line 35, Several functions in work_items.py
(and similarly in projects.py and states.py) declare return types like "->
list[WorkItemSummary]" but call ".slim()" and thus return dicts; remove the
".slim()" calls and return the Pydantic/Model instances directly so the runtime
return types match the annotations and FastMCP can handle serialization. Locate
methods that return list[WorkItemSummary] or single WorkItemSummary (and
analogous Project/State model returners) and replace "return something.slim()"
with "return something" (or ensure list elements are model instances), keeping
the existing type annotations unchanged; run type checks and unit tests to
confirm no regressions.

Comment thread pyproject.toml

dependencies = [
"fastmcp==2.14.1",
"fastmcp==2.14.5",
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

🏁 Script executed:

cat -n pyproject.toml | head -60

Repository: makeplane/plane-mcp-server

Length of output: 1615


🌐 Web query:

fastmcp v3.0.0rc2 release notes changelog

💡 Result:

FastMCP v3.0.0rc2 — release notes / changelog (Pre-release, Feb 14, 2026)

  • Breaking change: fastmcp dev is now a subcommand group; the Inspector moved to fastmcp dev inspector. [1]
  • Bug fix: Fix include_tags / exclude_tags being ignored when MCPConfig has no tools. [1]
  • Dependency update: Bump cryptography 46.0.4 → 46.0.5 (uv group). [1]
  • Full diff: v3.0.0rc1...v3.0.0rc2. [1]

Release listing on PyPI: shows 3.0.0rc2 dated Feb 14, 2026. [2]

Sources
[1] GitHub release: “v3.0.0rc2: Shell Game”
[2] PyPI release history for fastmcp


🌐 Web query:

fastmcp 3.0.0 final release PyPI latest version

💡 Result:

  • No, FastMCP 3.0.0 final is not released on PyPI yet; the newest 3.x on PyPI is 3.0.0rc2 (pre-release, Feb 14, 2026). [1]
  • The latest stable (non–pre-release) PyPI version of fastmcp is 2.14.5 (Feb 3, 2026). [1]
  • FastMCP’s own release notes also list the 3.0.0 release-candidate track (e.g., 3.0.0rc1 on Feb 12, 2026). [2]

Sources: [1] PyPI fastmcp project page, [2] FastMCP Updates page.


🏁 Script executed:

rg -i "fastmcp|dev|inspector|include_tags|exclude_tags" --type py -A 3

Repository: makeplane/plane-mcp-server

Length of output: 11412


Version mismatch: fastmcp==2.14.5 declares stable release, but [tool.uv.sources] overrides to v3.0.0rc2 pre-release.

Line 15 pins fastmcp==2.14.5 (latest stable from PyPI), but lines 52–53 override it via [tool.uv.sources] to install v3.0.0rc2 from git. This creates environment inconsistency:

  • pip / poetry users install 2.14.5 from PyPI
  • uv users install 3.0.0rc2 from git

Since fastmcp 3.0.0 is still unreleased (3.0.0rc2 is current pre-release as of Feb 14, 2026), this approach requires using an RC version in production without a documented reason.

Either align the declared version (fastmcp>=3.0.0rc2 or wait for stable 3.0.0), or add a clear comment explaining the override and its implications.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` at line 15, The pyproject.toml currently pins fastmcp==2.14.5
but [tool.uv.sources] overrides it to v3.0.0rc2 (pre-release), causing
inconsistency; update pyproject.toml so declared dependency and uv source agree
— either change the declared dependency to fastmcp>=3.0.0rc2 (or
fastmcp==3.0.0rc2) to match the git source, or remove the uv override to use the
stable 2.14.5, and if you intentionally require the RC add a clear comment next
to [tool.uv.sources] documenting why v3.0.0rc2 is required and its production
implications.

@CaliLuke CaliLuke closed this Feb 17, 2026
@CaliLuke CaliLuke deleted the tool-group-filtering branch February 17, 2026 22:52
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