@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.
- package/dist/tools/compute.js +1 -1
- package/dist/tools/describe.js +27 -0
- package/dist/tools/records.js +15 -4
- package/package.json +2 -2
- package/src/tools/compute.ts +1 -1
- package/src/tools/describe.ts +27 -0
- package/src/tools/records.ts +13 -3
- package/tests/records.translator.test.cjs +32 -7
package/dist/tools/compute.js
CHANGED
|
@@ -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:
|
|
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 {
|
package/dist/tools/describe.js
CHANGED
|
@@ -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
|
},
|
package/dist/tools/records.js
CHANGED
|
@@ -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
|
-
//
|
|
178
|
-
//
|
|
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("
|
|
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.
|
|
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.
|
|
28
|
+
"@centrali-io/centrali-sdk": "^6.1.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.28.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/src/tools/compute.ts
CHANGED
|
@@ -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:
|
|
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 }) => {
|
package/src/tools/describe.ts
CHANGED
|
@@ -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
|
},
|
package/src/tools/records.ts
CHANGED
|
@@ -176,8 +176,18 @@ function translateQueryRecordsArgs(args: QueryRecordsArgs): {
|
|
|
176
176
|
if (relations.length > 0) definition.include = relations;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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)", () => {
|