makeplane / makeplane/plane-mcp-server
PQL filters are silently ignored on self-hosted CE — filtered queries return the FULL result set with HTTP 200
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 325
- Forks
- 178
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 2
Description
Affected: plane-mcp-server 0.2.9 and 0.2.10 (SDK plane.api.work_items). Tested against self-hosted Plane Community Edition v1.3.1, API-key auth (X-Api-Key).
Dedup note: distinct from #136 (search param), #170/#172/#188 (
*-lite404s) and #183 (state silently ignored on update). Those either 404 or ignore one field. This one returns HTTP 200 with a complete, plausible, wrong result set for any filtered read. I couldn't find an existing PQL issue.
Summary
Self-hosted CE does not implement the pql query parameter, and — like most DRF endpoints — it ignores unknown query params rather than rejecting them. So every PQL-filtered call returns the entire unfiltered set with HTTP 200.
The MCP actively advertises PQL: list_work_items/count_work_items document it, and there's a dedicated get_pql_reference tool. An agent therefore composes a filter, gets a confident 200, and reasons over data that was never filtered.
Reproduction
Workspace with 113 work items in one project, most of them Done:
BASE=https://<self-hosted>/api/v1/workspaces/<slug>
PROJ=<project-uuid>
# 1. no filter
curl -sLG -H "x-api-key: $KEY" --data-urlencode 'fields=id' \
"$BASE/projects/$PROJ/work-items/" | jq .total_count
# => 113
# 2. filter to open states only
curl -sLG -H "x-api-key: $KEY" \
--data-urlencode 'pql=stateGroup IN openStates()' --data-urlencode 'fields=id' \
"$BASE/projects/$PROJ/work-items/" | jq .total_count
# => 113 (expected: far fewer; most items are Done)
# 3. deliberately INVALID pql
curl -sLG -H "x-api-key: $KEY" \
--data-urlencode 'pql=((((not valid' --data-urlencode 'fields=id' \
"$BASE/projects/$PROJ/work-items/" | jq .total_count
# => 113 HTTP 200
Step 3 is the important one: syntactically broken PQL still succeeds. There is no server-side validation to fail against, so nothing distinguishes "filter applied" from "filter discarded".
Same via the MCP tools:
list_work_items(project_id=…, pql='stateGroup IN openStates()')→ returnsDoneitems.list_work_items(project_id=…, pql='childOf("DS-111")')→ returns all 113 items, including every unrelated one.count_work_items(pql=…)→ 404s separately (workspace-scoped endpoint, likely the same family as #188).
Why this one bites harder than a 404
A 404 is self-announcing. This isn't. In our case an agent searched for an existing ticket, got a filtered-looking result set, concluded the ticket didn't exist, and filed a duplicate. Nothing in the response indicated the filter had been dropped. (Our search was also returning empty because of #136 on 0.2.9, so the two compounded — but the PQL behaviour stands on its own.)
Suggested fix — any one of these
- Preferred: probe capability once per session and fail loudly if
pqlis passed to a backend that doesn't implement it (raisewith a message pointing at client-side filtering). - Have the server reject unknown/unsupported query params with 400 instead of ignoring them.
- If CE genuinely won't support PQL (cf. #171), drop
pqlfrom the tool schemas when the target isn't Cloud, so agents never compose one.
Option 1 is what we've done locally as a stopgap: a guard in prepare_work_item_params — the single choke point every PQL-carrying call passes through (list, list_workspace, list_archived, count_workspace, plus the cycle/module list variants) — raising a ValueError explaining that the caller should omit pql and filter client-side using expand=state.
if payload.get("pql"):
raise ValueError(_PQL_UNSUPPORTED)
Happy to open a PR for option 1 if that's a direction you'd take.
Environment
| plane-mcp-server | 0.2.9 (also present in 0.2.10) |
| Plane | Community Edition v1.3.1, self-hosted |
| Auth | API key (X-Api-Key) |
| Transport | stdio |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the prepare_work_item_params choke point and trace the PQL-carrying list and count callers, including the cycle and module variants. Reproduce the self-hosted request with valid and invalid pql values, then verify that unsupported PQL cannot silently return unfiltered results and that the chosen behavior is covered across those paths.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100