@centrali-io/centrali-mcp 6.0.0 → 6.1.0

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.
@@ -379,7 +379,7 @@ function registerComputeTools(server, sdk, centraliUrl, workspaceId) {
379
379
  triggerMetadata: zod_1.z
380
380
  .record(zod_1.z.string(), zod_1.z.any())
381
381
  .optional()
382
- .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' }. All trigger types accept params: a dictionary of static values passed to the function as triggerParams. Mark any secret with { value: 'plaintext', encrypt: true } to store it AES-256-GCM-encrypted at rest; it arrives as plaintext in triggerParams at execution time."),
382
+ .describe("Type-specific configuration. For event-driven: { eventType, recordSlug }. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: { path } where path is a URL-safe slug (e.g. 'stripe-webhook'); the trigger is reachable at `<api-host>/data/workspace/<workspaceSlug>/api/v1/http-trigger/<path>`. Optional native HMAC signature verification (no crypto in the function body): the **recommended path** is { validateSignature: true, provider: 'svix' | 'stripe' | 'slack' | 'github' | 'shopify', signingSecret } — the `provider` shortcut wires every wire-format detail (header names, signed-value format, digest encoding, secret encoding) from a built-in preset. Covers Svix (Clerk, Resend, Loops, OpenAI, Brex), Stripe, Slack, GitHub, Shopify out of the box. **Advanced**: any preset field can be overridden, or the preset can be skipped entirely by supplying the raw knobs directly — { validateSignature: true, signingSecret, signatureHeaderName, signatureIdHeaderName?, timestampHeaderName?, timestampExtractionPattern?, extractionPattern?, hmacAlgorithm?, hmacEncoding?, secretEncoding?, encoding?, payloadFormat? }. Defaults when no provider: hmacAlgorithm 'sha256', hmacEncoding 'base64', extractionPattern '^(?:v1,)?(.+)$', secretEncoding 'raw'; built-in 5-minute replay tolerance. Two-step setup recommended: (1) create the trigger with just { path } and optionally { provider } — the public URL is `<api-host>/data/workspace/<ws>/api/v1/http-trigger/<path>`; (2) register that URL with the webhook provider; (3) when the provider issues the signing secret, call update_trigger with validateSignature: true + signingSecret. Providers will not give a signing secret until they have the 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' | 'svix' | 'stripe' | 'slack' | 'github' | 'shopify', secret? } — provider-named modes are HMAC verification with the matching preset (auth.secret is the colocated signing secret; legacy `mode: 'hmac'` keeps its single-header body-only behaviour for back-compat). All trigger types accept params: a dictionary of static values passed to the function as triggerParams. Mark any secret with { value: 'plaintext', encrypt: true } to store it AES-256-GCM-encrypted at rest; it arrives as plaintext in triggerParams at execution time."),
383
383
  enabled: zod_1.z.boolean().optional().describe("Whether the trigger is enabled (default: true)"),
384
384
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ name, functionId, executionType, description, triggerMetadata, enabled }) {
385
385
  try {
@@ -845,6 +845,33 @@ function registerDescribeTools(server) {
845
845
  triggerMetadata_examples: {
846
846
  "event-driven": { eventType: "record_created", recordSlug: "orders" },
847
847
  scheduled: { scheduleType: "cron", cronExpression: "0 9 * * *", timezone: "America/New_York" },
848
+ "http-trigger_url_shape": "Public URL: <api-host>/data/workspace/<workspaceSlug>/api/v1/http-trigger/<path>. The path is supplied by the caller in triggerMetadata.path and must be URL-safe. The URL is NOT returned in the create_trigger / get_trigger response — derive it from the workspaceSlug + path.",
849
+ "http-trigger_setup_flow": "Webhook signing secrets are chicken-and-egg: providers issue the secret only AFTER you give them a URL. Recommended flow — (1) create_trigger with { path, provider, validateSignature: true } and NO signingSecret — the trigger is created in a pending state with a stable URL; (2) register that URL in the provider's dashboard; (3) once the provider returns the signing secret, call update_trigger with signingSecret set. Until step 3 lands, the trigger rejects every request with 'HMAC signing secret not configured for this endpoint' — that's the desired safe default during URL-registration.",
850
+ "http-trigger_provider_shortcut": {
851
+ _note: "Recommended — one preset wires every wire-format detail. Use this for Svix (Clerk, Resend, Loops, OpenAI, Brex), Stripe, Slack, GitHub, Shopify. Override individual fields only if a provider's scheme has drifted (e.g. custom hmacAlgorithm).",
852
+ path: "clerk-webhook",
853
+ validateSignature: true,
854
+ provider: "svix",
855
+ signingSecret: "whsec_…",
856
+ },
857
+ "http-trigger_advanced_raw": {
858
+ _note: "Skip the preset for full manual control — useful for non-standard schemes the built-in providers don't cover. Every field maps 1:1 to the runtime SignatureMetadata interface.",
859
+ path: "custom-webhook",
860
+ validateSignature: true,
861
+ signingSecret: "whsec_…",
862
+ signatureHeaderName: "x-custom-signature",
863
+ timestampHeaderName: "x-custom-timestamp",
864
+ extractionPattern: "^v1,(.+)$",
865
+ hmacAlgorithm: "sha256",
866
+ hmacEncoding: "base64",
867
+ secretEncoding: "prefixed-base64",
868
+ },
869
+ endpoint_provider_shortcut: {
870
+ _note: "Provider-named endpoint auth mode + colocated secret. Equivalent to mode: 'hmac' + triggerMetadata.provider — the service layer normalises auth.secret onto triggerMetadata.signingSecret at create/update time.",
871
+ path: "stripe-events",
872
+ allowedMethods: ["POST"],
873
+ auth: { mode: "stripe", secret: "whsec_…" },
874
+ },
848
875
  endpoint: { path: "create-order", allowedMethods: ["POST"], timeoutMs: 10000, auth: { mode: "bearer" } },
849
876
  },
850
877
  },
@@ -104,7 +104,7 @@ function dateWindowToWhere(dw) {
104
104
  return { and: conds };
105
105
  }
106
106
  function translateQueryRecordsArgs(args) {
107
- var _a, _b;
107
+ var _a, _b, _c, _d;
108
108
  const isLegacy = args.recordSlug !== undefined ||
109
109
  args.filters !== undefined ||
110
110
  typeof args.sort === "string" ||
@@ -174,8 +174,19 @@ function translateQueryRecordsArgs(args) {
174
174
  if (relations.length > 0)
175
175
  definition.include = relations;
176
176
  }
177
- // `includeTotal` is dropped `meta.total` is always returned for offset
178
- // pagination on POST /records/query. `includeDeleted` is rejected above.
177
+ // CEN-1259: count(*) is opt-in on POST /records/query. Default for the
178
+ // canonical surface is OFF (skip the duplicate predicate scan); legacy
179
+ // 5.4.0 callers expected `meta.total` always, so when the caller used a
180
+ // legacy field we preserve total-by-default. `includeTotal` (any surface)
181
+ // is the explicit override — including `includeTotal: false`, which must
182
+ // clear an existing canonical `page.withTotal: true` so the override is
183
+ // authoritative on both directions. `includeDeleted` is rejected above.
184
+ if (args.includeTotal !== undefined) {
185
+ definition.page = Object.assign(Object.assign({}, ((_c = definition.page) !== null && _c !== void 0 ? _c : { limit: 50 })), { withTotal: args.includeTotal === true });
186
+ }
187
+ else if (isLegacy) {
188
+ definition.page = Object.assign(Object.assign({}, ((_d = definition.page) !== null && _d !== void 0 ? _d : { limit: 50 })), { withTotal: true });
189
+ }
179
190
  return { resource, definition };
180
191
  }
181
192
  /**
@@ -384,7 +395,7 @@ Returns the canonical { data, meta } envelope. 'select', 'text', and 'include' a
384
395
  includeTotal: zod_1.z
385
396
  .boolean()
386
397
  .optional()
387
- .describe("[Deprecated 2026-10-28] Ignored `meta.total` is always returned for offset pagination."),
398
+ .describe("Set to true to request `meta.total` in the response. Default false — counting is skipped to keep list calls fast (CEN-1259)."),
388
399
  // Templating
389
400
  variables: zod_1.z
390
401
  .record(zod_1.z.string(), zod_1.z.any())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
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": "^6.0.0",
28
+ "@centrali-io/centrali-sdk": "^6.1.0",
29
29
  "@modelcontextprotocol/sdk": "^1.28.0"
30
30
  },
31
31
  "devDependencies": {
@@ -422,7 +422,7 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centra
422
422
  triggerMetadata: z
423
423
  .record(z.string(), z.any())
424
424
  .optional()
425
- .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' }. All trigger types accept params: a dictionary of static values passed to the function as triggerParams. Mark any secret with { value: 'plaintext', encrypt: true } to store it AES-256-GCM-encrypted at rest; it arrives as plaintext in triggerParams at execution time."),
425
+ .describe("Type-specific configuration. For event-driven: { eventType, recordSlug }. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: { path } where path is a URL-safe slug (e.g. 'stripe-webhook'); the trigger is reachable at `<api-host>/data/workspace/<workspaceSlug>/api/v1/http-trigger/<path>`. Optional native HMAC signature verification (no crypto in the function body): the **recommended path** is { validateSignature: true, provider: 'svix' | 'stripe' | 'slack' | 'github' | 'shopify', signingSecret } — the `provider` shortcut wires every wire-format detail (header names, signed-value format, digest encoding, secret encoding) from a built-in preset. Covers Svix (Clerk, Resend, Loops, OpenAI, Brex), Stripe, Slack, GitHub, Shopify out of the box. **Advanced**: any preset field can be overridden, or the preset can be skipped entirely by supplying the raw knobs directly — { validateSignature: true, signingSecret, signatureHeaderName, signatureIdHeaderName?, timestampHeaderName?, timestampExtractionPattern?, extractionPattern?, hmacAlgorithm?, hmacEncoding?, secretEncoding?, encoding?, payloadFormat? }. Defaults when no provider: hmacAlgorithm 'sha256', hmacEncoding 'base64', extractionPattern '^(?:v1,)?(.+)$', secretEncoding 'raw'; built-in 5-minute replay tolerance. Two-step setup recommended: (1) create the trigger with just { path } and optionally { provider } — the public URL is `<api-host>/data/workspace/<ws>/api/v1/http-trigger/<path>`; (2) register that URL with the webhook provider; (3) when the provider issues the signing secret, call update_trigger with validateSignature: true + signingSecret. Providers will not give a signing secret until they have the 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' | 'svix' | 'stripe' | 'slack' | 'github' | 'shopify', secret? } — provider-named modes are HMAC verification with the matching preset (auth.secret is the colocated signing secret; legacy `mode: 'hmac'` keeps its single-header body-only behaviour for back-compat). All trigger types accept params: a dictionary of static values passed to the function as triggerParams. Mark any secret with { value: 'plaintext', encrypt: true } to store it AES-256-GCM-encrypted at rest; it arrives as plaintext in triggerParams at execution time."),
426
426
  enabled: z.boolean().optional().describe("Whether the trigger is enabled (default: true)"),
427
427
  },
428
428
  async ({ name, functionId, executionType, description, triggerMetadata, enabled }) => {
@@ -917,6 +917,33 @@ export function registerDescribeTools(server: McpServer) {
917
917
  triggerMetadata_examples: {
918
918
  "event-driven": { eventType: "record_created", recordSlug: "orders" },
919
919
  scheduled: { scheduleType: "cron", cronExpression: "0 9 * * *", timezone: "America/New_York" },
920
+ "http-trigger_url_shape": "Public URL: <api-host>/data/workspace/<workspaceSlug>/api/v1/http-trigger/<path>. The path is supplied by the caller in triggerMetadata.path and must be URL-safe. The URL is NOT returned in the create_trigger / get_trigger response — derive it from the workspaceSlug + path.",
921
+ "http-trigger_setup_flow": "Webhook signing secrets are chicken-and-egg: providers issue the secret only AFTER you give them a URL. Recommended flow — (1) create_trigger with { path, provider, validateSignature: true } and NO signingSecret — the trigger is created in a pending state with a stable URL; (2) register that URL in the provider's dashboard; (3) once the provider returns the signing secret, call update_trigger with signingSecret set. Until step 3 lands, the trigger rejects every request with 'HMAC signing secret not configured for this endpoint' — that's the desired safe default during URL-registration.",
922
+ "http-trigger_provider_shortcut": {
923
+ _note: "Recommended — one preset wires every wire-format detail. Use this for Svix (Clerk, Resend, Loops, OpenAI, Brex), Stripe, Slack, GitHub, Shopify. Override individual fields only if a provider's scheme has drifted (e.g. custom hmacAlgorithm).",
924
+ path: "clerk-webhook",
925
+ validateSignature: true,
926
+ provider: "svix",
927
+ signingSecret: "whsec_…",
928
+ },
929
+ "http-trigger_advanced_raw": {
930
+ _note: "Skip the preset for full manual control — useful for non-standard schemes the built-in providers don't cover. Every field maps 1:1 to the runtime SignatureMetadata interface.",
931
+ path: "custom-webhook",
932
+ validateSignature: true,
933
+ signingSecret: "whsec_…",
934
+ signatureHeaderName: "x-custom-signature",
935
+ timestampHeaderName: "x-custom-timestamp",
936
+ extractionPattern: "^v1,(.+)$",
937
+ hmacAlgorithm: "sha256",
938
+ hmacEncoding: "base64",
939
+ secretEncoding: "prefixed-base64",
940
+ },
941
+ endpoint_provider_shortcut: {
942
+ _note: "Provider-named endpoint auth mode + colocated secret. Equivalent to mode: 'hmac' + triggerMetadata.provider — the service layer normalises auth.secret onto triggerMetadata.signingSecret at create/update time.",
943
+ path: "stripe-events",
944
+ allowedMethods: ["POST"],
945
+ auth: { mode: "stripe", secret: "whsec_…" },
946
+ },
920
947
  endpoint: { path: "create-order", allowedMethods: ["POST"], timeoutMs: 10000, auth: { mode: "bearer" } },
921
948
  },
922
949
  },
@@ -176,8 +176,18 @@ function translateQueryRecordsArgs(args: QueryRecordsArgs): {
176
176
  if (relations.length > 0) definition.include = relations;
177
177
  }
178
178
 
179
- // `includeTotal` is dropped `meta.total` is always returned for offset
180
- // pagination on POST /records/query. `includeDeleted` is rejected above.
179
+ // CEN-1259: count(*) is opt-in on POST /records/query. Default for the
180
+ // canonical surface is OFF (skip the duplicate predicate scan); legacy
181
+ // 5.4.0 callers expected `meta.total` always, so when the caller used a
182
+ // legacy field we preserve total-by-default. `includeTotal` (any surface)
183
+ // is the explicit override — including `includeTotal: false`, which must
184
+ // clear an existing canonical `page.withTotal: true` so the override is
185
+ // authoritative on both directions. `includeDeleted` is rejected above.
186
+ if (args.includeTotal !== undefined) {
187
+ definition.page = { ...(definition.page ?? { limit: 50 }), withTotal: args.includeTotal === true };
188
+ } else if (isLegacy) {
189
+ definition.page = { ...(definition.page ?? { limit: 50 }), withTotal: true };
190
+ }
181
191
 
182
192
  return { resource, definition };
183
193
  }
@@ -414,7 +424,7 @@ Returns the canonical { data, meta } envelope. 'select', 'text', and 'include' a
414
424
  includeTotal: z
415
425
  .boolean()
416
426
  .optional()
417
- .describe("[Deprecated 2026-10-28] Ignored `meta.total` is always returned for offset pagination."),
427
+ .describe("Set to true to request `meta.total` in the response. Default false — counting is skipped to keep list calls fast (CEN-1259)."),
418
428
 
419
429
  // Templating
420
430
  variables: z
@@ -46,7 +46,10 @@ test("full 5.4.0 legacy shape translates", () => {
46
46
  ],
47
47
  },
48
48
  sort: [{ field: "createdAt", direction: "desc" }],
49
- page: { limit: 50, offset: 50 },
49
+ // CEN-1259: legacy 5.4.0 callers expected `meta.total` always; the
50
+ // translator preserves total-by-default by setting `withTotal: true`
51
+ // when any legacy field was used.
52
+ page: { limit: 50, offset: 50, withTotal: true },
50
53
  },
51
54
  });
52
55
  });
@@ -134,19 +137,41 @@ test("sort: multi-sort comma-separated mixes asc/desc", () => {
134
137
  ]);
135
138
  });
136
139
 
137
- test("page without pageSize defaults to limit 50", () => {
140
+ test("page without pageSize defaults to limit 50 (legacy → withTotal:true)", () => {
138
141
  const out = translateQueryRecordsArgs({ recordSlug: "orders", page: 3 });
139
- assert.deepEqual(out.definition.page, { limit: 50, offset: 100 });
142
+ assert.deepEqual(out.definition.page, { limit: 50, offset: 100, withTotal: true });
140
143
  });
141
144
 
142
- test("pageSize without page → limit only", () => {
145
+ test("pageSize without page → limit only (legacy → withTotal:true)", () => {
143
146
  const out = translateQueryRecordsArgs({ recordSlug: "orders", pageSize: 25 });
144
- assert.deepEqual(out.definition.page, { limit: 25 });
147
+ assert.deepEqual(out.definition.page, { limit: 25, withTotal: true });
145
148
  });
146
149
 
147
- test("includeTotal silently dropped (meta.total always returned)", () => {
150
+ // CEN-1259: `includeTotal` is now an explicit override on any surface.
151
+ // Legacy callers that pass it (with `recordSlug`) get `withTotal` reflected
152
+ // directly; legacy callers that omit it get total-by-default for back-compat.
153
+ test("includeTotal:true sets withTotal:true (explicit opt-in)", () => {
148
154
  const out = translateQueryRecordsArgs({ recordSlug: "orders", includeTotal: true });
149
- assert.deepEqual(out.definition, { resource: "orders" });
155
+ assert.deepEqual(out.definition, { resource: "orders", page: { limit: 50, withTotal: true } });
156
+ });
157
+
158
+ test("includeTotal:false on legacy caller overrides default-on (explicit opt-out)", () => {
159
+ const out = translateQueryRecordsArgs({ recordSlug: "orders", includeTotal: false });
160
+ assert.deepEqual(out.definition, { resource: "orders", page: { limit: 50, withTotal: false } });
161
+ });
162
+
163
+ test("includeTotal:false clears canonical page.withTotal:true", () => {
164
+ const out = translateQueryRecordsArgs({
165
+ resource: "orders",
166
+ page: { limit: 25, withTotal: true },
167
+ includeTotal: false,
168
+ });
169
+ assert.deepEqual(out.definition.page, { limit: 25, withTotal: false });
170
+ });
171
+
172
+ test("canonical caller without includeTotal stays opt-out (no legacy default-on)", () => {
173
+ const out = translateQueryRecordsArgs({ resource: "orders", page: { limit: 25, offset: 0 } });
174
+ assert.deepEqual(out.definition.page, { limit: 25, offset: 0 });
150
175
  });
151
176
 
152
177
  test("includeDeleted: true throws clear error (no silent privacy regression)", () => {