@centrali-io/centrali-mcp 4.4.5 → 4.4.7

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.
@@ -82,6 +82,10 @@ function registerDescribeTools(server) {
82
82
  "list_allowed_domains",
83
83
  "add_allowed_domain",
84
84
  "remove_allowed_domain",
85
+ "get_function_run",
86
+ "list_function_runs",
87
+ "get_compute_job_status",
88
+ "invoke_endpoint",
85
89
  ],
86
90
  },
87
91
  smart_queries: {
@@ -161,8 +165,129 @@ function registerDescribeTools(server) {
161
165
  "accept_page_proposal",
162
166
  ],
163
167
  },
168
+ auth_providers: {
169
+ summary: "External identity provider configuration for BYOT (Bring Your Own Token). Connect Clerk, Auth0, Okta, Keycloak, or any OIDC provider so your app's users can authenticate with their existing IdP and Centrali handles authorization via claim-based policies.",
170
+ describeWith: "describe_auth_providers",
171
+ tools: [
172
+ "list_auth_providers",
173
+ "get_auth_provider",
174
+ "create_auth_provider",
175
+ "update_auth_provider",
176
+ "delete_auth_provider",
177
+ "test_auth_provider",
178
+ "refresh_auth_provider_jwks",
179
+ ],
180
+ },
181
+ service_accounts: {
182
+ summary: "Machine identities for backend-to-backend API access. Create service accounts with client_credentials OAuth2 flow, manage roles and groups for fine-grained permissions, and introspect access with permission scanning.",
183
+ describeWith: "describe_service_accounts",
184
+ tools: [
185
+ "list_service_accounts",
186
+ "get_service_account",
187
+ "create_service_account",
188
+ "update_service_account_name",
189
+ "update_service_account_description",
190
+ "delete_service_account",
191
+ "rotate_service_account_secret",
192
+ "revoke_service_account",
193
+ "generate_dev_token",
194
+ "scan_service_account_permissions",
195
+ "simulate_service_account_permission",
196
+ "generate_remediation",
197
+ "preview_remediation",
198
+ "apply_remediation",
199
+ "list_service_account_roles",
200
+ "assign_role_to_service_account",
201
+ "remove_role_from_service_account",
202
+ "list_service_account_groups",
203
+ "add_service_account_to_group",
204
+ "remove_service_account_from_group",
205
+ "list_roles",
206
+ "get_role",
207
+ "create_role",
208
+ "update_role",
209
+ "delete_role",
210
+ "list_groups",
211
+ "get_group",
212
+ "create_group",
213
+ "update_group",
214
+ "delete_group",
215
+ "list_publishable_keys",
216
+ "get_publishable_key",
217
+ "create_publishable_key",
218
+ "update_publishable_key",
219
+ "revoke_publishable_key",
220
+ "get_current_identity",
221
+ ],
222
+ },
223
+ },
224
+ naming_guide: {
225
+ description: "Different tools use different parameter names for the same concept (collection identifier). This is a historical naming drift — all of these refer to the same thing.",
226
+ aliases: {
227
+ recordSlug: "Used by record tools (query_records, create_record, etc.). This is the collection's URL-safe slug, e.g., 'orders'.",
228
+ structureSlug: "Used by validation and insights tools. Same value as recordSlug.",
229
+ collections: "Used by search_records. Same value as recordSlug. Accepts a string or array of strings.",
230
+ structureIds: "Used by generate_starter_pages. This is the collection UUID (not the slug). Get it from list_collections → id field.",
231
+ },
232
+ rule: "When a tool asks for recordSlug, structureSlug, or collections — use the collection's slug (e.g., 'orders'). When a tool asks for structureIds — use the collection's UUID.",
233
+ },
234
+ workflow: "Typical workflow: 1) Define collections → 2) Create records → 3) Write compute functions → 4) Wire orchestrations → 5) Build pages to surface data → 6) Publish pages for end users. When building an app, also: 7) Create a service account → 8) Grant least-privilege permissions via remediation → 9) Create publishable keys for the frontend.",
235
+ app_credential_setup: {
236
+ description: "When building an app that uses Centrali, you need credentials. The flow depends on whether the code runs server-side or client-side.",
237
+ backend: {
238
+ description: "Server-side apps (Next.js API routes, Node.js services) use service accounts.",
239
+ flow: [
240
+ "1. create_service_account → get clientId + clientSecret",
241
+ "2. scan_service_account_permissions(filter='denied') → see what's missing",
242
+ "3. generate_remediation(sa, resource, category, actions) → get options to grant access",
243
+ "4. Pick the LEAST-PRIVILEGE option (prefer 'create_new' for exact permissions over broad role assignments)",
244
+ "5. apply_remediation → grant access",
245
+ "6. Use clientId/clientSecret in the SDK: new CentraliSDK({ clientId, clientSecret, ... })",
246
+ ],
247
+ principle: "ALWAYS grant least privilege. Only grant the exact resources and actions the app needs. Never assign a broad admin role when a narrow permission will do.",
248
+ },
249
+ frontend: {
250
+ description: "Client-side apps (React, Vue, mobile) use publishable keys — safe to embed in code.",
251
+ flow: [
252
+ "1. create_publishable_key with scoped access (e.g., records:list:products, records:retrieve:products)",
253
+ "2. Use the key in the SDK: new CentraliSDK({ publishableKey: 'pk_live_...', ... })",
254
+ ],
255
+ principle: "Publishable keys are already scoped. Only grant the specific collections and actions the UI needs. Write actions (create, execute) require explicit collection slugs — no wildcards.",
256
+ },
257
+ },
258
+ docs: "Full documentation: https://docs.centrali.io — SDK guide, API reference, compute functions, orchestrations, and more.",
259
+ feature_matrix: {
260
+ description: "Not all features are available through all interfaces. Use this matrix to know which tool to use.",
261
+ capabilities: {
262
+ "Collections CRUD": { sdk: true, mcp: true, console: true },
263
+ "Records CRUD": { sdk: true, mcp: true, console: true },
264
+ "Compute functions": { sdk: true, mcp: true, console: true },
265
+ "Triggers": { sdk: true, mcp: true, console: true },
266
+ "Smart queries": { sdk: true, mcp: true, console: true },
267
+ "Orchestrations": { sdk: true, mcp: true, console: true },
268
+ "Pages": { sdk: "partial", mcp: true, console: true },
269
+ "Realtime subscriptions (SSE)": { sdk: true, mcp: false, console: false, note: "SDK-only — use client.realtime.subscribe() in app code" },
270
+ "Validation scans": { sdk: true, mcp: true, console: true },
271
+ "Anomaly insights": { sdk: true, mcp: true, console: true },
272
+ "Search": { sdk: true, mcp: true, console: true },
273
+ "Function runs (execution history)": { sdk: true, mcp: true, console: true },
274
+ "Service accounts & IAM": { sdk: false, mcp: true, console: true, note: "MCP tools for full SA lifecycle, roles, groups, and permission introspection" },
275
+ "File uploads": { sdk: true, mcp: false, console: true, note: "SDK-only — use client.uploadFile() in app code" },
276
+ },
277
+ },
278
+ sdk_integration: {
279
+ description: "When building an app, use MCP tools to create backend resources (collections, functions, triggers, orchestrations) and the SDK in your app code to interact with them at runtime.",
280
+ install: "npm install @centrali-io/centrali-sdk",
281
+ nextjs_server: {
282
+ description: "For Next.js API routes and server components, use service account auth.",
283
+ code: "import { CentraliSDK } from '@centrali-io/centrali-sdk';\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', clientId: process.env.CENTRALI_CLIENT_ID, clientSecret: process.env.CENTRALI_CLIENT_SECRET });",
284
+ },
285
+ react_vite_client: {
286
+ description: "For React + Vite browser apps, use a publishable key (safe to expose in client code).",
287
+ code: "import { CentraliSDK } from '@centrali-io/centrali-sdk';\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', publishableKey: 'pk_live_your_key_here' });",
288
+ },
289
+ note: "workspaceId is the workspace slug (e.g., 'acme'), not a UUID. See https://docs.centrali.io/guides/centrali-sdk for the full guide.",
164
290
  },
165
- workflow: "Typical workflow: 1) Define collections → 2) Create records → 3) Write compute functions → 4) Wire orchestrations → 5) Build pages to surface data → 6) Publish pages for end users.",
166
291
  }, null, 2),
167
292
  },
168
293
  ],
@@ -408,13 +533,29 @@ function registerDescribeTools(server) {
408
533
  description: "Triggered by an external HTTP POST to a generated webhook URL.",
409
534
  config: "Each http-trigger gets a unique URL to share with external services",
410
535
  },
536
+ "endpoint": {
537
+ description: "Turns a compute function into a custom API endpoint. Unlike all other trigger types (which are async and return a job ID you must poll), endpoint triggers WAIT for the function to complete and return its output directly in the HTTP response. Max execution time: 30 seconds.",
538
+ when_to_use: "Use endpoint triggers when the caller needs the function's output immediately (REST APIs, form handlers, webhook responders, data calculations). Use on-demand/invoke_trigger for long-running work (>30s) or fire-and-forget background jobs.",
539
+ config: {
540
+ path: "string — URL-safe path (e.g., 'create-order', 'webhook/shipments'). Must be unique per workspace.",
541
+ allowedMethods: "string[] — HTTP methods to accept (default: ['POST']). Options: GET, POST, PUT, DELETE, PATCH.",
542
+ timeoutMs: "number — execution timeout 1000-30000ms (default: 30000). Function MUST complete within this window or the request returns 504 Gateway Timeout.",
543
+ auth: "{ mode: 'bearer'|'public'|'apiKey'|'hmac' } — authentication mode. bearer=IAM token, public=no auth, apiKey=X-API-Key header (auto-generated), hmac=X-Signature header (auto-generated signing secret).",
544
+ },
545
+ invocation: "Call invoke_endpoint with the path. Response comes back inline — no polling needed.",
546
+ example_use_cases: [
547
+ "Build a REST API endpoint backed by a compute function",
548
+ "Create a webhook receiver that processes and responds synchronously",
549
+ "Expose a function as an HTTP service for external integrations",
550
+ ],
551
+ },
411
552
  },
412
553
  trigger_shape: {
413
554
  id: "UUID",
414
555
  name: "string",
415
556
  description: "string | null",
416
557
  functionId: "UUID — the compute function to execute",
417
- executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger'",
558
+ executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger' | 'endpoint'",
418
559
  triggerMetadata: "object — type-specific configuration (event, cron, params, etc.)",
419
560
  enabled: "boolean — whether the trigger is active (default: true)",
420
561
  workspaceSlug: "string",
@@ -461,6 +602,7 @@ function registerDescribeTools(server) {
461
602
  triggerMetadata_examples: {
462
603
  "event-driven": { eventType: "record_created", recordSlug: "orders" },
463
604
  scheduled: { scheduleType: "cron", cronExpression: "0 9 * * *", timezone: "America/New_York" },
605
+ endpoint: { path: "create-order", allowedMethods: ["POST"], timeoutMs: 10000, auth: { mode: "bearer" } },
464
606
  },
465
607
  },
466
608
  update_trigger: {
@@ -505,26 +647,93 @@ function registerDescribeTools(server) {
505
647
  wildcards: "Use '*.example.com' to allow all subdomains of example.com",
506
648
  note: "Functions can only make HTTP requests to domains on this list. Add domains before creating functions that call external APIs.",
507
649
  },
650
+ compute_input_contract: {
651
+ description: "Every compute function has three globals: api, triggerParams, executionParams. What they contain depends on how the function was invoked.",
652
+ signature: "async function run() { /* use api, triggerParams, executionParams */ return result; }",
653
+ important: "Do NOT use module.exports. Always use 'async function run() { ... }'.",
654
+ per_trigger_type: {
655
+ "http-trigger": {
656
+ triggerParams: "Static params from trigger.triggerMetadata.params (set at trigger creation time)",
657
+ executionParams: "{ payload } — parsed request body from the incoming HTTP POST",
658
+ },
659
+ "endpoint": {
660
+ triggerParams: "Static params from trigger.triggerMetadata.params",
661
+ executionParams: "{ payload, method, headers, query } — full HTTP request context",
662
+ },
663
+ "scheduled": {
664
+ triggerParams: "Static params from trigger.triggerMetadata.params (max 64KB)",
665
+ executionParams: "{} — empty (no request context for scheduled runs)",
666
+ },
667
+ "on-demand": {
668
+ triggerParams: "Static params from trigger.triggerMetadata.params",
669
+ executionParams: "{ payload } — the payload passed to invoke_trigger",
670
+ },
671
+ "pages-action": {
672
+ triggerParams: "{ input, token } — from page action invocation",
673
+ executionParams: "{} — empty",
674
+ },
675
+ "orchestration-step": {
676
+ triggerParams: "Merged: orchestration input + previous step outputs + decrypted encrypted params",
677
+ executionParams: "Not used — all input arrives via triggerParams for orchestration steps",
678
+ },
679
+ },
680
+ },
508
681
  sandbox_api: {
509
- description: "Functions receive an `api` object with built-in utilities. Key crypto methods:",
682
+ description: "Functions receive an `api` object with built-in utilities.",
683
+ records: "api.queryRecords, api.fetchRecord, api.createRecord, api.updateRecord, api.deleteRecord, api.bulkCreateRecords, api.bulkUpdateRecords, api.bulkDeleteRecords",
684
+ http: "api.httpRequest, api.httpFetch — outbound calls to allowed domains only",
685
+ files: "api.storeFile, api.storeAsCSV, api.storeAsJSON",
510
686
  crypto: {
511
687
  "api.crypto.sha256(data)": "SHA256 hash → base64 string",
512
688
  "api.crypto.hmacSha256(key, data, options?)": "HMAC-SHA256 → base64 string. options: { keyEncoding: 'utf8' | 'base64' }",
513
689
  "api.crypto.rsaSign(privateKeyPem, data, algorithm?)": "RSA signature → base64url string. algorithm: 'RS256' (default), 'RS384', 'RS512'",
514
690
  "api.crypto.signJwt(privateKeyPem, payload, options?)": "Build + sign a JWT → 'header.payload.signature'. options: { algorithm, expiresIn (seconds), issuer }",
515
691
  },
516
- note: "Private keys should be stored as encrypted trigger params. signJwt automatically sets iat and supports RS256/RS384/RS512.",
692
+ utilities: "api.uuid, api.formatDate, api.chunk, api.merge, api.math, api.evaluate, api.renderTemplate, api.log, api.logError, api.toCSV, api.toJSON",
693
+ },
694
+ secrets: {
695
+ description: "Compute functions have NO built-in environment variables or secrets field.",
696
+ recommended: "Wrap the function in an orchestration and use encrypted params on the compute step. Encrypted params are stored with AES-256-GCM at rest, decrypted at execution time, and arrive in triggerParams.",
697
+ alternative: "Pass secrets in the trigger invocation payload via executionParams. This works but means the calling app is the courier for the secret.",
698
+ encrypted_params_flow: "Create orchestration → add compute step with encryptedParams: { API_KEY: { value: 'secret', encrypt: true } } → at execution, API_KEY arrives decrypted in triggerParams.API_KEY",
699
+ },
700
+ async_execution: {
701
+ description: "Trigger invocation is ASYNCHRONOUS. invoke_trigger returns a job ID, not the execution result. There are two ways to get the result.",
702
+ path_1_realtime: {
703
+ description: "SDK realtime subscription (recommended for apps). Subscribe to run status events via SSE — no polling needed. The SDK handles reconnection automatically.",
704
+ when_to_use: "In your app code (Next.js, React, Node.js server). Best for dashboards and UIs that need to show run progress in real-time.",
705
+ how: "Use client.realtime.subscribe() in your app to listen for function run completion events keyed by the jobId.",
706
+ note: "Realtime is SDK-only — not available through MCP.",
707
+ },
708
+ path_2_polling: {
709
+ description: "Poll with get_compute_job_status(jobId) (recommended for MCP and scripts).",
710
+ when_to_use: "In MCP tool calls, CLI scripts, or any context where you can't hold a realtime connection.",
711
+ flow: "1) Call invoke_trigger → get jobId. 2) Call get_compute_job_status(jobId) to check state. 3) Repeat until state is 'completed' or 'failed'.",
712
+ },
713
+ job_statuses: "queued → running → completed | failed",
714
+ tools: {
715
+ get_compute_job_status: "Poll by job ID — returns state, returnValue (on success), failedReason (on failure). This is the direct way to check a specific invocation.",
716
+ get_function_run: "Get a function run by its run UUID — returns detailed status, output (runData), errors, timing, memory/CPU usage",
717
+ list_function_runs: "List runs by triggerId or functionId — useful for browsing execution history",
718
+ },
719
+ tip: "For orchestration workflows, use get_orchestration_run(includeSteps=true) instead — it shows step-by-step execution with inputs/outputs.",
720
+ },
721
+ success_vs_failure: {
722
+ description: "Compute functions can fail in two ways. The distinction matters for debugging.",
723
+ throw: "Throwing an error → run status = 'failure', errorMessage populated. Use for unexpected errors.",
724
+ return_error: "Returning { ok: false, ... } → run status = 'completed', but the business action failed. Use for expected failures (e.g., provider returned 403).",
725
+ recommendation: "For provider-backed calls (Resend, Stripe, etc.), check the response status and return { ok: false, status, body } so the caller can diagnose. Only throw for truly unexpected errors.",
517
726
  },
518
727
  tips: [
519
728
  "Use list_functions to see all available functions",
520
729
  "Use list_triggers to see how functions are wired to execution events",
521
730
  "Only on-demand triggers can be invoked via invoke_trigger — other types fire automatically",
522
- "Functions receive the trigger payload in their execution context",
523
- "Use pause_trigger/resume_trigger to temporarily disable triggers without deleting them",
524
731
  "Use test_function to validate code before creating or updating a function",
525
732
  "Use create_function + create_trigger together to set up a complete compute pipeline",
733
+ "Use get_function_run or list_function_runs to check execution results after invoking a trigger",
526
734
  "Use api.crypto.signJwt() for GitHub App, Google Cloud, or Azure AD authentication flows",
527
735
  "Before creating functions that call external APIs, add the target domains with add_allowed_domain",
736
+ "For secrets (API keys, credentials), use orchestration encrypted params — see the 'secrets' section above",
528
737
  ],
529
738
  }, null, 2),
530
739
  },
@@ -788,7 +997,7 @@ function registerDescribeTools(server) {
788
997
  id: "UUID",
789
998
  structureSlug: "string — the collection where the anomaly was detected",
790
999
  type: "string — anomaly type (e.g., 'spike', 'drop', 'outlier', 'pattern_break')",
791
- severity: "'critical' | 'high' | 'medium' | 'low'",
1000
+ severity: "'info' | 'warning' | 'critical'",
792
1001
  status: "'active' | 'acknowledged' | 'dismissed'",
793
1002
  title: "string — human-readable summary",
794
1003
  description: "string — detailed explanation of the anomaly",
@@ -805,12 +1014,12 @@ function registerDescribeTools(server) {
805
1014
  totalActive: "number",
806
1015
  totalAcknowledged: "number",
807
1016
  totalDismissed: "number",
808
- bySeverity: "{ critical: n, high: n, medium: n, low: n }",
1017
+ bySeverity: "{ info: n, warning: n, critical: n }",
809
1018
  },
810
1019
  tips: [
811
1020
  "Use trigger_anomaly_analysis to scan a specific collection on-demand",
812
1021
  "Use get_insights_summary for a quick overview before diving into individual insights",
813
- "Filter by severity='critical' to focus on the most important issues first",
1022
+ "Filter by severity='critical' to focus on the most important issues first. Severity levels: info (noteworthy), warning (needs attention), critical (requires action).",
814
1023
  "Acknowledged insights are still visible — use dismiss for false positives",
815
1024
  ],
816
1025
  }, null, 2),
@@ -927,6 +1136,12 @@ function registerDescribeTools(server) {
927
1136
  "4_publish": "publish_page — makes the page accessible at its runtime URL",
928
1137
  "5_iterate": "Repeat steps 2-4 to update. Each publish creates a new version.",
929
1138
  unpublish: "unpublish_page — removes the page from its runtime URL (keeps the definition)",
1139
+ important_behavior: {
1140
+ description: "Publishing does NOT consume or delete the draft. After publishing, the page has BOTH an activePublication (the live version) and a currentDraft (a new working draft for future edits). This is intentional — it allows iterating on changes without affecting the live page.",
1141
+ example: "After publishing version 1, get_page shows: activePublication.versionNumber=1 (live) and currentDraft.versionNumber=2 (editable). The draft is not stale — it's the starting point for the next publish cycle.",
1142
+ agent_tip: "Do not treat the draft as an error or leftover. The normal state of a published page is: one active publication + one working draft. Edit the draft, validate, publish again to create version 3, etc.",
1143
+ },
1144
+ eventual_consistency_note: "After save_page_draft or set_navigation, there may be a brief delay before the data is visible via get_page_draft or get_navigation. If you get a 'not found' immediately after writing, wait 1-2 seconds and retry.",
930
1145
  },
931
1146
  access_modes: {
932
1147
  public: "Anyone can view — no authentication required",
@@ -1675,4 +1890,317 @@ function registerDescribeTools(server) {
1675
1890
  ],
1676
1891
  });
1677
1892
  }));
1893
+ // ── Auth Providers (BYOT) ──────────────────────────────────────────
1894
+ server.tool("describe_auth_providers", "Get the full reference for configuring external auth providers (BYOT — Bring Your Own Token). Explains how to connect Clerk, Auth0, Okta, or any OIDC provider so your app's users authenticate with their existing IdP while Centrali handles authorization.", {}, () => __awaiter(this, void 0, void 0, function* () {
1895
+ return ({
1896
+ content: [
1897
+ {
1898
+ type: "text",
1899
+ text: JSON.stringify({
1900
+ domain: "External Auth Providers (BYOT)",
1901
+ description: "BYOT (Bring Your Own Token) lets your app keep using Clerk, Auth0, Okta, or any OIDC provider for authentication while Centrali handles fine-grained authorization. No user duplication, no token exchange — just pass the JWT.",
1902
+ how_it_works: {
1903
+ step1: "User authenticates via your IdP (Clerk, Auth0, Okta, etc.) → receives a JWT",
1904
+ step2: "Your app passes the JWT to Centrali in the Authorization header",
1905
+ step3: "Centrali validates the JWT signature via the provider's JWKS (auto-discovered and cached)",
1906
+ step4: "Centrali extracts claims using your configured claim mappings (e.g., org_role → ext_role)",
1907
+ step5: "Authorization policies evaluate using extracted claims (ext_* attributes)",
1908
+ step6: "Centrali returns Allow or Deny",
1909
+ },
1910
+ blog_guide: "https://centrali.io/blog/byot-bring-your-own-token-external-idp",
1911
+ docs: "https://docs.centrali.io/guides/external-auth",
1912
+ provider_types: {
1913
+ clerk: "Clerk — auto-discovers JWKS from issuer URL (https://clerk.your-domain.com)",
1914
+ auth0: "Auth0 — auto-discovers JWKS from issuer URL (https://your-tenant.auth0.com/)",
1915
+ okta: "Okta — auto-discovers JWKS from issuer URL (https://your-org.okta.com)",
1916
+ keycloak: "Keycloak — auto-discovers JWKS from issuer URL",
1917
+ oidc: "Any OIDC-compliant provider — auto-discovers JWKS from issuer's .well-known/openid-configuration",
1918
+ custom: "Custom JWT issuer — requires manual jwksUrl",
1919
+ },
1920
+ provider_shape: {
1921
+ id: "UUID — auto-generated",
1922
+ name: "string — display name",
1923
+ slug: "string — URL-safe unique identifier",
1924
+ providerType: "'oidc' | 'clerk' | 'auth0' | 'keycloak' | 'okta' | 'custom'",
1925
+ issuer: "string (URL) — JWT issuer. Must match the 'iss' claim in tokens. Unique per workspace.",
1926
+ jwksUrl: "string (URL) | null — override JWKS URL (usually auto-discovered from issuer)",
1927
+ allowedAudiences: "string[] — if set, tokens must include a matching 'aud' claim",
1928
+ clockSkewSeconds: "number — tolerance for token expiration (default: 60, max: 300)",
1929
+ allowedAlgorithms: "string[] — accepted signing algorithms (default: ['RS256'])",
1930
+ claimMappings: "ClaimMapping[] — defines how JWT claims map to policy attributes",
1931
+ allowedOrigins: "string[] — CORS origins for browser requests",
1932
+ isActive: "boolean — inactive providers reject all tokens",
1933
+ },
1934
+ claim_mappings: {
1935
+ description: "Claim mappings extract values from JWT tokens and make them available as ext_<attribute> in authorization policies.",
1936
+ shape: {
1937
+ attribute: "string — becomes ext_<attribute> in policies. Example: 'role' → ext_role. Must start with a letter, alphanumeric + underscores only. Do NOT include the ext_ prefix — it's added automatically.",
1938
+ jwtPath: "string — dot-notation path into the JWT payload. Examples: 'org_role', 'metadata.plan', 'organization.role'",
1939
+ required: "boolean (default: false) — if true, token validation fails when this claim is missing",
1940
+ defaultValue: "string | number | boolean | string[] — fallback when claim is absent",
1941
+ transform: "'lowercase' | 'uppercase' | 'string' | 'boolean' | 'array' — applied to extracted value",
1942
+ },
1943
+ examples: [
1944
+ { attribute: "role", jwtPath: "org_role", description: "Maps JWT org_role claim → ext_role in policies" },
1945
+ { attribute: "plan", jwtPath: "metadata.plan", description: "Maps nested metadata.plan → ext_plan" },
1946
+ { attribute: "department", jwtPath: "user.public_metadata.department", defaultValue: "general", description: "Deep path with fallback" },
1947
+ ],
1948
+ },
1949
+ clerk_setup: {
1950
+ description: "Step-by-step guide for Clerk integration.",
1951
+ steps: [
1952
+ "1. In Clerk Dashboard → Configure → JWT Templates, create a template named 'centrali' with the claims you need",
1953
+ "2. Example JWT template: { \"org_id\": \"{{org.id}}\", \"org_role\": \"{{org.role}}\", \"metadata\": { \"plan\": \"{{user.public_metadata.plan}}\" } }",
1954
+ "3. Create the auth provider in Centrali: create_auth_provider with providerType='clerk', issuer='https://clerk.your-domain.com'",
1955
+ "4. Add claim mappings: [{ attribute: 'role', jwtPath: 'org_role' }, { attribute: 'plan', jwtPath: 'metadata.plan' }]",
1956
+ "5. In your Next.js app, get the token: const token = await getToken({ template: 'centrali' })",
1957
+ "6. Pass the token to Centrali SDK: centrali.checkAuthorization({ token, resource: 'orders', action: 'approve' })",
1958
+ ],
1959
+ nextjs_example: "import { auth } from '@clerk/nextjs/server';\nimport { CentraliSDK } from '@centrali-io/centrali-sdk';\n\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', });\n\nexport async function POST(request: Request) {\n const { getToken } = await auth();\n const token = await getToken({ template: 'centrali' });\n const result = await centrali.checkAuthorization({ token, resource: 'premium-features', action: 'access' });\n if (!result.data.allowed) return Response.json({ error: 'Upgrade to premium' }, { status: 403 });\n return Response.json({ data: '...' });\n}",
1960
+ },
1961
+ policy_attributes: {
1962
+ description: "Attributes available in authorization policies when using external tokens.",
1963
+ attributes: {
1964
+ "ext_*": "Claims extracted via claim mappings (e.g., ext_role, ext_plan)",
1965
+ "is_external_principal": "Always true for external tokens",
1966
+ "external_issuer": "The JWT issuer URL",
1967
+ "external_subject": "The JWT subject (sub) claim",
1968
+ "request_metadata.*": "Context values passed via the context parameter in checkAuthorization()",
1969
+ },
1970
+ },
1971
+ policy_example: {
1972
+ description: "Example policy that allows premium users to access a feature.",
1973
+ policy: {
1974
+ name: "premium_access",
1975
+ specification: {
1976
+ rules: [
1977
+ {
1978
+ rule_id: "premium-allow",
1979
+ effect: "Allow",
1980
+ conditions: [
1981
+ { function: "string_equal", attribute: "ext_plan", value: "premium" },
1982
+ ],
1983
+ },
1984
+ ],
1985
+ default: { effect: "Deny" },
1986
+ },
1987
+ },
1988
+ },
1989
+ tools: {
1990
+ list_auth_providers: "List all configured providers",
1991
+ get_auth_provider: "Get provider details including claim mappings",
1992
+ create_auth_provider: "Create a new provider (Clerk, Auth0, Okta, OIDC, etc.)",
1993
+ update_auth_provider: "Update provider config (mappings, audiences, active status)",
1994
+ delete_auth_provider: "Delete a provider (tokens will be rejected)",
1995
+ test_auth_provider: "Test claim extraction with a sample JWT — validates your mappings without affecting production",
1996
+ refresh_auth_provider_jwks: "Force refresh JWKS cache after key rotation",
1997
+ },
1998
+ tips: [
1999
+ "Call create_auth_provider first, then test_auth_provider with a real JWT to validate claim mappings before deploying",
2000
+ "Claim attributes are automatically prefixed with ext_ — if you map attribute 'role', it becomes ext_role in policies",
2001
+ "JWKS is auto-discovered from the issuer URL for standard providers — you rarely need to set jwksUrl manually",
2002
+ "Use test_auth_provider to debug claim extraction without affecting production traffic",
2003
+ "Use allowedAudiences to restrict which apps can use this provider (matches the JWT 'aud' claim)",
2004
+ "Set isActive to false to temporarily disable a provider without deleting it",
2005
+ "For Clerk: create a JWT template in the Clerk Dashboard and retrieve tokens with getToken({ template: 'centrali' })",
2006
+ ],
2007
+ }, null, 2),
2008
+ },
2009
+ ],
2010
+ });
2011
+ }));
2012
+ // ── Service Accounts & IAM ──────────────────────────────────────
2013
+ server.tool("describe_service_accounts", "Get the full reference for managing service accounts, roles, groups, and permissions. Explains the complete IAM setup flow from creating a service account to granting fine-grained access.", {}, () => __awaiter(this, void 0, void 0, function* () {
2014
+ return ({
2015
+ content: [
2016
+ {
2017
+ type: "text",
2018
+ text: JSON.stringify({
2019
+ domain: "Service Accounts & IAM",
2020
+ description: "Service accounts are machine identities for backend-to-backend API access. They authenticate via OAuth2 client_credentials flow and receive permissions through roles and groups.",
2021
+ setup_flow: {
2022
+ description: "End-to-end flow for setting up a service account for a new app with least-privilege permissions.",
2023
+ steps: [
2024
+ "1. Create a service account: create_service_account — returns clientId + clientSecret (secret shown ONCE)",
2025
+ "2. Identify what the app needs: which collections it reads/writes, which triggers it invokes, which files it accesses",
2026
+ "3. Grant exactly those permissions: generate_remediation → pick the LEAST-PRIVILEGE option (prefer 'create_new' for exact access) → apply_remediation",
2027
+ "4. Verify: scan_service_account_permissions(filter='denied') — the only denied items should be things the app doesn't need",
2028
+ "5. Use the credentials: new CentraliSDK({ clientId, clientSecret, ... }) in your app",
2029
+ ],
2030
+ least_privilege_principle: "ALWAYS grant the minimum permissions needed. When generate_remediation returns multiple options, prefer 'create_new' (creates a minimal policy for exactly the requested access) over 'role_assignment' (which may grant broader access than needed). Only use role_assignment if the existing role exactly matches what you need.",
2031
+ },
2032
+ authentication: {
2033
+ production: {
2034
+ description: "OAuth2 client_credentials flow — standard, secure, auto-refreshing.",
2035
+ flow: "POST /iam/oauth2/token with grant_type=client_credentials, client_id, client_secret → receives JWT access token",
2036
+ sdk_example: "const centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'acme', clientId: 'ci_...', clientSecret: 'sk_...' }); // SDK handles token refresh automatically",
2037
+ },
2038
+ development: {
2039
+ description: "Dev tokens — short-lived tokens for testing without the OAuth2 flow.",
2040
+ tool: "generate_dev_token(serviceAccountId, ttlSeconds)",
2041
+ note: "Dev tokens have limited TTL. For production, always use client_credentials flow.",
2042
+ },
2043
+ },
2044
+ service_account_shape: {
2045
+ id: "number — auto-generated numeric ID",
2046
+ clientId: "string — format: ci_<hex> — used as the client_id in OAuth2",
2047
+ clientSecret: "string — format: sk_<hex> — ONLY returned on creation and rotation",
2048
+ name: "string — display name",
2049
+ description: "string | null — optional description",
2050
+ workspaceSlug: "string — workspace this SA belongs to",
2051
+ revoked: "boolean — if true, all tokens are invalid and no new tokens can be issued",
2052
+ createdBy: "string — user ID who created this SA",
2053
+ createdAt: "string — ISO 8601 timestamp",
2054
+ },
2055
+ permission_model: {
2056
+ description: "Permissions flow: Service Account → Roles → Permissions. Groups are optional bundles of SAs.",
2057
+ role: {
2058
+ description: "A named bundle of permissions. Roles are assigned directly to SAs or inherited through groups.",
2059
+ shape: {
2060
+ id: "UUID",
2061
+ name: "string (e.g., 'Data Reader', 'Compute Admin')",
2062
+ description: "string | null",
2063
+ permissions: "string[] — permission UUIDs",
2064
+ },
2065
+ },
2066
+ group: {
2067
+ description: "A named bundle of service accounts. Roles assigned to a group are inherited by all SAs in it.",
2068
+ shape: {
2069
+ id: "UUID",
2070
+ name: "string (e.g., 'Backend Services')",
2071
+ description: "string | null",
2072
+ },
2073
+ },
2074
+ permission_actions: [
2075
+ "create — create new resources",
2076
+ "retrieve — read a single resource by ID",
2077
+ "list — list/query resources",
2078
+ "update — modify existing resources",
2079
+ "delete — remove resources",
2080
+ "execute — run compute functions, invoke triggers",
2081
+ "manage — administrative actions (e.g., generate dev tokens)",
2082
+ ],
2083
+ common_resources: [
2084
+ "workspace::records",
2085
+ "workspace::structures",
2086
+ "workspace::compute-functions",
2087
+ "workspace::function-triggers",
2088
+ "workspace::orchestrations",
2089
+ "workspace::smart-queries",
2090
+ "workspace::pages",
2091
+ "workspace::service-accounts",
2092
+ "workspace::roles",
2093
+ "workspace::groups",
2094
+ "workspace::files",
2095
+ "workspace::search",
2096
+ ],
2097
+ },
2098
+ introspection: {
2099
+ scan: "scan_service_account_permissions — full access matrix showing every resource/action with Allow/Deny and reasons",
2100
+ simulate: "simulate_service_account_permission — test a specific resource+action, get evaluation trace and suggestions for granting access",
2101
+ tip: "Use scan with filter='denied' to find exactly what permissions a SA is missing, then use the remediation flow to grant them.",
2102
+ },
2103
+ remediation_flow: {
2104
+ description: "The recommended way to grant permissions. Instead of manually creating roles and policies, use the remediation wizard — it finds the best option automatically.",
2105
+ steps: [
2106
+ "1. Identify the gap: scan_service_account_permissions(filter='denied') or simulate_service_account_permission",
2107
+ "2. Generate options: generate_remediation(serviceAccountId, resource, resourceCategory, actions) — returns multiple options ranked by effort (low/medium/high) with a recommended pick",
2108
+ "3. Preview changes: preview_remediation(same args + chosen option) — shows exactly what would be created/modified without making changes",
2109
+ "4. Apply: apply_remediation(same args + chosen option) — creates roles/policies/assignments and verifies access was granted",
2110
+ ],
2111
+ option_types: {
2112
+ role_assignment: "Assign an existing role that already covers the needed permissions (effort: low)",
2113
+ group_assignment: "Add the SA to a group that has the right role (effort: low)",
2114
+ create_new: "Create a minimal new policy + permission + role for exactly the requested access (effort: medium-high)",
2115
+ },
2116
+ tip: "Always prefer the 'recommended' option. If multiple options exist, role_assignment is usually lowest effort. The response includes sideEffects showing what additional access the SA would gain.",
2117
+ },
2118
+ secret_management: {
2119
+ creation: "clientSecret shown ONCE at creation — store securely immediately",
2120
+ rotation: "rotate_service_account_secret — old secret immediately invalidated, new one returned (shown once)",
2121
+ revocation: "revoke_service_account — permanent, cannot be undone, all tokens invalidated",
2122
+ },
2123
+ tools: {
2124
+ lifecycle: {
2125
+ list_service_accounts: "List all SAs in the workspace",
2126
+ get_service_account: "Get SA details (no secret)",
2127
+ create_service_account: "Create SA — returns clientId + clientSecret",
2128
+ update_service_account_name: "Update SA display name",
2129
+ update_service_account_description: "Update SA description",
2130
+ delete_service_account: "Permanently delete SA",
2131
+ rotate_service_account_secret: "Generate new secret, invalidate old one",
2132
+ revoke_service_account: "Permanently disable SA",
2133
+ generate_dev_token: "Short-lived token for testing",
2134
+ },
2135
+ permissions: {
2136
+ scan_service_account_permissions: "Full access matrix audit",
2137
+ simulate_service_account_permission: "Test specific resource+action check",
2138
+ generate_remediation: "Generate options to grant missing access (role assignment, group assignment, or new policy)",
2139
+ preview_remediation: "Preview what changes an option would make (dry run)",
2140
+ apply_remediation: "Apply a remediation option to actually grant access (creates roles/policies as needed)",
2141
+ },
2142
+ roles: {
2143
+ list_roles: "List all roles",
2144
+ get_role: "Get role details with permissions",
2145
+ create_role: "Create role with permissions",
2146
+ update_role: "Update role name/description/permissions",
2147
+ delete_role: "Delete role",
2148
+ list_service_account_roles: "List roles assigned to a SA",
2149
+ assign_role_to_service_account: "Assign role to SA",
2150
+ remove_role_from_service_account: "Remove role from SA",
2151
+ },
2152
+ groups: {
2153
+ list_groups: "List all groups",
2154
+ get_group: "Get group details",
2155
+ create_group: "Create group",
2156
+ update_group: "Update group name/description",
2157
+ delete_group: "Delete group",
2158
+ list_service_account_groups: "List groups a SA belongs to",
2159
+ add_service_account_to_group: "Add SA to group",
2160
+ remove_service_account_from_group: "Remove SA from group",
2161
+ },
2162
+ },
2163
+ publishable_keys: {
2164
+ description: "Frontend-safe API keys for browser/client-side apps. Scoped to specific collections, actions, and triggers.",
2165
+ key_format: "pk_live_<hex> — safe to embed in client-side code, scoped and rate-limited",
2166
+ scope_format: {
2167
+ description: "Scopes control what the key can access. Format: resource:action:target",
2168
+ examples: [
2169
+ "records:list:products — list products collection",
2170
+ "records:retrieve:* — read any collection (wildcard OK for reads)",
2171
+ "records:create:orders — create orders (write actions require explicit slug)",
2172
+ "triggers:execute:send-email — invoke a specific HTTP trigger",
2173
+ "files:retrieve — read files (two-part scope, no target)",
2174
+ "collections:list — list collection schemas",
2175
+ ],
2176
+ rules: [
2177
+ "Write actions (create, execute) require explicit collection/trigger slugs — no wildcards",
2178
+ "Read actions (retrieve, list) allow wildcard targets (*)",
2179
+ "Any records scope auto-grants collections:retrieve for that collection (schema access)",
2180
+ ],
2181
+ },
2182
+ tools: {
2183
+ list_publishable_keys: "List all publishable keys",
2184
+ get_publishable_key: "Get key details with scopes",
2185
+ create_publishable_key: "Create a new scoped key",
2186
+ update_publishable_key: "Update label or scopes",
2187
+ revoke_publishable_key: "Revoke a key (immediate, irreversible)",
2188
+ },
2189
+ },
2190
+ tips: [
2191
+ "LEAST PRIVILEGE: When granting permissions, only grant what the app actually needs. Use generate_remediation → prefer 'create_new' for exact access rather than broad role assignments.",
2192
+ "Use scan_service_account_permissions with filter='denied' to find missing permissions, then use the remediation flow to grant them",
2193
+ "Use simulate_service_account_permission to test before deploying — it shows the evaluation trace so you can debug policy decisions",
2194
+ "For frontend apps, use publishable keys (create_publishable_key) with scoped access — never expose service account credentials in client code",
2195
+ "For backend apps, use service accounts — they authenticate via OAuth2 client_credentials",
2196
+ "Group SAs by function (e.g., 'Backend Services', 'Analytics Pipeline') and assign roles to groups for easier management",
2197
+ "Always rotate secrets via rotate_service_account_secret rather than creating a new SA — preserves role/group assignments",
2198
+ "For development, use generate_dev_token instead of hardcoding client credentials in test scripts",
2199
+ "The clientSecret is only shown once — at creation and rotation. If lost, rotate to get a new one.",
2200
+ ],
2201
+ }, null, 2),
2202
+ },
2203
+ ],
2204
+ });
2205
+ }));
1678
2206
  }