@centrali-io/centrali-mcp 5.2.0 → 5.3.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.
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.registerWebhookSubscriptionTools = registerWebhookSubscriptionTools;
13
+ const zod_1 = require("zod");
14
+ const _register_js_1 = require("./_register.js");
15
+ const RECORD_EVENT_VALUES = [
16
+ "record_created",
17
+ "record_updated",
18
+ "record_deleted",
19
+ "records_bulk_created",
20
+ ];
21
+ const DELIVERY_STATUS_VALUES = ["success", "failed", "retrying"];
22
+ function registerWebhookSubscriptionTools(server, sdk) {
23
+ // ── Subscription CRUD ──────────────────────────────────────────
24
+ (0, _register_js_1.registerTool)(server, "list_webhook_subscriptions", "List all webhook subscriptions in the workspace. Subscriptions deliver record events to an external URL, signed with HMAC-SHA256 under the subscription's `whsec_` secret.", {}, () => __awaiter(this, void 0, void 0, function* () {
25
+ try {
26
+ const result = yield sdk.webhookSubscriptions.list();
27
+ return {
28
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
29
+ };
30
+ }
31
+ catch (error) {
32
+ return {
33
+ content: [{ type: "text", text: (0, _register_js_1.formatError)(error, "listing webhook subscriptions") }],
34
+ isError: true,
35
+ };
36
+ }
37
+ }));
38
+ (0, _register_js_1.registerTool)(server, "get_webhook_subscription", "Get a single webhook subscription by ID. The signing secret is not returned on reads — use rotate_webhook_subscription_secret to regenerate it.", {
39
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
40
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId }) {
41
+ try {
42
+ const result = yield sdk.webhookSubscriptions.get(subscriptionId);
43
+ return {
44
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
45
+ };
46
+ }
47
+ catch (error) {
48
+ return {
49
+ content: [
50
+ { type: "text", text: (0, _register_js_1.formatError)(error, `getting webhook subscription '${subscriptionId}'`) },
51
+ ],
52
+ isError: true,
53
+ };
54
+ }
55
+ }));
56
+ (0, _register_js_1.registerTool)(server, "create_webhook_subscription", "Create a webhook subscription. The response includes the signing secret (whsec_...) — capture it immediately, it is not returned on subsequent reads.", {
57
+ name: zod_1.z.string().describe("Display name for the subscription"),
58
+ url: zod_1.z.string().describe("HTTPS URL that will receive POSTed event payloads"),
59
+ events: zod_1.z
60
+ .array(zod_1.z.enum(RECORD_EVENT_VALUES))
61
+ .describe("Record events to subscribe to: record_created, record_updated, record_deleted, records_bulk_created"),
62
+ recordSlugs: zod_1.z
63
+ .array(zod_1.z.string())
64
+ .optional()
65
+ .describe("Optional list of collection record slugs to scope the subscription to. Omit for all collections."),
66
+ active: zod_1.z
67
+ .boolean()
68
+ .optional()
69
+ .describe("Whether the subscription is active on creation. Defaults to true."),
70
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ name, url, events, recordSlugs, active }) {
71
+ try {
72
+ const input = { name, url, events };
73
+ if (recordSlugs !== undefined)
74
+ input.recordSlugs = recordSlugs;
75
+ if (active !== undefined)
76
+ input.active = active;
77
+ const result = yield sdk.webhookSubscriptions.create(input);
78
+ return {
79
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
80
+ };
81
+ }
82
+ catch (error) {
83
+ return {
84
+ content: [
85
+ { type: "text", text: (0, _register_js_1.formatError)(error, `creating webhook subscription '${name}'`) },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ }));
91
+ (0, _register_js_1.registerTool)(server, "update_webhook_subscription", "Update fields on a webhook subscription. The signing secret is server-managed — use rotate_webhook_subscription_secret to change it.", {
92
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
93
+ name: zod_1.z.string().optional().describe("Updated display name"),
94
+ url: zod_1.z.string().optional().describe("Updated HTTPS URL"),
95
+ events: zod_1.z
96
+ .array(zod_1.z.enum(RECORD_EVENT_VALUES))
97
+ .optional()
98
+ .describe("Updated list of subscribed events"),
99
+ recordSlugs: zod_1.z
100
+ .array(zod_1.z.string())
101
+ .optional()
102
+ .describe("Updated collection scope. Pass [] to clear the restriction (subscribe to all collections). Omit to leave scope unchanged."),
103
+ active: zod_1.z.boolean().optional().describe("Enable or disable the subscription"),
104
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId, name, url, events, recordSlugs, active }) {
105
+ try {
106
+ const patch = {};
107
+ if (name !== undefined)
108
+ patch.name = name;
109
+ if (url !== undefined)
110
+ patch.url = url;
111
+ if (events !== undefined)
112
+ patch.events = events;
113
+ if (recordSlugs !== undefined)
114
+ patch.recordSlugs = recordSlugs;
115
+ if (active !== undefined)
116
+ patch.active = active;
117
+ const result = yield sdk.webhookSubscriptions.update(subscriptionId, patch);
118
+ return {
119
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
120
+ };
121
+ }
122
+ catch (error) {
123
+ return {
124
+ content: [
125
+ {
126
+ type: "text",
127
+ text: (0, _register_js_1.formatError)(error, `updating webhook subscription '${subscriptionId}'`),
128
+ },
129
+ ],
130
+ isError: true,
131
+ };
132
+ }
133
+ }));
134
+ (0, _register_js_1.registerTool)(server, "delete_webhook_subscription", "Delete a webhook subscription. Historical delivery records are retained but no new deliveries will be dispatched.", {
135
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
136
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId }) {
137
+ try {
138
+ yield sdk.webhookSubscriptions.delete(subscriptionId);
139
+ return {
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: `Webhook subscription '${subscriptionId}' deleted successfully.`,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ catch (error) {
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: (0, _register_js_1.formatError)(error, `deleting webhook subscription '${subscriptionId}'`),
154
+ },
155
+ ],
156
+ isError: true,
157
+ };
158
+ }
159
+ }));
160
+ (0, _register_js_1.registerTool)(server, "rotate_webhook_subscription_secret", "Rotate the signing secret on a webhook subscription. Immediate cutover — the old secret stops signing on the next dispatch. The response includes the new secret; capture it before closing.", {
161
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
162
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId }) {
163
+ try {
164
+ const result = yield sdk.webhookSubscriptions.rotateSecret(subscriptionId);
165
+ return {
166
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
167
+ };
168
+ }
169
+ catch (error) {
170
+ return {
171
+ content: [
172
+ {
173
+ type: "text",
174
+ text: (0, _register_js_1.formatError)(error, `rotating secret for webhook subscription '${subscriptionId}'`),
175
+ },
176
+ ],
177
+ isError: true,
178
+ };
179
+ }
180
+ }));
181
+ // ── Delivery history + replay ──────────────────────────────────
182
+ (0, _register_js_1.registerTool)(server, "list_webhook_deliveries", "List delivery history for a webhook subscription. Rows omit requestPayload and responseBody — use get_webhook_delivery to fetch full payload and response body for a specific delivery.", {
183
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
184
+ status: zod_1.z
185
+ .enum(DELIVERY_STATUS_VALUES)
186
+ .optional()
187
+ .describe("Filter deliveries by status"),
188
+ since: zod_1.z
189
+ .string()
190
+ .optional()
191
+ .describe("ISO 8601 datetime — include deliveries created at or after this time"),
192
+ until: zod_1.z
193
+ .string()
194
+ .optional()
195
+ .describe("ISO 8601 datetime — include deliveries created at or before this time"),
196
+ limit: zod_1.z.number().optional().describe("Max rows to return (default 50)"),
197
+ offset: zod_1.z.number().optional().describe("Number of rows to skip for pagination"),
198
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId, status, since, until, limit, offset }) {
199
+ try {
200
+ const options = {};
201
+ if (status)
202
+ options.status = status;
203
+ if (since)
204
+ options.since = since;
205
+ if (until)
206
+ options.until = until;
207
+ if (limit !== undefined)
208
+ options.limit = limit;
209
+ if (offset !== undefined)
210
+ options.offset = offset;
211
+ const result = yield sdk.webhookSubscriptions.deliveries.list(subscriptionId, Object.keys(options).length > 0 ? options : undefined);
212
+ return {
213
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
214
+ };
215
+ }
216
+ catch (error) {
217
+ return {
218
+ content: [
219
+ {
220
+ type: "text",
221
+ text: (0, _register_js_1.formatError)(error, `listing deliveries for webhook subscription '${subscriptionId}'`),
222
+ },
223
+ ],
224
+ isError: true,
225
+ };
226
+ }
227
+ }));
228
+ (0, _register_js_1.registerTool)(server, "get_webhook_delivery", "Get a single webhook delivery including the full request payload and response body.", {
229
+ subscriptionId: zod_1.z.string().describe("The webhook subscription UUID"),
230
+ deliveryId: zod_1.z.string().describe("The delivery UUID"),
231
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ subscriptionId, deliveryId }) {
232
+ try {
233
+ const result = yield sdk.webhookSubscriptions.deliveries.get(subscriptionId, deliveryId);
234
+ return {
235
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
236
+ };
237
+ }
238
+ catch (error) {
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: (0, _register_js_1.formatError)(error, `getting webhook delivery '${deliveryId}'`),
244
+ },
245
+ ],
246
+ isError: true,
247
+ };
248
+ }
249
+ }));
250
+ (0, _register_js_1.registerTool)(server, "retry_webhook_delivery", "Replay a previously recorded webhook delivery. Queues a new delivery row pointing at the original via replayedFrom; the retry worker picks it up within ~1 second. The original payload and signature are reused.", {
251
+ deliveryId: zod_1.z.string().describe("The delivery UUID to replay"),
252
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ deliveryId }) {
253
+ try {
254
+ const result = yield sdk.webhookSubscriptions.deliveries.retry(deliveryId);
255
+ return {
256
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
257
+ };
258
+ }
259
+ catch (error) {
260
+ return {
261
+ content: [
262
+ {
263
+ type: "text",
264
+ text: (0, _register_js_1.formatError)(error, `replaying webhook delivery '${deliveryId}'`),
265
+ },
266
+ ],
267
+ isError: true,
268
+ };
269
+ }
270
+ }));
271
+ (0, _register_js_1.registerTool)(server, "cancel_webhook_delivery", "Cancel a webhook delivery that is currently in 'retrying' status. Flips the row to 'failed' with lastError set to 'Cancelled by user'. Returns 409 if the delivery is not retrying or is mid-dispatch.", {
272
+ deliveryId: zod_1.z.string().describe("The delivery UUID to cancel"),
273
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ deliveryId }) {
274
+ try {
275
+ const result = yield sdk.webhookSubscriptions.deliveries.cancel(deliveryId);
276
+ return {
277
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
278
+ };
279
+ }
280
+ catch (error) {
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: (0, _register_js_1.formatError)(error, `cancelling webhook delivery '${deliveryId}'`),
286
+ },
287
+ ],
288
+ isError: true,
289
+ };
290
+ }
291
+ }));
292
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -13,8 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "build": "tsc --project tsconfig.json",
16
- "prepublishOnly": "npm run build",
17
- "test": "echo \"Error: no test specified\" && exit 1"
16
+ "prepublishOnly": "npm run build"
18
17
  },
19
18
  "keywords": [
20
19
  "mcp",
@@ -25,13 +24,13 @@
25
24
  "author": "Blueinit",
26
25
  "license": "ISC",
27
26
  "dependencies": {
28
- "@centrali-io/centrali-sdk": "^4.4.8",
29
- "@modelcontextprotocol/sdk": "^1.12.1"
27
+ "@centrali-io/centrali-sdk": "^5.3.0",
28
+ "@modelcontextprotocol/sdk": "^1.28.0"
30
29
  },
31
30
  "devDependencies": {
32
31
  "@types/node": "^25.3.0",
33
32
  "@types/qs": "^6.14.0",
34
- "typescript": "^5.8.3"
33
+ "typescript": "5.9.3"
35
34
  },
36
35
  "overrides": {
37
36
  "@hono/node-server": "^1.19.10",
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import { registerPageTools } from "./tools/pages.js";
15
15
  import { registerDescribeTools } from "./tools/describe.js";
16
16
  import { registerAuthProviderTools } from "./tools/auth-providers.js";
17
17
  import { registerServiceAccountTools } from "./tools/service-accounts.js";
18
+ import { registerWebhookSubscriptionTools } from "./tools/webhook-subscriptions.js";
18
19
  import { registerStructureResources, registerCollectionResources } from "./resources/structures.js";
19
20
 
20
21
  function getRequiredEnv(name: string): string {
@@ -79,6 +80,7 @@ async function main() {
79
80
  clientId,
80
81
  isServiceAccount: true,
81
82
  });
83
+ registerWebhookSubscriptionTools(server, sdk);
82
84
  registerDescribeTools(server);
83
85
 
84
86
  // Register resources
@@ -0,0 +1,53 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ZodRawShape } from "zod";
3
+
4
+ export type ToolResult = {
5
+ content: Array<{ type: "text"; text: string }>;
6
+ isError?: boolean;
7
+ };
8
+
9
+ /**
10
+ * Thin wrapper around `server.tool(...)` that severs the MCP SDK's deep
11
+ * Zod-shape → handler-arg type inference. Each direct call to `server.tool()`
12
+ * forces TypeScript to elaborate a ~10M-instantiation type graph (MCP SDK
13
+ * `ShapeOutput` generics over Zod raw shapes), which OOMs tsc at ~4 GB.
14
+ *
15
+ * This helper casts the schema and handler at the MCP boundary, so tsc never
16
+ * walks that graph. Runtime behavior is identical — the MCP SDK still
17
+ * validates the payload against the Zod schema before invoking the handler.
18
+ *
19
+ * Call sites annotate the arg shape explicitly via the `Args` generic so the
20
+ * handler body stays fully type-safe.
21
+ */
22
+ export function registerTool<Args>(
23
+ server: McpServer,
24
+ name: string,
25
+ description: string,
26
+ schema: ZodRawShape,
27
+ handler: (args: Args) => Promise<ToolResult>
28
+ ): void {
29
+ (server.tool as any)(name, description, schema, handler);
30
+ }
31
+
32
+ export function formatError(error: unknown, context: string): string {
33
+ if (error && typeof error === "object") {
34
+ const e = error as Record<string, any>;
35
+ if ("message" in e) {
36
+ let msg = `Error ${context}`;
37
+ if ("code" in e || "status" in e) {
38
+ msg += `: [${e.code ?? e.status ?? "ERROR"}] ${e.message}`;
39
+ } else {
40
+ msg += `: ${e.message}`;
41
+ }
42
+ if (Array.isArray(e.fieldErrors) && e.fieldErrors.length > 0) {
43
+ msg +=
44
+ "\nField errors:\n" +
45
+ (e.fieldErrors as Array<{ field: string; message: string }>)
46
+ .map((f) => ` ${f.field}: ${f.message}`)
47
+ .join("\n");
48
+ }
49
+ return msg;
50
+ }
51
+ }
52
+ return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
53
+ }
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
3
  import axios, { AxiosInstance } from "axios";
4
4
  import { z } from "zod";
5
-
5
+ import { registerTool, formatError } from "./_register.js";
6
6
  /**
7
7
  * Ensures the SDK has a valid token by making a lightweight SDK call if needed.
8
8
  */
@@ -62,22 +62,6 @@ function createIamClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: str
62
62
  return client;
63
63
  }
64
64
 
65
- function formatError(error: unknown, context: string): string {
66
- if (error && typeof error === "object") {
67
- const e = error as Record<string, any>;
68
- if (e.response?.data) {
69
- const d = e.response.data;
70
- const code = d.code ?? d.error?.code ?? e.response.status ?? "ERROR";
71
- const message = d.message ?? d.error?.message ?? JSON.stringify(d);
72
- return `Error ${context}: [${code}] ${message}`;
73
- }
74
- if ("message" in e) {
75
- return `Error ${context}: ${e.message}`;
76
- }
77
- }
78
- return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
79
- }
80
-
81
65
  const ClaimMappingZod = z.object({
82
66
  attribute: z.string().describe("Attribute name used in policies (automatically prefixed with ext_). Must start with a letter, alphanumeric + underscores only. Example: 'role' becomes ext_role in policies."),
83
67
  jwtPath: z.string().describe("Dot-notation path to extract from the JWT payload. Examples: 'org_role', 'metadata.plan', 'organization.role'"),
@@ -91,7 +75,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
91
75
 
92
76
  // ── List ──────────────────────────────────────────────────────────
93
77
 
94
- server.tool(
78
+ registerTool<any>(server,
95
79
  "list_auth_providers",
96
80
  "List external auth providers configured in the workspace. These are identity providers (Clerk, Auth0, Okta, etc.) that can issue JWTs accepted by Centrali for BYOT (Bring Your Own Token) authorization.",
97
81
  {
@@ -116,7 +100,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
116
100
 
117
101
  // ── Get ───────────────────────────────────────────────────────────
118
102
 
119
- server.tool(
103
+ registerTool<any>(server,
120
104
  "get_auth_provider",
121
105
  "Get details of an external auth provider by ID, including claim mappings and JWKS configuration.",
122
106
  {
@@ -139,7 +123,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
139
123
 
140
124
  // ── Create ────────────────────────────────────────────────────────
141
125
 
142
- server.tool(
126
+ registerTool<any>(server,
143
127
  "create_auth_provider",
144
128
  "Create an external auth provider for BYOT (Bring Your Own Token). This lets your app's users authenticate with Clerk, Auth0, Okta, or any OIDC provider, and Centrali validates their JWTs and extracts claims for authorization policies. Call describe_auth_providers for the full setup guide.",
145
129
  {
@@ -179,7 +163,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
179
163
 
180
164
  // ── Update ────────────────────────────────────────────────────────
181
165
 
182
- server.tool(
166
+ registerTool<any>(server,
183
167
  "update_auth_provider",
184
168
  "Update an external auth provider. Only include the fields you want to change. Use this to update claim mappings, allowed audiences, or deactivate a provider.",
185
169
  {
@@ -220,7 +204,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
220
204
 
221
205
  // ── Delete ────────────────────────────────────────────────────────
222
206
 
223
- server.tool(
207
+ registerTool<any>(server,
224
208
  "delete_auth_provider",
225
209
  "Delete an external auth provider. Tokens from this provider will no longer be accepted.",
226
210
  {
@@ -243,7 +227,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
243
227
 
244
228
  // ── Test Claim Extraction ─────────────────────────────────────────
245
229
 
246
- server.tool(
230
+ registerTool<any>(server,
247
231
  "test_auth_provider",
248
232
  "Test claim extraction for an auth provider by passing a sample JWT. Decodes the token (without signature verification) and shows which claims would be extracted using the provider's configured mappings. Use this to validate your claim mappings before deploying.",
249
233
  {
@@ -267,7 +251,7 @@ export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, c
267
251
 
268
252
  // ── Refresh JWKS ──────────────────────────────────────────────────
269
253
 
270
- server.tool(
254
+ registerTool<any>(server,
271
255
  "refresh_auth_provider_jwks",
272
256
  "Force refresh the cached JWKS (JSON Web Key Set) for an auth provider. Use this after your identity provider rotates its signing keys.",
273
257
  {