You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Upstream 00d9d1f collapses ~177 per-operation tools into 28 action-dispatch
tools. Two conflicts, both from this fork's divergence rather than anything
upstream broke.
server.py: kept get_authentik_oauth_mcp() and _public_base_url(), and routed the
Authentik factory through upstream's new _configured(). That helper is why this
merge is worth taking promptly — it puts the middleware stack and tool
registration in one place, so a transport can no longer be left a middleware
behind. A hand-copied add_middleware line missed in the previous upstream merge
is exactly what took the web connector offline (427c8b0); after this, that class
of bug is structurally impossible.
tools/workspaces.py: superseded by upstream's tools/workspace.py. Ported
get_active/set_active there as actions, adopting the local conventions — guards
return "Error: ..." strings rather than raising, the absent-parameter check runs
before the identity guard so the one refusal a caller can act on is not masked,
and the description is generated from ACTIONS.
Dropped the LEGACY aliases for the standalone get_active_workspace /
set_active_workspace spellings. The retired-name table is upstream's frozen
record of its own pre-consolidation surface, and those two names were never in
it — nor released to any user, since production still runs a pre-consolidation
image. Adding them would mean carrying an edit to a pinned upstream literal
forever.
Two exemptions added to their dispatch matrix: get_active reads the resolved
request context and never calls the SDK; set_active refuses before the SDK
unless the request carries a verified OIDC identity, which that harness has no
request context to supply — it is covered against a real identity in
tests/tools/test_workspace_active.py.
Also updated the server instructions to the backticked `workspace get_active`
reference form their test_references.py enforces, and the two README counts.
1155 tests pass; the merged server boots against the live Authentik and
advertises 28 tools with all four workspace actions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+45-11Lines changed: 45 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,19 @@ ruff check plane_mcp/
45
45
46
46
### Server Factories (`server.py`)
47
47
48
-
Three factory functions (`get_oauth_mcp`, `get_header_mcp`, `get_stdio_mcp`) each create a `FastMCP` instance, register all tools, and configure the appropriate auth provider. OAuth/HTTP modes use Redis for token storage (falls back to in-memory).
48
+
Three factory functions (`get_oauth_mcp`, `get_header_mcp`, `get_stdio_mcp`) each create a `FastMCP` instance and configure the appropriate auth provider, then hand it to `_configured()` for the middleware stack and tools — one place, so a transport cannot be left a middleware behind. OAuth/HTTP modes use Redis for token storage (falls back to in-memory).
49
+
50
+
### Middleware (`middleware.py`)
51
+
52
+
Ordered as registered; the earlier one wraps the later:
53
+
54
+
| Middleware | Does |
55
+
|---|---|
56
+
|`PlaneLoggingMiddleware`| structured logging, plus the tool name |
57
+
|`CoerceArguments`| repairs arguments a client encoded as strings, before validation (`coercion.py`) |
58
+
|`ValidateActionArguments`| refuses arguments the chosen action does not accept, from the `ACTIONS` declaration |
59
+
60
+
Coercion runs before validation so an argument is judged by the value it repairs to. `ValidateActionArguments` closes a gap a per-tool schema cannot: every action's parameters share one schema, so an argument meant for another action validated cleanly and was then dropped, and the call answered a different question than the one asked. Only arguments carrying a value are judged, and retired names are exempt — they arrive with no `action` and under their own parameter spelling.
29 tool modules organized by Plane domain (projects, work_items, cycles, modules, releases, etc.), totaling 160+ tools. Each module exports a `register_*_tools(mcp: FastMCP)` function called from `tools/__init__.py`.
73
+
One action-dispatch tool per Plane resource: **28 tools, 183 actions, ~57k chars advertised**. `tools/__init__.py` re-exports `register_tools`, so `server.py` and `__main__.py` see a single entry point.
One module per resource, each exporting `NAME`, `ACTIONS`, `LEGACY` and `register(mcp)`. `ACTIONS` is the single source of truth: the tool description and its `ToolAnnotations` are generated from it, and the conformance suite asserts they agree with the function signature. See `tools/README.md` for the full convention.
76
+
77
+
`tools/` contains resource modules plus `registry.py` (the `RESOURCES` tuple and alias tables) and `legacy.py` (retired-name resolution). Shared helpers live in `plane_mcp/toolkit/`, not here — see below.
78
+
79
+
Where a resource exists at both project and workspace scope, it resolves that once in a local `_scope_of` rather than through a shared abstraction: the two resources that need it need different shapes (`workitem_type` is a two-way split, `workitem_property` three-way plus a method-name suffix).
80
+
81
+
`RESOURCES` is an explicit tuple, not a directory scan. Its order is the advertised order and therefore a wire-format guarantee: tool definitions head a client's prompt cache, so reordering invalidates live conversations. Append; never re-sort. `test_resource_order_is_pinned` holds it to a literal list.
82
+
83
+
**Retired names.** Before consolidation this server exposed 177 tools, one per operation. 169 of those names still resolve, via a `Transform` mapping each to its `(tool, action)` pair with `action` hidden, and keeping the parameter spelling they shipped with (`work_item_id`, not `workitem_id`). The transforms implement `list_tools`/`get_tool` only — execution keeps the full schema, so tool results are unchanged, and nothing is advertised so the listing is unaffected. Seven encoded their action in a parameter and are declared in `LEGACY_UNMAPPED` with a replacement. `tests/tools/_retired_names.py` is the frozen record of all 177.
72
84
73
85
Tools return Pydantic models from `plane-sdk` and use Python 3.10+ union syntax (`str | None`).
74
86
87
+
### Toolkit (`toolkit/`)
88
+
89
+
Shared building blocks for the tool surface, split by *when* they act:
Governance has two questions, and both matter. `workspace_owns_resource` reads the workspace flag that governs a resource — used *before* a write, to pick the scope. `workspace_owns` reads the refusal — used *after*, because the flag is cached and the lockout outlives it being toggled off. There is no single flag: work item types carry their own (`is_work_item_types_enabled`, public `work_item_types`), while states, labels, workflows, templates and automations share `workspace_governance_status` (public `states_owned_by_workspace`). `GOVERNED_BY` maps resource to flag so a newly governed resource is one row.
100
+
101
+
Names are re-exported from `plane_mcp/toolkit/__init__.py`, so a resource module needs one import: `from plane_mcp.toolkit import Action, build_description, missing, opt`.
102
+
103
+
These sit outside `tools/` deliberately. They were previously `_`-prefixed modules inside the resource package, where the underscore was the module-discovery filter rather than a privacy marker — which made helper filenames load-bearing and made the most widely imported module in the package look private. Nothing here knows which catalogue is calling it.
104
+
105
+
Anything that encodes the catalogue's history — `LegacyNames`, the `RESOURCES` tuple — belongs under `tools/`, not here.
106
+
75
107
### Testing
76
108
109
+
`tests/tools/` covers the surface with no network and no credentials: surface-wide invariants, plus every action of every resource executed against `SpyClient`, a stand-in that binds each call against the genuine SDK signature and type-checks its arguments.
110
+
77
111
Integration tests in `tests/test_integration.py` use `FastMCP.Client` with `StreamableHttpTransport`. Tests run against a live Plane instance — configure via `.env.test` (copy to `.env.test.local` with real values).
0 commit comments