@centrali-io/centrali-mcp 4.4.8-rc.7 → 4.4.8-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -338,18 +338,46 @@ function registerDescribeTools(server) {
338
338
  },
339
339
  render_url: {
340
340
  method: "client.getFileRenderUrl(renderId, options?)",
341
- note: "Returns a URL that serves the file. Supports image transformations: width, height, fit ('cover'|'contain'|'fill'), format ('webp'|'png'|'jpeg'), quality (1-100).",
342
- example: "const url = centrali.getFileRenderUrl(renderId, { width: 200, height: 200, fit: 'cover', format: 'webp' });",
341
+ important: "uploadFile() returns a renderId (string), NOT a URL. You MUST call getFileRenderUrl(renderId) to construct the actual URL for display.",
342
+ url_pattern: "https://api.{domain}/storage/ws/{workspaceId}/api/v1/render/{renderId}",
343
+ transforms: "Optional image transformations: width, height, fit ('cover'|'contain'|'fill'), format ('webp'|'png'|'jpeg'), quality (1-100).",
344
+ public_vs_private: {
345
+ public: "If uploaded with isPublic=true, the render URL works without any auth — use directly in <img> tags.",
346
+ private: "If uploaded with isPublic=false (default), the render URL requires authentication. Pass a Bearer token (service account) or x-api-key (publishable key) in the request headers. For browser rendering of private images, use a server-side proxy route or a publishable key.",
347
+ },
348
+ example: [
349
+ "// Upload returns renderId, NOT a URL",
350
+ "const { data: renderId } = await centrali.uploadFile(file, '/root/shared/logos', true);",
351
+ "",
352
+ "// Build the render URL from the renderId",
353
+ "const url = centrali.getFileRenderUrl(renderId);",
354
+ "// => 'https://api.centrali.io/storage/ws/my-workspace/api/v1/render/abc123'",
355
+ "",
356
+ "// With image transforms (thumbnail)",
357
+ "const thumbUrl = centrali.getFileRenderUrl(renderId, { width: 200, height: 200, fit: 'cover', format: 'webp' });",
358
+ "",
359
+ "// For private images in Next.js — proxy through an API route",
360
+ "// app/api/image/[renderId]/route.ts",
361
+ "export async function GET(req, { params }) {",
362
+ " const centrali = new CentraliSDK({ baseUrl, workspaceId, clientId, clientSecret });",
363
+ " const url = centrali.getFileRenderUrl(params.renderId);",
364
+ " const resp = await fetch(url, {",
365
+ " headers: { Authorization: `Bearer ${await centrali.getToken()}` }",
366
+ " });",
367
+ " return new Response(resp.body, { headers: { 'Content-Type': resp.headers.get('Content-Type') } });",
368
+ "}",
369
+ ].join("\n"),
343
370
  },
344
371
  download_url: {
345
372
  method: "client.getFileDownloadUrl(renderId)",
346
- note: "Returns a URL that triggers a file download.",
373
+ note: "Returns a URL that triggers a file download. Same auth rules as render URLs.",
347
374
  },
348
375
  tips: [
349
376
  "/root/shared always exists — upload there directly or create subfolders like /root/shared/logos",
350
377
  "Set isPublic=true for files that need to be accessible without auth (logos, avatars, public images)",
351
- "Store the renderId or full URL on a record field for easy retrieval",
378
+ "Store the renderId on a record field then use getFileRenderUrl() to build the URL when displaying",
352
379
  "Use image transformations for thumbnails instead of uploading multiple sizes",
380
+ "For private images in browser apps: either use a server-side proxy route or a publishable key with storage scope",
353
381
  ],
354
382
  },
355
383
  realtime: {
@@ -2188,23 +2216,29 @@ function registerDescribeTools(server) {
2188
2216
  createdAt: "string — ISO 8601 timestamp",
2189
2217
  },
2190
2218
  permission_model: {
2191
- description: "Permissions flow: Service Account Roles Permissions. Groups are optional bundles of SAs.",
2219
+ description: "Access control is attribute-based. The caller's groups, roles, and other attributes (from JWT claims) are evaluated against policy conditions using JSON-Logic.",
2220
+ evaluation_flow: "Request → extract groups/roles from JWT → find permissions for the requested resource + action → evaluate each permission's linked policy against caller attributes → first Allow wins, otherwise Deny",
2192
2221
  role: {
2193
- description: "A named bundle of permissions. Roles are assigned directly to SAs or inherited through groups.",
2194
- shape: {
2195
- id: "UUID",
2196
- name: "string (e.g., 'Data Reader', 'Compute Admin')",
2197
- description: "string | null",
2198
- permissions: "string[] — permission UUIDs",
2199
- },
2222
+ description: "A named label assigned to users and service accounts. Appears in JWT claims as an attribute. Policy conditions can check roles. Names are immutable after creation.",
2223
+ shape: { id: "UUID", name: "string — immutable", description: "string | null" },
2200
2224
  },
2201
2225
  group: {
2202
- description: "A named bundle of service accounts. Roles assigned to a group are inherited by all SAs in it.",
2203
- shape: {
2204
- id: "UUID",
2205
- name: "string (e.g., 'Backend Services')",
2206
- description: "string | null",
2207
- },
2226
+ description: "A named label for organizing users and service accounts. Appears in JWT claims. Policy conditions check group membership to decide access. Groups may also optionally appear on permissions. Names are immutable after creation.",
2227
+ shape: { id: "UUID", name: "string — immutable", description: "string | null" },
2228
+ },
2229
+ permission: {
2230
+ description: "Links a resource + actions to a policy. When a request comes in, the system finds permissions matching the resource + action, then evaluates each permission's linked policy against the caller's attributes.",
2231
+ shape: { id: "UUID", name: "string", resourceId: "UUID (from resources)", actions: "string[]", policyId: "UUID (from policies)", priority: "number" },
2232
+ },
2233
+ policy: {
2234
+ description: "A reusable set of JSON-Logic rules with an effect (Allow/Deny). Policy conditions check caller attributes like groups, roles, user_id, ip_address, time of day, etc. Example: 'Allow if caller is in group engineering'.",
2235
+ },
2236
+ resource: {
2237
+ description: "A protected thing in the system (e.g., 'workspace::records', 'workspace::functions'). Has a category and a list of valid actions.",
2238
+ },
2239
+ granting_access: {
2240
+ workflow: "1. Ensure the SA is in the right group → 2. Create a policy with conditions that match (e.g., group membership check) → 3. Create a permission linking the resource + actions + policy",
2241
+ shortcut: "Use generate_remediation + apply_remediation to auto-generate the correct policy + permission + group assignment for a denied action",
2208
2242
  },
2209
2243
  permission_actions: [
2210
2244
  "create — create new resources",
@@ -228,18 +228,18 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
228
228
  };
229
229
  }
230
230
  }));
231
- server.tool("delete_service_account", "Permanently delete a service account. This is irreversible — all tokens are invalidated immediately.", {
232
- serviceAccountId: zod_1.z.number().describe("The service account numeric ID to delete"),
233
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ serviceAccountId }) {
231
+ server.tool("delete_service_account", "Permanently delete a service account. This is irreversible — all tokens are invalidated immediately. Note: the service account must not be revoked (revoke prevents deletion).", {
232
+ clientId: zod_1.z.string().describe("The service account's clientId string (e.g., 'ci_abc123') — NOT the numeric ID"),
233
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ clientId }) {
234
234
  try {
235
- yield getSaClient().delete(`/${serviceAccountId}`);
235
+ yield getSaClient().delete(`/${clientId}`);
236
236
  return {
237
- content: [{ type: "text", text: `Service account '${serviceAccountId}' deleted successfully.` }],
237
+ content: [{ type: "text", text: `Service account '${clientId}' deleted successfully.` }],
238
238
  };
239
239
  }
240
240
  catch (error) {
241
241
  return {
242
- content: [{ type: "text", text: formatError(error, `deleting service account '${serviceAccountId}'`) }],
242
+ content: [{ type: "text", text: formatError(error, `deleting service account '${clientId}'`) }],
243
243
  isError: true,
244
244
  };
245
245
  }
@@ -553,7 +553,7 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
553
553
  }
554
554
  }));
555
555
  // ── Role CRUD ────────────────────────────────────────────────────
556
- server.tool("list_roles", "List all roles in the workspace. Roles bundle permissions that can be assigned to service accounts or groups.", {
556
+ server.tool("list_roles", "List all roles in the workspace. Roles are named labels assigned to users and service accounts.", {
557
557
  page: zod_1.z.number().optional().describe("Page number (default: 1)"),
558
558
  pageSize: zod_1.z.number().optional().describe("Results per page (default: 20)"),
559
559
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ page, pageSize }) {
@@ -591,17 +591,14 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
591
591
  };
592
592
  }
593
593
  }));
594
- server.tool("create_role", "Create a new role with a set of permissions. Permissions are UUIDs from the IAM permission registry. Use scan_service_account_permissions to discover available resource/action pairs.", {
595
- name: zod_1.z.string().describe("Role name (e.g., 'Data Reader', 'Compute Admin')"),
594
+ server.tool("create_role", "Create a new role. Roles are named labels assigned to users and service accounts. They do NOT contain permissions directly — to grant access, create a policy that targets the role as a principal. Role names are immutable after creation.", {
595
+ name: zod_1.z.string().describe("Role name (e.g., 'Data Reader', 'Compute Admin'). Cannot be changed after creation."),
596
596
  description: zod_1.z.string().optional().describe("Optional description of the role's purpose"),
597
- permissions: zod_1.z.array(zod_1.z.string()).optional().describe("Array of permission UUIDs to include in this role"),
598
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ name, description, permissions }) {
597
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ name, description }) {
599
598
  try {
600
599
  const input = { name };
601
600
  if (description !== undefined)
602
601
  input.description = description;
603
- if (permissions !== undefined)
604
- input.permissions = permissions;
605
602
  const result = yield getRolesClient().post("/", input);
606
603
  return {
607
604
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -614,20 +611,14 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
614
611
  };
615
612
  }
616
613
  }));
617
- server.tool("update_role", "Update a role's name, description, or permissions.", {
614
+ server.tool("update_role", "Update a role's description. Role names are immutable. Roles are named labels assigned to users/service accounts — they do NOT contain permissions directly. To grant access, create a policy that references the role.", {
618
615
  roleId: zod_1.z.string().describe("The role ID (UUID) to update"),
619
- name: zod_1.z.string().optional().describe("Updated name"),
620
616
  description: zod_1.z.string().optional().describe("Updated description"),
621
- permissions: zod_1.z.array(zod_1.z.string()).optional().describe("Updated permission UUIDs (replaces all existing)"),
622
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId, name, description, permissions }) {
617
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId, description }) {
623
618
  try {
624
619
  const input = {};
625
- if (name !== undefined)
626
- input.name = name;
627
620
  if (description !== undefined)
628
621
  input.description = description;
629
- if (permissions !== undefined)
630
- input.permissions = permissions;
631
622
  const result = yield getRolesClient().put(`/${roleId}`, input);
632
623
  return {
633
624
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -640,7 +631,7 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
640
631
  };
641
632
  }
642
633
  }));
643
- server.tool("delete_role", "Delete a role. Service accounts and groups that had this role lose its permissions.", {
634
+ server.tool("delete_role", "Delete a role. Service accounts assigned to this role will lose the label.", {
644
635
  roleId: zod_1.z.string().describe("The role ID (UUID) to delete"),
645
636
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId }) {
646
637
  try {
@@ -715,15 +706,12 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
715
706
  };
716
707
  }
717
708
  }));
718
- server.tool("update_group", "Update a group's name or description.", {
709
+ server.tool("update_group", "Update a group's description. Group names are immutable and cannot be changed after creation.", {
719
710
  groupId: zod_1.z.string().describe("The group ID (UUID) to update"),
720
- name: zod_1.z.string().optional().describe("Updated name"),
721
711
  description: zod_1.z.string().optional().describe("Updated description"),
722
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ groupId, name, description }) {
712
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ groupId, description }) {
723
713
  try {
724
714
  const input = {};
725
- if (name !== undefined)
726
- input.name = name;
727
715
  if (description !== undefined)
728
716
  input.description = description;
729
717
  const result = yield getGroupsClient().put(`/${groupId}`, input);
@@ -940,7 +928,7 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
940
928
  }
941
929
  }));
942
930
  server.tool("create_permission", "Create a new permission definition. Permissions bind actions to a resource within a policy. Required fields: name, resourceId (UUID from list_resources), actions (string array), policyId (UUID from list_policies or create_policy).", {
943
- permission: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).describe("Required: { name: string, resourceId: UUID, actions: string[], policyId: UUID }. Optional: description, groups (string[]), priority (number)."),
931
+ permission: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).describe("Required: { name: string, resourceId: UUID, actions: string[], policyId: UUID }. Optional: description, priority (number)."),
944
932
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ permission }) {
945
933
  try {
946
934
  const result = yield getPermissionsClient().post("/", permission);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "4.4.8-rc.7",
3
+ "version": "4.4.8-rc.9",
4
4
  "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -346,18 +346,46 @@ export function registerDescribeTools(server: McpServer) {
346
346
  },
347
347
  render_url: {
348
348
  method: "client.getFileRenderUrl(renderId, options?)",
349
- note: "Returns a URL that serves the file. Supports image transformations: width, height, fit ('cover'|'contain'|'fill'), format ('webp'|'png'|'jpeg'), quality (1-100).",
350
- example: "const url = centrali.getFileRenderUrl(renderId, { width: 200, height: 200, fit: 'cover', format: 'webp' });",
349
+ important: "uploadFile() returns a renderId (string), NOT a URL. You MUST call getFileRenderUrl(renderId) to construct the actual URL for display.",
350
+ url_pattern: "https://api.{domain}/storage/ws/{workspaceId}/api/v1/render/{renderId}",
351
+ transforms: "Optional image transformations: width, height, fit ('cover'|'contain'|'fill'), format ('webp'|'png'|'jpeg'), quality (1-100).",
352
+ public_vs_private: {
353
+ public: "If uploaded with isPublic=true, the render URL works without any auth — use directly in <img> tags.",
354
+ private: "If uploaded with isPublic=false (default), the render URL requires authentication. Pass a Bearer token (service account) or x-api-key (publishable key) in the request headers. For browser rendering of private images, use a server-side proxy route or a publishable key.",
355
+ },
356
+ example: [
357
+ "// Upload returns renderId, NOT a URL",
358
+ "const { data: renderId } = await centrali.uploadFile(file, '/root/shared/logos', true);",
359
+ "",
360
+ "// Build the render URL from the renderId",
361
+ "const url = centrali.getFileRenderUrl(renderId);",
362
+ "// => 'https://api.centrali.io/storage/ws/my-workspace/api/v1/render/abc123'",
363
+ "",
364
+ "// With image transforms (thumbnail)",
365
+ "const thumbUrl = centrali.getFileRenderUrl(renderId, { width: 200, height: 200, fit: 'cover', format: 'webp' });",
366
+ "",
367
+ "// For private images in Next.js — proxy through an API route",
368
+ "// app/api/image/[renderId]/route.ts",
369
+ "export async function GET(req, { params }) {",
370
+ " const centrali = new CentraliSDK({ baseUrl, workspaceId, clientId, clientSecret });",
371
+ " const url = centrali.getFileRenderUrl(params.renderId);",
372
+ " const resp = await fetch(url, {",
373
+ " headers: { Authorization: `Bearer ${await centrali.getToken()}` }",
374
+ " });",
375
+ " return new Response(resp.body, { headers: { 'Content-Type': resp.headers.get('Content-Type') } });",
376
+ "}",
377
+ ].join("\n"),
351
378
  },
352
379
  download_url: {
353
380
  method: "client.getFileDownloadUrl(renderId)",
354
- note: "Returns a URL that triggers a file download.",
381
+ note: "Returns a URL that triggers a file download. Same auth rules as render URLs.",
355
382
  },
356
383
  tips: [
357
384
  "/root/shared always exists — upload there directly or create subfolders like /root/shared/logos",
358
385
  "Set isPublic=true for files that need to be accessible without auth (logos, avatars, public images)",
359
- "Store the renderId or full URL on a record field for easy retrieval",
386
+ "Store the renderId on a record field then use getFileRenderUrl() to build the URL when displaying",
360
387
  "Use image transformations for thumbnails instead of uploading multiple sizes",
388
+ "For private images in browser apps: either use a server-side proxy route or a publishable key with storage scope",
361
389
  ],
362
390
  },
363
391
  realtime: {
@@ -2485,23 +2513,29 @@ export function registerDescribeTools(server: McpServer) {
2485
2513
  createdAt: "string — ISO 8601 timestamp",
2486
2514
  },
2487
2515
  permission_model: {
2488
- description: "Permissions flow: Service Account Roles Permissions. Groups are optional bundles of SAs.",
2516
+ description: "Access control is attribute-based. The caller's groups, roles, and other attributes (from JWT claims) are evaluated against policy conditions using JSON-Logic.",
2517
+ evaluation_flow: "Request → extract groups/roles from JWT → find permissions for the requested resource + action → evaluate each permission's linked policy against caller attributes → first Allow wins, otherwise Deny",
2489
2518
  role: {
2490
- description: "A named bundle of permissions. Roles are assigned directly to SAs or inherited through groups.",
2491
- shape: {
2492
- id: "UUID",
2493
- name: "string (e.g., 'Data Reader', 'Compute Admin')",
2494
- description: "string | null",
2495
- permissions: "string[] — permission UUIDs",
2496
- },
2519
+ description: "A named label assigned to users and service accounts. Appears in JWT claims as an attribute. Policy conditions can check roles. Names are immutable after creation.",
2520
+ shape: { id: "UUID", name: "string — immutable", description: "string | null" },
2497
2521
  },
2498
2522
  group: {
2499
- description: "A named bundle of service accounts. Roles assigned to a group are inherited by all SAs in it.",
2500
- shape: {
2501
- id: "UUID",
2502
- name: "string (e.g., 'Backend Services')",
2503
- description: "string | null",
2504
- },
2523
+ description: "A named label for organizing users and service accounts. Appears in JWT claims. Policy conditions check group membership to decide access. Groups may also optionally appear on permissions. Names are immutable after creation.",
2524
+ shape: { id: "UUID", name: "string — immutable", description: "string | null" },
2525
+ },
2526
+ permission: {
2527
+ description: "Links a resource + actions to a policy. When a request comes in, the system finds permissions matching the resource + action, then evaluates each permission's linked policy against the caller's attributes.",
2528
+ shape: { id: "UUID", name: "string", resourceId: "UUID (from resources)", actions: "string[]", policyId: "UUID (from policies)", priority: "number" },
2529
+ },
2530
+ policy: {
2531
+ description: "A reusable set of JSON-Logic rules with an effect (Allow/Deny). Policy conditions check caller attributes like groups, roles, user_id, ip_address, time of day, etc. Example: 'Allow if caller is in group engineering'.",
2532
+ },
2533
+ resource: {
2534
+ description: "A protected thing in the system (e.g., 'workspace::records', 'workspace::functions'). Has a category and a list of valid actions.",
2535
+ },
2536
+ granting_access: {
2537
+ workflow: "1. Ensure the SA is in the right group → 2. Create a policy with conditions that match (e.g., group membership check) → 3. Create a permission linking the resource + actions + policy",
2538
+ shortcut: "Use generate_remediation + apply_remediation to auto-generate the correct policy + permission + group assignment for a denied action",
2505
2539
  },
2506
2540
  permission_actions: [
2507
2541
  "create — create new resources",
@@ -261,19 +261,19 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
261
261
 
262
262
  server.tool(
263
263
  "delete_service_account",
264
- "Permanently delete a service account. This is irreversible — all tokens are invalidated immediately.",
264
+ "Permanently delete a service account. This is irreversible — all tokens are invalidated immediately. Note: the service account must not be revoked (revoke prevents deletion).",
265
265
  {
266
- serviceAccountId: z.number().describe("The service account numeric ID to delete"),
266
+ clientId: z.string().describe("The service account's clientId string (e.g., 'ci_abc123') — NOT the numeric ID"),
267
267
  },
268
- async ({ serviceAccountId }) => {
268
+ async ({ clientId }) => {
269
269
  try {
270
- await getSaClient().delete(`/${serviceAccountId}`);
270
+ await getSaClient().delete(`/${clientId}`);
271
271
  return {
272
- content: [{ type: "text", text: `Service account '${serviceAccountId}' deleted successfully.` }],
272
+ content: [{ type: "text", text: `Service account '${clientId}' deleted successfully.` }],
273
273
  };
274
274
  } catch (error: unknown) {
275
275
  return {
276
- content: [{ type: "text", text: formatError(error, `deleting service account '${serviceAccountId}'`) }],
276
+ content: [{ type: "text", text: formatError(error, `deleting service account '${clientId}'`) }],
277
277
  isError: true,
278
278
  };
279
279
  }
@@ -682,7 +682,7 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
682
682
 
683
683
  server.tool(
684
684
  "list_roles",
685
- "List all roles in the workspace. Roles bundle permissions that can be assigned to service accounts or groups.",
685
+ "List all roles in the workspace. Roles are named labels assigned to users and service accounts.",
686
686
  {
687
687
  page: z.number().optional().describe("Page number (default: 1)"),
688
688
  pageSize: z.number().optional().describe("Results per page (default: 20)"),
@@ -728,17 +728,15 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
728
728
 
729
729
  server.tool(
730
730
  "create_role",
731
- "Create a new role with a set of permissions. Permissions are UUIDs from the IAM permission registry. Use scan_service_account_permissions to discover available resource/action pairs.",
731
+ "Create a new role. Roles are named labels assigned to users and service accounts. They do NOT contain permissions directly — to grant access, create a policy that targets the role as a principal. Role names are immutable after creation.",
732
732
  {
733
- name: z.string().describe("Role name (e.g., 'Data Reader', 'Compute Admin')"),
733
+ name: z.string().describe("Role name (e.g., 'Data Reader', 'Compute Admin'). Cannot be changed after creation."),
734
734
  description: z.string().optional().describe("Optional description of the role's purpose"),
735
- permissions: z.array(z.string()).optional().describe("Array of permission UUIDs to include in this role"),
736
735
  },
737
- async ({ name, description, permissions }) => {
736
+ async ({ name, description }) => {
738
737
  try {
739
738
  const input: Record<string, any> = { name };
740
739
  if (description !== undefined) input.description = description;
741
- if (permissions !== undefined) input.permissions = permissions;
742
740
  const result = await getRolesClient().post("/", input);
743
741
  return {
744
742
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -754,19 +752,15 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
754
752
 
755
753
  server.tool(
756
754
  "update_role",
757
- "Update a role's name, description, or permissions.",
755
+ "Update a role's description. Role names are immutable. Roles are named labels assigned to users/service accounts — they do NOT contain permissions directly. To grant access, create a policy that references the role.",
758
756
  {
759
757
  roleId: z.string().describe("The role ID (UUID) to update"),
760
- name: z.string().optional().describe("Updated name"),
761
758
  description: z.string().optional().describe("Updated description"),
762
- permissions: z.array(z.string()).optional().describe("Updated permission UUIDs (replaces all existing)"),
763
759
  },
764
- async ({ roleId, name, description, permissions }) => {
760
+ async ({ roleId, description }) => {
765
761
  try {
766
762
  const input: Record<string, any> = {};
767
- if (name !== undefined) input.name = name;
768
763
  if (description !== undefined) input.description = description;
769
- if (permissions !== undefined) input.permissions = permissions;
770
764
  const result = await getRolesClient().put(`/${roleId}`, input);
771
765
  return {
772
766
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -782,7 +776,7 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
782
776
 
783
777
  server.tool(
784
778
  "delete_role",
785
- "Delete a role. Service accounts and groups that had this role lose its permissions.",
779
+ "Delete a role. Service accounts assigned to this role will lose the label.",
786
780
  {
787
781
  roleId: z.string().describe("The role ID (UUID) to delete"),
788
782
  },
@@ -875,16 +869,14 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
875
869
 
876
870
  server.tool(
877
871
  "update_group",
878
- "Update a group's name or description.",
872
+ "Update a group's description. Group names are immutable and cannot be changed after creation.",
879
873
  {
880
874
  groupId: z.string().describe("The group ID (UUID) to update"),
881
- name: z.string().optional().describe("Updated name"),
882
875
  description: z.string().optional().describe("Updated description"),
883
876
  },
884
- async ({ groupId, name, description }) => {
877
+ async ({ groupId, description }) => {
885
878
  try {
886
879
  const input: Record<string, any> = {};
887
- if (name !== undefined) input.name = name;
888
880
  if (description !== undefined) input.description = description;
889
881
  const result = await getGroupsClient().put(`/${groupId}`, input);
890
882
  return {
@@ -1168,7 +1160,7 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
1168
1160
  "create_permission",
1169
1161
  "Create a new permission definition. Permissions bind actions to a resource within a policy. Required fields: name, resourceId (UUID from list_resources), actions (string array), policyId (UUID from list_policies or create_policy).",
1170
1162
  {
1171
- permission: z.record(z.string(), z.any()).describe("Required: { name: string, resourceId: UUID, actions: string[], policyId: UUID }. Optional: description, groups (string[]), priority (number)."),
1163
+ permission: z.record(z.string(), z.any()).describe("Required: { name: string, resourceId: UUID, actions: string[], policyId: UUID }. Optional: description, priority (number)."),
1172
1164
  },
1173
1165
  async ({ permission }) => {
1174
1166
  try {