put_users_by_id
Calls PUT /api/users/{id} as the signed-in person · REST reference
Update a user's profile
Requires execute:UpdateUserCommand; platform staff bypass that gate. WHAT IS AND IS NOT SETTABLE. The DAL's UpdateUserInput declares firstName, lastName, phone, status, emailVerified, mfaEnabled and metadata — and NO email, so an address is immutable through this route and a body email is ignored rather than half-applied. status is likewise not accepted here: the admin UI models a user as APPROVED/PENDING/SUSPENDED while UserStatus has nine members, so writing a projected value back would silently rewrite an INVITED or ACCEPTED user. Status transitions belong to their own operations. orgId is required for the same reason POST /api/users requires one — the DAL declares it UUID! and evaluates the write against that scope: the caller must hold execute:UpdateUserCommand IN that org, and the user being edited must be a MEMBER of it unless it is the platform org (403 otherwise). No membership moves. A body with none of the four settable fields is a 400 rather than a no-op write, because the command itself refuses an empty SET with "No fields to update".
Arguments
| Argument | Type | Description | |
|---|---|---|---|
idpath | string (uuid) | required | User id. Not format-validated by this route. |
bodyJSON | object | required | The request body, as a JSON object. Its fields are listed below. |
Fields of body
| Field | Type | Description | |
|---|---|---|---|
firstName | string | 2-50 characters; the DAL rejects anything shorter or longer. | |
lastName | string | 2-50 characters; the DAL rejects anything shorter or longer. | |
orgId | string (uuid) | required | Non-empty string, the DAL's permission scope. Not validated as a UUID by this route. |
phone | string | E.164 (+12025550142) or the empty string, which clears the number. The DAL's Phone scalar rejects every other format. | |
profile | object | Stored as the user's metadata, replacing it wholesale. |
Example call
A client sends this over the MCP session; Claude and other agents build it for you. The values are placeholders — the shape is real.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "put_users_by_id",
"arguments": {
"id": "11111111-1111-4111-8111-111111111111",
"body": {
"orgId": "11111111-1111-4111-8111-111111111111",
"firstName": "string",
"lastName": "string",
"phone": "string",
"profile": {}
}
}
}
}Result
On success (HTTP 200) the tool returns one text item holding the API's JSON, pretty-printed.
The updated user — the DAL entity, passed through unchanged. Bare object, no envelope; timestamps are epoch MILLISECONDS, and no membership is projected.
{
"createdAt": 1,
"deleted": true,
"email": "string",
"emailVerified": true,
"firstName": "string",
"id": "string",
"lastName": "string",
"mfaEnabled": true,
"status": "ACCEPTED",
"acceptedAt": 1,
"deletedAt": 1,
"deletedBy": "string",
"invitedAt": 1,
"lastLoginAt": 1
}Fields
| Field | Type | Description |
|---|---|---|
acceptedAt | number | null | |
createdAt | number | |
deleted | boolean | |
deletedAt | number | null | |
deletedBy | string | null | |
email | string | |
emailVerified | boolean | |
firstName | string | |
id | string | |
invitedAt | number | null | |
lastLoginAt | number | null | |
lastName | string | |
metadata | object | null | |
mfaEnabled | boolean | |
phone | string | null | |
status | "ACCEPTED" | "ACTIVE" | "APPROVED" | "DELETED" | "DONOR_ONLY" | "INVITED" | … (9 values) | |
suspendedAt | number | null |
When it fails
A failed call returns isError: true with the API's message as text. A change the person declines, or lets time out, also comes back this way and nothing changes.
400orgIdmissing, or the body carried none offirstName/lastName/phone/profile— or a field the DAL refuses (a name's length, a phone's format), in the DAL's words.403Authenticated, but the caller holds none of the required permissions.
404No such user, or the user is already soft-deleted.
What the model is shown
This is the tool exactly as the server lists it in tools/list. The description starts with the REST call it makes.
{
"name": "put_users_by_id",
"description": "PUT /api/users/{id} — Requires `execute:UpdateUserCommand`; platform staff bypass that gate. WHAT IS AND IS NOT SETTABLE. The DAL's `UpdateUserInput` declares `firstName`, `lastName`, `phone`, `status`, `emailVerified`, `mfaEnabled` and `metadata` — and NO `email`, so an address is immutable through this route and a body `email` is ignored rather than half-applied. `status` is likewise not accepted here: the admin UI models a user as APPROVED/PENDING/SUSPENDED while `UserStatus` has nine members, so writing a projected value back would silently rewrite an INVITED or ACCEPTED user. Status transitions belong to their own operations. `orgId` is required for the same reason `POST /api/users` requires one — the DAL declares it `UUID!` and evaluates the write against that scope: the caller must hold `execute:UpdateUserCommand` IN that org, and the user being edited must be a MEMBER of it unless it is the platform org (403 otherwise). No membership moves. A body with none of the four settable fields is a 400 rather than a no-op write, because the command itself refuses an empty SET with \"No fields to update\".\n\nThis changes data. The person is asked to confirm it before it runs, and nothing happens if they decline.",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User id. Not format-validated by this route."
},
"body": {
"type": "string",
"description": "The JSON request body: { firstName: string (2-50 characters; the DAL rejects anything shorter or longer.); lastName: string (2-50 characters; the DAL rejects anything shorter or longer.); orgId*: string (Non-empty string, the DAL's permission scope.); phone: string (E.164 (`+12025550142`) or the empty string, which clears the number.); profile: object (Stored as the user's `metadata`, replacing it wholesale.) }"
}
},
"required": [
"id",
"body"
]
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": true,
"openWorldHint": false
}
}