@centrali-io/centrali-mcp 4.4.6 → 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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ MCP (Model Context Protocol) server for the Centrali platform. Lets AI assistant
4
4
 
5
5
  > **Full documentation:** [docs.centrali.io](https://docs.centrali.io) — SDK guide, API reference, compute functions, orchestrations, and more.
6
6
 
7
- Built on `@centrali-io/centrali-sdk` v4.4.6. Authenticates via service account client credentials.
7
+ Built on `@centrali-io/centrali-sdk` v4.4.7. Authenticates via service account client credentials.
8
8
 
9
9
  ## Setup
10
10
 
@@ -110,6 +110,7 @@ After connecting, call `describe_centrali` first — it returns the full capabil
110
110
  | `get_compute_job_status` | Check async job status by job ID (poll after invoke_trigger) |
111
111
  | `list_allowed_domains` | List allowed domains for outbound HTTP |
112
112
  | `add_allowed_domain` | Add a domain to the allowlist |
113
+ | `invoke_endpoint` | Call a sync compute endpoint by path (returns response inline) |
113
114
  | `remove_allowed_domain` | Remove a domain from the allowlist |
114
115
 
115
116
  ### Smart Queries
@@ -37,10 +37,10 @@ function ensureToken(sdk) {
37
37
  */
38
38
  function createIamClient(sdk, centraliUrl, workspaceId) {
39
39
  const url = new URL(centraliUrl);
40
- const hostname = url.hostname.startsWith("api.")
40
+ const hostname = url.hostname.startsWith("auth.")
41
41
  ? url.hostname
42
- : `api.${url.hostname}`;
43
- const baseURL = `${url.protocol}//${hostname}/iam/workspace/${workspaceId}/api/v1/external-auth-providers`;
42
+ : `auth.${url.hostname.replace(/^api\./, '')}`;
43
+ const baseURL = `${url.protocol}//${hostname}/workspace/${workspaceId}/api/v1/external-auth-providers`;
44
44
  const client = axios_1.default.create({ baseURL });
45
45
  client.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
46
46
  const token = yield ensureToken(sdk);
@@ -82,9 +82,9 @@ function registerComputeTools(server, sdk, centraliUrl, workspaceId) {
82
82
  };
83
83
  }
84
84
  }));
85
- server.tool("list_triggers", "List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger).", {
85
+ server.tool("list_triggers", "List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger, endpoint).", {
86
86
  executionType: zod_1.z
87
- .enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
87
+ .enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
88
88
  .optional()
89
89
  .describe("Filter by trigger execution type"),
90
90
  page: zod_1.z.number().optional().describe("Page number"),
@@ -394,13 +394,13 @@ function registerComputeTools(server, sdk, centraliUrl, workspaceId) {
394
394
  name: zod_1.z.string().describe("Display name for the trigger"),
395
395
  functionId: zod_1.z.string().describe("The compute function ID (UUID) to execute"),
396
396
  executionType: zod_1.z
397
- .enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
398
- .describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron), or http-trigger (external HTTP POST)"),
397
+ .enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
398
+ .describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron), http-trigger (external HTTP POST), or endpoint (synchronous HTTP API — returns response inline)"),
399
399
  description: zod_1.z.string().optional().describe("Optional description"),
400
400
  triggerMetadata: zod_1.z
401
401
  .record(zod_1.z.string(), zod_1.z.any())
402
402
  .optional()
403
- .describe("Type-specific configuration. For event-driven: { eventType, recordSlug } where eventType is record_created | record_updated | record_deleted. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: auto-generated URL."),
403
+ .describe("Type-specific configuration. For event-driven: { eventType, recordSlug }. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: auto-generated URL. For endpoint: { path, allowedMethods?, timeoutMs?, auth? } where path is URL-safe (e.g., 'create-order'), allowedMethods defaults to ['POST'], timeoutMs 1000-30000 (default 30000), auth is { mode: 'bearer'|'public'|'apiKey'|'hmac' }."),
404
404
  enabled: zod_1.z.boolean().optional().describe("Whether the trigger is enabled (default: true)"),
405
405
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ name, functionId, executionType, description, triggerMetadata, enabled }) {
406
406
  try {
@@ -616,6 +616,54 @@ function registerComputeTools(server, sdk, centraliUrl, workspaceId) {
616
616
  };
617
617
  }
618
618
  }));
619
+ // ── Endpoint Trigger (Sync Execution) ─────────────────────────────
620
+ server.tool("invoke_endpoint", "Invoke a compute endpoint trigger by path. The function executes synchronously — Centrali waits for the function to complete and returns its output directly in the response. No polling needed. Max execution time: 30 seconds (configurable via triggerMetadata.timeoutMs, range 1–30s). If the function exceeds the timeout, returns 504. Endpoint triggers must be created first with executionType='endpoint'. Use this for real-time API responses; use invoke_trigger for long-running background work that doesn't need an immediate response.", {
621
+ path: zod_1.z.string().describe("The endpoint path (e.g., 'create-order', 'webhook/shipments'). This is set in the trigger's triggerMetadata.path."),
622
+ method: zod_1.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (default: POST). Must be in the trigger's allowedMethods."),
623
+ payload: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional().describe("Request body payload (sent as JSON)"),
624
+ headers: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional().describe("Additional headers (e.g., X-API-Key for apiKey auth)"),
625
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ path, method, payload, headers: extraHeaders }) {
626
+ try {
627
+ const token = yield ensureToken(sdk);
628
+ const url = new URL(centraliUrl);
629
+ const hostname = url.hostname.startsWith("api.")
630
+ ? url.hostname
631
+ : `api.${url.hostname}`;
632
+ const apiUrl = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/endpoints/${path}`;
633
+ const reqHeaders = {};
634
+ if (token)
635
+ reqHeaders.Authorization = `Bearer ${token}`;
636
+ if (extraHeaders)
637
+ Object.assign(reqHeaders, extraHeaders);
638
+ const httpMethod = (method || "POST").toLowerCase();
639
+ const result = yield (0, axios_1.default)({
640
+ method: httpMethod,
641
+ url: apiUrl,
642
+ data: ["get", "delete"].includes(httpMethod) ? undefined : (payload || {}),
643
+ headers: reqHeaders,
644
+ validateStatus: () => true, // Don't throw on non-2xx — return the function's response as-is
645
+ });
646
+ return {
647
+ content: [{
648
+ type: "text",
649
+ text: JSON.stringify({
650
+ status: result.status,
651
+ headers: {
652
+ "content-type": result.headers["content-type"],
653
+ "x-execution-id": result.headers["x-execution-id"],
654
+ },
655
+ body: result.data,
656
+ }, null, 2),
657
+ }],
658
+ };
659
+ }
660
+ catch (error) {
661
+ return {
662
+ content: [{ type: "text", text: formatError(error, `invoking endpoint '${path}'`) }],
663
+ isError: true,
664
+ };
665
+ }
666
+ }));
619
667
  // ── Allowed Domains tools ──────────────────────────────────────────
620
668
  server.tool("list_allowed_domains", "List all allowed domains for compute function HTTP requests. Functions can only call external APIs on domains in this allowlist.", {}, () => __awaiter(this, void 0, void 0, function* () {
621
669
  try {
@@ -85,6 +85,7 @@ function registerDescribeTools(server) {
85
85
  "get_function_run",
86
86
  "list_function_runs",
87
87
  "get_compute_job_status",
88
+ "invoke_endpoint",
88
89
  ],
89
90
  },
90
91
  smart_queries: {
@@ -220,6 +221,16 @@ function registerDescribeTools(server) {
220
221
  ],
221
222
  },
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
+ },
223
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.",
224
235
  app_credential_setup: {
225
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.",
@@ -522,13 +533,29 @@ function registerDescribeTools(server) {
522
533
  description: "Triggered by an external HTTP POST to a generated webhook URL.",
523
534
  config: "Each http-trigger gets a unique URL to share with external services",
524
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
+ },
525
552
  },
526
553
  trigger_shape: {
527
554
  id: "UUID",
528
555
  name: "string",
529
556
  description: "string | null",
530
557
  functionId: "UUID — the compute function to execute",
531
- executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger'",
558
+ executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger' | 'endpoint'",
532
559
  triggerMetadata: "object — type-specific configuration (event, cron, params, etc.)",
533
560
  enabled: "boolean — whether the trigger is active (default: true)",
534
561
  workspaceSlug: "string",
@@ -575,6 +602,7 @@ function registerDescribeTools(server) {
575
602
  triggerMetadata_examples: {
576
603
  "event-driven": { eventType: "record_created", recordSlug: "orders" },
577
604
  scheduled: { scheduleType: "cron", cronExpression: "0 9 * * *", timezone: "America/New_York" },
605
+ endpoint: { path: "create-order", allowedMethods: ["POST"], timeoutMs: 10000, auth: { mode: "bearer" } },
578
606
  },
579
607
  },
580
608
  update_trigger: {
@@ -969,7 +997,7 @@ function registerDescribeTools(server) {
969
997
  id: "UUID",
970
998
  structureSlug: "string — the collection where the anomaly was detected",
971
999
  type: "string — anomaly type (e.g., 'spike', 'drop', 'outlier', 'pattern_break')",
972
- severity: "'critical' | 'high' | 'medium' | 'low'",
1000
+ severity: "'info' | 'warning' | 'critical'",
973
1001
  status: "'active' | 'acknowledged' | 'dismissed'",
974
1002
  title: "string — human-readable summary",
975
1003
  description: "string — detailed explanation of the anomaly",
@@ -986,12 +1014,12 @@ function registerDescribeTools(server) {
986
1014
  totalActive: "number",
987
1015
  totalAcknowledged: "number",
988
1016
  totalDismissed: "number",
989
- bySeverity: "{ critical: n, high: n, medium: n, low: n }",
1017
+ bySeverity: "{ info: n, warning: n, critical: n }",
990
1018
  },
991
1019
  tips: [
992
1020
  "Use trigger_anomaly_analysis to scan a specific collection on-demand",
993
1021
  "Use get_insights_summary for a quick overview before diving into individual insights",
994
- "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).",
995
1023
  "Acknowledged insights are still visible — use dismiss for false positives",
996
1024
  ],
997
1025
  }, null, 2),
@@ -1108,6 +1136,12 @@ function registerDescribeTools(server) {
1108
1136
  "4_publish": "publish_page — makes the page accessible at its runtime URL",
1109
1137
  "5_iterate": "Repeat steps 2-4 to update. Each publish creates a new version.",
1110
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.",
1111
1145
  },
1112
1146
  access_modes: {
1113
1147
  public: "Anyone can view — no authentication required",
@@ -46,7 +46,7 @@ function registerInsightTools(server, sdk) {
46
46
  .optional()
47
47
  .describe("Filter by insight status"),
48
48
  severity: zod_1.z
49
- .enum(["critical", "high", "medium", "low"])
49
+ .enum(["info", "warning", "critical"])
50
50
  .optional()
51
51
  .describe("Filter by severity level"),
52
52
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ structureSlug, status, severity }) {
@@ -37,10 +37,10 @@ function ensureToken(sdk) {
37
37
  */
38
38
  function createIamClient(sdk, centraliUrl, workspaceId, baseSuffix) {
39
39
  const url = new URL(centraliUrl);
40
- const hostname = url.hostname.startsWith("api.")
40
+ const hostname = url.hostname.startsWith("auth.")
41
41
  ? url.hostname
42
- : `api.${url.hostname}`;
43
- const baseURL = `${url.protocol}//${hostname}/iam/workspace/${workspaceId}/api/v1/${baseSuffix}`;
42
+ : `auth.${url.hostname.replace(/^api\./, '')}`;
43
+ const baseURL = `${url.protocol}//${hostname}/workspace/${workspaceId}/api/v1/${baseSuffix}`;
44
44
  const client = axios_1.default.create({ baseURL });
45
45
  client.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
46
46
  const token = yield ensureToken(sdk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "4.4.6",
3
+ "version": "4.4.7",
4
4
  "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -25,7 +25,7 @@
25
25
  "author": "Blueinit",
26
26
  "license": "ISC",
27
27
  "dependencies": {
28
- "@centrali-io/centrali-sdk": "^4.4.6",
28
+ "@centrali-io/centrali-sdk": "^4.4.7",
29
29
  "@modelcontextprotocol/sdk": "^1.12.1"
30
30
  },
31
31
  "devDependencies": {
@@ -22,10 +22,10 @@ async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
22
22
  */
23
23
  function createIamClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: string): AxiosInstance {
24
24
  const url = new URL(centraliUrl);
25
- const hostname = url.hostname.startsWith("api.")
25
+ const hostname = url.hostname.startsWith("auth.")
26
26
  ? url.hostname
27
- : `api.${url.hostname}`;
28
- const baseURL = `${url.protocol}//${hostname}/iam/workspace/${workspaceId}/api/v1/external-auth-providers`;
27
+ : `auth.${url.hostname.replace(/^api\./, '')}`;
28
+ const baseURL = `${url.protocol}//${hostname}/workspace/${workspaceId}/api/v1/external-auth-providers`;
29
29
 
30
30
  const client = axios.create({ baseURL });
31
31
 
@@ -74,10 +74,10 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centra
74
74
 
75
75
  server.tool(
76
76
  "list_triggers",
77
- "List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger).",
77
+ "List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger, endpoint).",
78
78
  {
79
79
  executionType: z
80
- .enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
80
+ .enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
81
81
  .optional()
82
82
  .describe("Filter by trigger execution type"),
83
83
  page: z.number().optional().describe("Page number"),
@@ -437,13 +437,13 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centra
437
437
  name: z.string().describe("Display name for the trigger"),
438
438
  functionId: z.string().describe("The compute function ID (UUID) to execute"),
439
439
  executionType: z
440
- .enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
441
- .describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron), or http-trigger (external HTTP POST)"),
440
+ .enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
441
+ .describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron), http-trigger (external HTTP POST), or endpoint (synchronous HTTP API — returns response inline)"),
442
442
  description: z.string().optional().describe("Optional description"),
443
443
  triggerMetadata: z
444
444
  .record(z.string(), z.any())
445
445
  .optional()
446
- .describe("Type-specific configuration. For event-driven: { eventType, recordSlug } where eventType is record_created | record_updated | record_deleted. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: auto-generated URL."),
446
+ .describe("Type-specific configuration. For event-driven: { eventType, recordSlug }. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: auto-generated URL. For endpoint: { path, allowedMethods?, timeoutMs?, auth? } where path is URL-safe (e.g., 'create-order'), allowedMethods defaults to ['POST'], timeoutMs 1000-30000 (default 30000), auth is { mode: 'bearer'|'public'|'apiKey'|'hmac' }."),
447
447
  enabled: z.boolean().optional().describe("Whether the trigger is enabled (default: true)"),
448
448
  },
449
449
  async ({ name, functionId, executionType, description, triggerMetadata, enabled }) => {
@@ -687,6 +687,61 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centra
687
687
  }
688
688
  );
689
689
 
690
+ // ── Endpoint Trigger (Sync Execution) ─────────────────────────────
691
+
692
+ server.tool(
693
+ "invoke_endpoint",
694
+ "Invoke a compute endpoint trigger by path. The function executes synchronously — Centrali waits for the function to complete and returns its output directly in the response. No polling needed. Max execution time: 30 seconds (configurable via triggerMetadata.timeoutMs, range 1–30s). If the function exceeds the timeout, returns 504. Endpoint triggers must be created first with executionType='endpoint'. Use this for real-time API responses; use invoke_trigger for long-running background work that doesn't need an immediate response.",
695
+ {
696
+ path: z.string().describe("The endpoint path (e.g., 'create-order', 'webhook/shipments'). This is set in the trigger's triggerMetadata.path."),
697
+ method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (default: POST). Must be in the trigger's allowedMethods."),
698
+ payload: z.record(z.string(), z.any()).optional().describe("Request body payload (sent as JSON)"),
699
+ headers: z.record(z.string(), z.string()).optional().describe("Additional headers (e.g., X-API-Key for apiKey auth)"),
700
+ },
701
+ async ({ path, method, payload, headers: extraHeaders }) => {
702
+ try {
703
+ const token = await ensureToken(sdk);
704
+ const url = new URL(centraliUrl);
705
+ const hostname = url.hostname.startsWith("api.")
706
+ ? url.hostname
707
+ : `api.${url.hostname}`;
708
+ const apiUrl = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/endpoints/${path}`;
709
+
710
+ const reqHeaders: Record<string, string> = {};
711
+ if (token) reqHeaders.Authorization = `Bearer ${token}`;
712
+ if (extraHeaders) Object.assign(reqHeaders, extraHeaders);
713
+
714
+ const httpMethod = (method || "POST").toLowerCase();
715
+ const result = await axios({
716
+ method: httpMethod as any,
717
+ url: apiUrl,
718
+ data: ["get", "delete"].includes(httpMethod) ? undefined : (payload || {}),
719
+ headers: reqHeaders,
720
+ validateStatus: () => true, // Don't throw on non-2xx — return the function's response as-is
721
+ });
722
+
723
+ return {
724
+ content: [{
725
+ type: "text",
726
+ text: JSON.stringify({
727
+ status: result.status,
728
+ headers: {
729
+ "content-type": result.headers["content-type"],
730
+ "x-execution-id": result.headers["x-execution-id"],
731
+ },
732
+ body: result.data,
733
+ }, null, 2),
734
+ }],
735
+ };
736
+ } catch (error: unknown) {
737
+ return {
738
+ content: [{ type: "text", text: formatError(error, `invoking endpoint '${path}'`) }],
739
+ isError: true,
740
+ };
741
+ }
742
+ }
743
+ );
744
+
690
745
  // ── Allowed Domains tools ──────────────────────────────────────────
691
746
 
692
747
  server.tool(
@@ -85,6 +85,7 @@ export function registerDescribeTools(server: McpServer) {
85
85
  "get_function_run",
86
86
  "list_function_runs",
87
87
  "get_compute_job_status",
88
+ "invoke_endpoint",
88
89
  ],
89
90
  },
90
91
  smart_queries: {
@@ -227,6 +228,16 @@ export function registerDescribeTools(server: McpServer) {
227
228
  ],
228
229
  },
229
230
  },
231
+ naming_guide: {
232
+ 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.",
233
+ aliases: {
234
+ recordSlug: "Used by record tools (query_records, create_record, etc.). This is the collection's URL-safe slug, e.g., 'orders'.",
235
+ structureSlug: "Used by validation and insights tools. Same value as recordSlug.",
236
+ collections: "Used by search_records. Same value as recordSlug. Accepts a string or array of strings.",
237
+ structureIds: "Used by generate_starter_pages. This is the collection UUID (not the slug). Get it from list_collections → id field.",
238
+ },
239
+ 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.",
240
+ },
230
241
  workflow:
231
242
  "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.",
232
243
  app_credential_setup: {
@@ -594,13 +605,30 @@ export function registerDescribeTools(server: McpServer) {
594
605
  "Triggered by an external HTTP POST to a generated webhook URL.",
595
606
  config: "Each http-trigger gets a unique URL to share with external services",
596
607
  },
608
+ "endpoint": {
609
+ description:
610
+ "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.",
611
+ 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.",
612
+ config: {
613
+ path: "string — URL-safe path (e.g., 'create-order', 'webhook/shipments'). Must be unique per workspace.",
614
+ allowedMethods: "string[] — HTTP methods to accept (default: ['POST']). Options: GET, POST, PUT, DELETE, PATCH.",
615
+ timeoutMs: "number — execution timeout 1000-30000ms (default: 30000). Function MUST complete within this window or the request returns 504 Gateway Timeout.",
616
+ 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).",
617
+ },
618
+ invocation: "Call invoke_endpoint with the path. Response comes back inline — no polling needed.",
619
+ example_use_cases: [
620
+ "Build a REST API endpoint backed by a compute function",
621
+ "Create a webhook receiver that processes and responds synchronously",
622
+ "Expose a function as an HTTP service for external integrations",
623
+ ],
624
+ },
597
625
  },
598
626
  trigger_shape: {
599
627
  id: "UUID",
600
628
  name: "string",
601
629
  description: "string | null",
602
630
  functionId: "UUID — the compute function to execute",
603
- executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger'",
631
+ executionType: "'on-demand' | 'event-driven' | 'scheduled' | 'http-trigger' | 'endpoint'",
604
632
  triggerMetadata: "object — type-specific configuration (event, cron, params, etc.)",
605
633
  enabled: "boolean — whether the trigger is active (default: true)",
606
634
  workspaceSlug: "string",
@@ -647,6 +675,7 @@ export function registerDescribeTools(server: McpServer) {
647
675
  triggerMetadata_examples: {
648
676
  "event-driven": { eventType: "record_created", recordSlug: "orders" },
649
677
  scheduled: { scheduleType: "cron", cronExpression: "0 9 * * *", timezone: "America/New_York" },
678
+ endpoint: { path: "create-order", allowedMethods: ["POST"], timeoutMs: 10000, auth: { mode: "bearer" } },
650
679
  },
651
680
  },
652
681
  update_trigger: {
@@ -1074,7 +1103,7 @@ export function registerDescribeTools(server: McpServer) {
1074
1103
  id: "UUID",
1075
1104
  structureSlug: "string — the collection where the anomaly was detected",
1076
1105
  type: "string — anomaly type (e.g., 'spike', 'drop', 'outlier', 'pattern_break')",
1077
- severity: "'critical' | 'high' | 'medium' | 'low'",
1106
+ severity: "'info' | 'warning' | 'critical'",
1078
1107
  status: "'active' | 'acknowledged' | 'dismissed'",
1079
1108
  title: "string — human-readable summary",
1080
1109
  description: "string — detailed explanation of the anomaly",
@@ -1093,12 +1122,12 @@ export function registerDescribeTools(server: McpServer) {
1093
1122
  totalActive: "number",
1094
1123
  totalAcknowledged: "number",
1095
1124
  totalDismissed: "number",
1096
- bySeverity: "{ critical: n, high: n, medium: n, low: n }",
1125
+ bySeverity: "{ info: n, warning: n, critical: n }",
1097
1126
  },
1098
1127
  tips: [
1099
1128
  "Use trigger_anomaly_analysis to scan a specific collection on-demand",
1100
1129
  "Use get_insights_summary for a quick overview before diving into individual insights",
1101
- "Filter by severity='critical' to focus on the most important issues first",
1130
+ "Filter by severity='critical' to focus on the most important issues first. Severity levels: info (noteworthy), warning (needs attention), critical (requires action).",
1102
1131
  "Acknowledged insights are still visible — use dismiss for false positives",
1103
1132
  ],
1104
1133
  },
@@ -1255,6 +1284,12 @@ export function registerDescribeTools(server: McpServer) {
1255
1284
  "Repeat steps 2-4 to update. Each publish creates a new version.",
1256
1285
  unpublish:
1257
1286
  "unpublish_page — removes the page from its runtime URL (keeps the definition)",
1287
+ important_behavior: {
1288
+ 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.",
1289
+ 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.",
1290
+ 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.",
1291
+ },
1292
+ 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.",
1258
1293
  },
1259
1294
  access_modes: {
1260
1295
  public: "Anyone can view — no authentication required",
@@ -41,7 +41,7 @@ export function registerInsightTools(server: McpServer, sdk: CentraliSDK) {
41
41
  .optional()
42
42
  .describe("Filter by insight status"),
43
43
  severity: z
44
- .enum(["critical", "high", "medium", "low"])
44
+ .enum(["info", "warning", "critical"])
45
45
  .optional()
46
46
  .describe("Filter by severity level"),
47
47
  },
@@ -22,10 +22,10 @@ async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
22
22
  */
23
23
  function createIamClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: string, baseSuffix: string): AxiosInstance {
24
24
  const url = new URL(centraliUrl);
25
- const hostname = url.hostname.startsWith("api.")
25
+ const hostname = url.hostname.startsWith("auth.")
26
26
  ? url.hostname
27
- : `api.${url.hostname}`;
28
- const baseURL = `${url.protocol}//${hostname}/iam/workspace/${workspaceId}/api/v1/${baseSuffix}`;
27
+ : `auth.${url.hostname.replace(/^api\./, '')}`;
28
+ const baseURL = `${url.protocol}//${hostname}/workspace/${workspaceId}/api/v1/${baseSuffix}`;
29
29
 
30
30
  const client = axios.create({ baseURL });
31
31