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-server 0.2.11, plane-sdk 0.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.
What happens
Assigning a user who isn't an assignable member of the project returns
200 OKand a normal work item payload, but two things went wrong and neither is reported: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
1.2.0plane-mcp-server0.2.11,plane-sdk0.2.20Reproduction
In a project
ACME,aliceis a project member andbobis a workspace admin who is not a member of that project.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
IssueSerializerfilters 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: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=15floor; or the project membership exists but isis_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
assigneesstraight 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_assigneeis 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_members404s 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.