makeplane / makeplane/plane-mcp-server
Bug: assigning a non-project-member silently clears a work item's existing assignees
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 325
- Forks
- 178
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 2
Description
Edit, 2026-08-16: still present in v0.3.0, and one claim below is wrong —
manage_work_item_assigneedoes not wipe the existing assignees. Retest and correction in this comment.
What happens
Assigning a user who isn't an assignable member of the project returns 200 OK and a normal work item payload, but two things went wrong and neither is reported:
- the requested assignee was not added, and
- the assignees the work item already had were deleted.
The second part is the one that hurt. I lost assignees on real work items before I understood what was happening, because nothing in the response distinguishes this from a successful assignment.
Environment
- self-managed Plane Community Edition, API
1.2.0 plane-mcp-server0.2.11,plane-sdk0.2.20- stdio transport + PAT, Python 3.12 on Windows
- also reproduced over the HTTP header-auth transport
Reproduction
In a project ACME, alice is a project member and bob is a workspace admin who is not a member of that project.
create_work_item(project_id=ACME, name="probe", assignees=[alice])
-> 200, assignees: [alice]
update_work_item(project_id=ACME, work_item_id=<id>, assignees=[bob])
-> 200
retrieve_work_item(project_id=ACME, work_item_id=<id>, fields="id,assignees")
-> assignees: [] # alice is gone, bob was never added
Same outcome through manage_work_item_assignee(add_user_id=bob) — the tool whose docstring promises to add one assignee "without replacing the full list".
It reproduces with plain curl against PATCH /api/v1/workspaces/{slug}/projects/{project_id}/work-items/{id}/ too, so it isn't an MCP serialization problem.
Why it happens
IssueSerializer filters ids it won't accept out of the payload rather than rejecting them, and the update deletes the existing assignees before writing that filtered list:
# validate()
if data.get("assignees", []):
data["assignees"] = ProjectMember.objects.filter(
project_id=..., is_active=True, role__gte=15, member_id__in=data["assignees"],
).values_list("member_id", flat=True)
# update()
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete() # runs first, unconditionally
IssueAssignee.objects.bulk_create([...])
So a payload that filters down to empty clears the field. Three ways to land there, none of them visible to the caller: the user is a workspace member but not a member of this project; their project role is guest (5), below the role__gte=15 floor; or the project membership exists but is is_active=False.
I could only test self-managed 1.2.0. That serializer doesn't look edition-specific, so Cloud may behave the same way, but I don't want to claim something I haven't verified — if someone can check Cloud that would be useful to know.
Why it's worse through MCP than through the API
The server passes assignees straight through and returns whatever comes back; it never compares what was requested against what was applied. An agent doing bulk triage ("assign these twelve items to X") can strip the assignees off twelve items and report success.
manage_work_item_assignee is the worst case, because it reads the current assignees, appends one id, and writes the whole list back — so a single bad id discards the list it just read.
It also compounds with #172 / #188: get_project_members 404s on self-managed instances, so an agent has no way to find out who is actually assignable, and will reasonably reach for a UUID from somewhere else.
Suggested fix
Check the project's members before the write and raise, naming the ids that would work. A read-back afterwards can only report the damage — it can't undo it.
I have this implemented with unit tests and a live repro, happy to send it as a PR.
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 with IssueSerializer's validate() and update() paths, then inspect manage_work_item_assignee and reproduce the supplied API sequence with an invalid project assignee. Check the existing unit tests and live reproduction; done means invalid assignee IDs are reported without removing valid existing assignees, with coverage for the affected update paths.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authorization, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100