@centrali-io/centrali-mcp 4.4.8-rc.8 → 4.4.8

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",
@@ -2276,9 +2310,9 @@ function registerDescribeTools(server) {
2276
2310
  },
2277
2311
  roles: {
2278
2312
  list_roles: "List all roles",
2279
- get_role: "Get role details with permissions",
2280
- create_role: "Create role with permissions",
2281
- update_role: "Update role name/description/permissions",
2313
+ get_role: "Get role details",
2314
+ create_role: "Create a named role (names are immutable)",
2315
+ update_role: "Update role description",
2282
2316
  delete_role: "Delete role",
2283
2317
  list_service_account_roles: "List roles assigned to a SA",
2284
2318
  assign_role_to_service_account: "Assign role to SA",
@@ -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,17 +611,14 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
614
611
  };
615
612
  }
616
613
  }));
617
- server.tool("update_role", "Update a role's description or permissions. Role names are immutable and cannot be changed after creation.", {
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
616
  description: zod_1.z.string().optional().describe("Updated description"),
620
- permissions: zod_1.z.array(zod_1.z.string()).optional().describe("Updated permission UUIDs (replaces all existing)"),
621
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId, description, permissions }) {
617
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId, description }) {
622
618
  try {
623
619
  const input = {};
624
620
  if (description !== undefined)
625
621
  input.description = description;
626
- if (permissions !== undefined)
627
- input.permissions = permissions;
628
622
  const result = yield getRolesClient().put(`/${roleId}`, input);
629
623
  return {
630
624
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -637,7 +631,7 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
637
631
  };
638
632
  }
639
633
  }));
640
- 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.", {
641
635
  roleId: zod_1.z.string().describe("The role ID (UUID) to delete"),
642
636
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ roleId }) {
643
637
  try {
@@ -934,7 +928,7 @@ function registerServiceAccountTools(server, sdk, centraliUrl, workspaceId, ownC
934
928
  }
935
929
  }));
936
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).", {
937
- 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)."),
938
932
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ permission }) {
939
933
  try {
940
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.8",
3
+ "version": "4.4.8",
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",
@@ -2573,9 +2607,9 @@ export function registerDescribeTools(server: McpServer) {
2573
2607
  },
2574
2608
  roles: {
2575
2609
  list_roles: "List all roles",
2576
- get_role: "Get role details with permissions",
2577
- create_role: "Create role with permissions",
2578
- update_role: "Update role name/description/permissions",
2610
+ get_role: "Get role details",
2611
+ create_role: "Create a named role (names are immutable)",
2612
+ update_role: "Update role description",
2579
2613
  delete_role: "Delete role",
2580
2614
  list_service_account_roles: "List roles assigned to a SA",
2581
2615
  assign_role_to_service_account: "Assign role to SA",
@@ -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,17 +752,15 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
754
752
 
755
753
  server.tool(
756
754
  "update_role",
757
- "Update a role's description or permissions. Role names are immutable and cannot be changed after creation.",
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
758
  description: z.string().optional().describe("Updated description"),
761
- permissions: z.array(z.string()).optional().describe("Updated permission UUIDs (replaces all existing)"),
762
759
  },
763
- async ({ roleId, description, permissions }) => {
760
+ async ({ roleId, description }) => {
764
761
  try {
765
762
  const input: Record<string, any> = {};
766
763
  if (description !== undefined) input.description = description;
767
- if (permissions !== undefined) input.permissions = permissions;
768
764
  const result = await getRolesClient().put(`/${roleId}`, input);
769
765
  return {
770
766
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
@@ -780,7 +776,7 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
780
776
 
781
777
  server.tool(
782
778
  "delete_role",
783
- "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.",
784
780
  {
785
781
  roleId: z.string().describe("The role ID (UUID) to delete"),
786
782
  },
@@ -1164,7 +1160,7 @@ export function registerServiceAccountTools(server: McpServer, sdk: CentraliSDK,
1164
1160
  "create_permission",
1165
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).",
1166
1162
  {
1167
- 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)."),
1168
1164
  },
1169
1165
  async ({ permission }) => {
1170
1166
  try {