post_roles

Changes data · you confirm first

Calls POST /api/roles as the signed-in person · REST reference

Create a role

Requires execute:CreateRoleCommand; platform staff bypass that gate. The caller's own token is forwarded to the DAL, and this route sends no orgId, so the role belongs to no organization: the DAL creates it only for a caller with platform authority and refuses anyone else with 403. Duplicate names are refused up front with 409 ROLE_NAME_CONFLICT, matched against non-archived roles only — a name freed by archiving can be reused. TWO FIELDS DO NOT PERSIST. isSystem is validated, assigned to the model and echoed in the 201, but the DAL's CreateRoleInput has no such field, so the stored role is never marked as a system role. And the id in the response is a uuid the MODEL generated before the call — CreateRoleInput carries no id, so the DAL assigns its own and the returned id may not identify the stored role. Read the role back with GET /api/roles before using its id. 201 with a message, in the { success, data, message } envelope.

Arguments

ArgumentTypeDescription
bodyJSONobjectrequiredThe request body, as a JSON object. Its fields are listed below.

Fields of body

FieldTypeDescription
descriptionstringOptional here, but sent to the DAL as '' when omitted.
isSystembooleanAccepted, echoed, and NOT persisted — see the endpoint description.
namestringrequiredTrimmed before validation. Must not collide with a live role.
permissionsarray of string

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.

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "post_roles",
    "arguments": {
      "body": {
        "name": "string",
        "description": "string",
        "isSystem": true,
        "permissions": [
          "string"
        ]
      }
    }
  }
}
Confirmation
The client shows the person the exact request and waits. Nothing runs until they tick Make this change. See Reading and changing.

Result

On success (HTTP 201) the tool returns one text item holding the API's JSON, pretty-printed.

The in-memory Role model, echoed before the DAL entity is read back. ISO-8601 timestamps.

json
{
  "data": {
    "createdAt": "2026-01-15T12:00:00Z",
    "groupCount": 0,
    "id": "11111111-1111-4111-8111-111111111111",
    "isSystem": true,
    "metadata": {},
    "name": "string",
    "permissions": [
      "string"
    ],
    "status": "ACTIVE",
    "createdBy": "11111111-1111-4111-8111-111111111111",
    "description": "string"
  },
  "message": "Role created successfully",
  "success": true
}

Fields

FieldTypeDescription
dataobject
data.createdAtstring (date-time)
data.createdBystring (uuid) | null
data.descriptionstring | null
data.groupCount0
data.idstring (uuid)Model-generated; NOT necessarily the stored role's id.
data.isSystembooleanEchoes the request; the stored role is not marked.
data.metadataobject
data.namestring
data.permissionsarray of string
data.status"ACTIVE"
message"Role created successfully"
successtrue

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.

400

express-validator rejected the body — missing/oversized name, oversized description, non-boolean isSystem, or a non-array/non-string-element permissions — with an ARRAY-shaped details; or the DAL's own validation refused it (a name that is not 2-50 letters, digits and underscores, a permission name not of the execute:/query: shape), carrying the DAL's sentence and no details.

403

Authenticated, but the caller holds none of the required permissions.

409

A live role already has that name — found by the controller's lookup (Role name already exists, code: ROLE_NAME_CONFLICT) or by the DAL's own check (the DAL's sentence, no code).

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.

json
{
  "name": "post_roles",
  "description": "POST /api/roles — Requires `execute:CreateRoleCommand`; platform staff bypass that gate. The caller's own token is forwarded to the DAL, and this route sends no `orgId`, so the role belongs to no organization: the DAL creates it only for a caller with platform authority and refuses anyone else with 403. Duplicate names are refused up front with 409 `ROLE_NAME_CONFLICT`, matched against non-archived roles only — a name freed by archiving can be reused. TWO FIELDS DO NOT PERSIST. `isSystem` is validated, assigned to the model and echoed in the 201, but the DAL's `CreateRoleInput` has no such field, so the stored role is never marked as a system role. And the `id` in the response is a uuid the MODEL generated before the call — `CreateRoleInput` carries no id, so the DAL assigns its own and the returned id may not identify the stored role. Read the role back with `GET /api/roles` before using its id. 201 with a `message`, in the `{ success, data, message }` envelope.\n\nThis changes data. The person is asked to confirm it before it runs, and nothing happens if they decline.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "body": {
        "type": "string",
        "description": "The JSON request body: { description: string (Optional here, but sent to the DAL as '' when omitted.); isSystem: boolean (Accepted, echoed, and NOT persisted — see the endpoint description.); name*: string (Trimmed before validation.); permissions: array of string }"
      }
    },
    "required": [
      "body"
    ]
  },
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "idempotentHint": false,
    "openWorldHint": false
  }
}