Skip to content

Add MCP tools for team creation, updates, roles and invitations - #8498

Merged
cstns merged 3 commits into
mainfrom
7694-mcp-team-write-tools
Sep 22, 2026
Merged

cstns merged 3 commits into
mainfrom
7694-mcp-team-write-tools

Conversation

@cstns

@cstns cstns commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Closes #7694

Adds five team and membership write tools:

  • platform_create_team wraps POST /api/v1/teams. The description covers the things an agent can't guess: the team type hashid comes from platform_list_team_types, slug rules (create is reserved, uniqueness), the self-service team creation setting for non-admins, the strict trial eligibility rules, and the fact that billing platforms return a billingURL checkout link the user has to visit.
  • platform_update_team wraps the name/slug branch of PUT /api/v1/teams/:teamId and only exposes those two fields. Type, suspended, features and properties stay out on purpose (the route treats them as mutually exclusive branches, and properties is admin-only).
  • platform_change_member_role wraps PUT /api/v1/teams/:teamId/members/:userId with role only (the permissions branch needs the RBAC feature and stays out of v1). One reality worth noting from the route: failures like demoting the only owner come back as a generic 403 invalid_request with no detail, so the description explains the usual causes instead of promising a descriptive error. The SSO-managed-membership block does have a descriptive message and is quoted verbatim.
  • platform_invite_team_member wraps POST /api/v1/teams/:teamId/invitations. The route reports per-person failures as HTTP 200 with code: invitation_failed and an error map, so the description tells agents to read the body rather than trust the status code. Max 5 per call (429 beyond that), role defaults to Member.
  • platform_resend_team_invitation wraps POST /api/v1/teams/:teamId/invitations/:invitationId (no body, extends expiry, 404 for foreign/unknown invitations).

@cstns cstns self-assigned this Sep 14, 2026
@codecov

codecov Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.10%. Comparing base (8c3cc3f) to head (67db919).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8498      +/-   ##
==========================================
+ Coverage   77.07%   77.10%   +0.02%     
==========================================
  Files         466      466              
  Lines       24943    24972      +29     
  Branches     6643     6648       +5     
==========================================
+ Hits        19226    19255      +29     
  Misses       5717     5717              
Flag Coverage Δ
backend 77.10% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Changing a member's role is now delete-class. Demoting a team's only owner
is blocked when the new role is Member but not when it is Viewer or
Dashboard (#8580), and those leave the team with no owner and nobody but a
platform admin able to restore one. That is less recoverable than anything
else here, so it belongs behind destructive tool access, and the caution
says which demotions actually bite.

The invite description claimed invitation_failed always arrives as HTTP 200
with an error map. It also arrives as a 400 with error as a plain string
when the whole call is rejected, for instance on the team user limit. Say
both, and mention the route's own 5-calls-per-30-seconds rate limit, which
is a second 429 meaning the opposite of too_many_invites.

An empty team update answered 200 with the team unchanged, which reads as an
edit that never happened; reject it the way the snapshot update tool does.

Note that a team created with a team-scoped token falls outside that scope,
so the create succeeds and every follow-up call against it does not.
@cstns

cstns commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Tested all five tools against a running platform. Most of the PR description holds up well, in particular the two calls that were clearly made from reading the routes rather than guessing:

  • the invitation_failed warning is real. Per-person failures genuinely come back as HTTP 200 with an error map ({"nobody@example.com": "External invites not permitted"}, {"definitely-not-a-user": "Not an existing user"}, {"cstns": "Already a member of the team"})
  • the "generic 403 without detail" note on role changes is accurate. A non-member and a last-owner demotion both return an identical contentless 403 invalid_request / "Invalid request", so describing it honestly rather than promising a useful error was right

Also confirmed: reserved and duplicate slugs, invalid_team_type, trial rejection for an established user, a real Stripe checkout billingURL on create, >5 invitees giving 429 too_many_invites, resend extending the expiry (verified in mailpit, real mail goes out), and 404 for unknown or foreign invitations.

Four things changed in df928b7:

Role changes are now delete-class. Demoting a team's only owner is blocked for Member but not for Viewer or Dashboard, and those succeed and leave the team with zero owners. Proved it on a throwaway team: {"role": 30} gives 403, {"role": 10} gives 200, and afterwards I couldn't promote myself back, delete the team, rename it, or invite anyone. No non-admin recovery at all. Raised as #8580. Since one call can do that irreversibly, destructiveHint: true felt more honest than leaving it alongside ordinary writes, and the caution now says which demotions actually bite.

invitation_failed has two shapes, not one. Alongside the documented HTTP 200 with an error map, there's an HTTP 400 with error as a plain string when the whole call is rejected, which is what the team user limit returns. An agent following the old wording would iterate a string.

The invite route has its own rate limit of 5 calls per 30 seconds, which is a second 429 that means the opposite of too_many_invites: one clears by waiting, the other never will. Worth noting platform_resend_team_invitation already documented its limit, so the two are consistent now.

An empty team update was a silent no-op, answering 200 with the team unchanged. platform_update_snapshot in #8497 already rejects exactly this, so this one now does too.

Also noted on platform_create_team: with a team-scoped token the new team falls outside the scope, so the create succeeds and then reading, updating or inviting to it all fail, and it doesn't show up in platform_list_teams. Not a defect, but it's a trap worth naming since scoping an agent's token is the natural way to sandbox it.

One more from reading rather than running: #8581, POST /api/v1/teams replies 403 for a non-admin when self-service creation is off and then keeps executing.

@cstns
cstns marked this pull request as ready for review September 21, 2026 12:56

@andypalmi andypalmi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked all five tools against the routes they wrap and everything holds up:

  • required vs optional fields on each tool
  • the reserved/duplicate slug and invalid_team_type handling on create
  • the trial rules and billingURL on create
  • the only-owner demotion asymmetry sitting behind destructiveHint
  • both invitation_failed shapes, plus the separate rate-limit 429
  • the empty-update guard matching platform_update_snapshot

Descriptions are accurate, handlers forward what the routes expect, and the tests cover it.

The two route issues this surfaced (#8580 for the owner-orphaning demotion, #8581 for the create route continuing after a 403) are real but correctly left as their own follow-ups rather than holding up this PR.

@cstns
cstns merged commit 2bde8b0 into main Sep 22, 2026
43 of 47 checks passed
@cstns
cstns deleted the 7694-mcp-team-write-tools branch September 22, 2026 11:43

This branch was successfully deployed

1 active deployment
staging — 67db9190 Deployed Sep 22, 2026 by cstns via Remove application #11833
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.

5.4-b Write tools, non-destructive (phase 2)

2 participants