@centrali-io/centrali-mcp 3.1.4 → 3.1.5

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.
@@ -2,6 +2,27 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
3
  import { z } from "zod";
4
4
 
5
+ function formatError(error: unknown, context: string): string {
6
+ if (error && typeof error === 'object') {
7
+ const e = error as Record<string, any>;
8
+ if ('message' in e) {
9
+ let msg = `Error ${context}`;
10
+ if ('code' in e || 'status' in e) {
11
+ msg += `: [${e.code ?? e.status ?? 'ERROR'}] ${e.message}`;
12
+ } else {
13
+ msg += `: ${e.message}`;
14
+ }
15
+ if (Array.isArray(e.fieldErrors) && e.fieldErrors.length > 0) {
16
+ msg += '\nField errors:\n' + (e.fieldErrors as Array<{field: string; message: string}>)
17
+ .map(f => ` ${f.field}: ${f.message}`)
18
+ .join('\n');
19
+ }
20
+ return msg;
21
+ }
22
+ }
23
+ return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
24
+ }
25
+
5
26
  export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
6
27
  server.tool(
7
28
  "list_functions",
@@ -24,12 +45,12 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
24
45
  { type: "text", text: JSON.stringify(result.data, null, 2) },
25
46
  ],
26
47
  };
27
- } catch (error: any) {
48
+ } catch (error: unknown) {
28
49
  return {
29
50
  content: [
30
51
  {
31
52
  type: "text",
32
- text: `Error listing functions: ${error.message}`,
53
+ text: formatError(error, "listing functions"),
33
54
  },
34
55
  ],
35
56
  isError: true,
@@ -64,12 +85,12 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
64
85
  { type: "text", text: JSON.stringify(result.data, null, 2) },
65
86
  ],
66
87
  };
67
- } catch (error: any) {
88
+ } catch (error: unknown) {
68
89
  return {
69
90
  content: [
70
91
  {
71
92
  type: "text",
72
- text: `Error listing triggers: ${error.message}`,
93
+ text: formatError(error, "listing triggers"),
73
94
  },
74
95
  ],
75
96
  isError: true,
@@ -104,12 +125,12 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
104
125
  },
105
126
  ],
106
127
  };
107
- } catch (error: any) {
128
+ } catch (error: unknown) {
108
129
  return {
109
130
  content: [
110
131
  {
111
132
  type: "text",
112
- text: `Error invoking trigger '${triggerId}': ${error.message}`,
133
+ text: formatError(error, `invoking trigger '${triggerId}'`),
113
134
  },
114
135
  ],
115
136
  isError: true,
@@ -0,0 +1,198 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ function formatError(error: unknown, context: string): string {
6
+ if (error && typeof error === "object") {
7
+ const e = error as Record<string, any>;
8
+ if ("message" in e) {
9
+ let msg = `Error ${context}`;
10
+ if ("code" in e || "status" in e) {
11
+ msg += `: [${e.code ?? e.status ?? "ERROR"}] ${e.message}`;
12
+ } else {
13
+ msg += `: ${e.message}`;
14
+ }
15
+ if (Array.isArray(e.fieldErrors) && e.fieldErrors.length > 0) {
16
+ msg +=
17
+ "\nField errors:\n" +
18
+ (e.fieldErrors as Array<{ field: string; message: string }>)
19
+ .map((f) => ` ${f.field}: ${f.message}`)
20
+ .join("\n");
21
+ }
22
+ return msg;
23
+ }
24
+ }
25
+ return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
26
+ }
27
+
28
+ export function registerInsightTools(server: McpServer, sdk: CentraliSDK) {
29
+ server.tool(
30
+ "list_insights",
31
+ "List anomaly insights detected by Centrali's AI analysis. Insights flag unusual patterns in your data such as spikes, drops, or outliers.",
32
+ {
33
+ structureSlug: z
34
+ .string()
35
+ .optional()
36
+ .describe(
37
+ "Filter insights by structure slug. If omitted, returns insights for all structures."
38
+ ),
39
+ status: z
40
+ .enum(["active", "acknowledged", "dismissed"])
41
+ .optional()
42
+ .describe("Filter by insight status"),
43
+ severity: z
44
+ .enum(["critical", "high", "medium", "low"])
45
+ .optional()
46
+ .describe("Filter by severity level"),
47
+ },
48
+ async ({ structureSlug, status, severity }) => {
49
+ try {
50
+ const options: Record<string, any> = {};
51
+ if (structureSlug) options.structureSlug = structureSlug;
52
+ if (status) options.status = status;
53
+ if (severity) options.severity = severity;
54
+
55
+ const result = await sdk.anomalyInsights.list(
56
+ Object.keys(options).length > 0 ? options : undefined
57
+ );
58
+ return {
59
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
60
+ };
61
+ } catch (error: unknown) {
62
+ return {
63
+ content: [{ type: "text", text: formatError(error, "listing insights") }],
64
+ isError: true,
65
+ };
66
+ }
67
+ }
68
+ );
69
+
70
+ server.tool(
71
+ "get_insight",
72
+ "Get full details of a single anomaly insight by ID.",
73
+ {
74
+ insightId: z.string().describe("The insight ID (UUID)"),
75
+ },
76
+ async ({ insightId }) => {
77
+ try {
78
+ const result = await sdk.anomalyInsights.get(insightId);
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
81
+ };
82
+ } catch (error: unknown) {
83
+ return {
84
+ content: [
85
+ { type: "text", text: formatError(error, `getting insight '${insightId}'`) },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ }
91
+ );
92
+
93
+ server.tool(
94
+ "acknowledge_insight",
95
+ "Mark an anomaly insight as acknowledged (reviewed/handled). The insight remains visible but is flagged as addressed.",
96
+ {
97
+ insightId: z.string().describe("The insight ID (UUID) to acknowledge"),
98
+ },
99
+ async ({ insightId }) => {
100
+ try {
101
+ const result = await sdk.anomalyInsights.acknowledge(insightId);
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
104
+ };
105
+ } catch (error: unknown) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: formatError(error, `acknowledging insight '${insightId}'`),
111
+ },
112
+ ],
113
+ isError: true,
114
+ };
115
+ }
116
+ }
117
+ );
118
+
119
+ server.tool(
120
+ "dismiss_insight",
121
+ "Dismiss an anomaly insight, marking it as a false positive or not relevant.",
122
+ {
123
+ insightId: z.string().describe("The insight ID (UUID) to dismiss"),
124
+ },
125
+ async ({ insightId }) => {
126
+ try {
127
+ const result = await sdk.anomalyInsights.dismiss(insightId);
128
+ return {
129
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
130
+ };
131
+ } catch (error: unknown) {
132
+ return {
133
+ content: [
134
+ { type: "text", text: formatError(error, `dismissing insight '${insightId}'`) },
135
+ ],
136
+ isError: true,
137
+ };
138
+ }
139
+ }
140
+ );
141
+
142
+ server.tool(
143
+ "get_insights_summary",
144
+ "Get a summary of anomaly insights in the workspace — counts by status and severity. Optionally filter by structure.",
145
+ {
146
+ structureSlug: z
147
+ .string()
148
+ .optional()
149
+ .describe(
150
+ "Filter summary to a specific structure. If omitted, returns workspace-wide summary."
151
+ ),
152
+ },
153
+ async ({ structureSlug }) => {
154
+ try {
155
+ const result = await sdk.anomalyInsights.getSummary(structureSlug);
156
+ return {
157
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
158
+ };
159
+ } catch (error: unknown) {
160
+ return {
161
+ content: [
162
+ { type: "text", text: formatError(error, "getting insights summary") },
163
+ ],
164
+ isError: true,
165
+ };
166
+ }
167
+ }
168
+ );
169
+
170
+ server.tool(
171
+ "trigger_anomaly_analysis",
172
+ "Trigger AI-powered anomaly analysis for a structure. Starts a background analysis to detect unusual patterns. Check list_insights after the scan completes.",
173
+ {
174
+ structureSlug: z.string().describe("The structure's record slug to analyze"),
175
+ },
176
+ async ({ structureSlug }) => {
177
+ try {
178
+ const result = await sdk.anomalyInsights.triggerAnalysis(structureSlug);
179
+ return {
180
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
181
+ };
182
+ } catch (error: unknown) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: formatError(
188
+ error,
189
+ `triggering anomaly analysis for '${structureSlug}'`
190
+ ),
191
+ },
192
+ ],
193
+ isError: true,
194
+ };
195
+ }
196
+ }
197
+ );
198
+ }
@@ -0,0 +1,209 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ function formatError(error: unknown, context: string): string {
6
+ if (error && typeof error === "object") {
7
+ const e = error as Record<string, any>;
8
+ if ("message" in e) {
9
+ let msg = `Error ${context}`;
10
+ if ("code" in e || "status" in e) {
11
+ msg += `: [${e.code ?? e.status ?? "ERROR"}] ${e.message}`;
12
+ } else {
13
+ msg += `: ${e.message}`;
14
+ }
15
+ if (Array.isArray(e.fieldErrors) && e.fieldErrors.length > 0) {
16
+ msg +=
17
+ "\nField errors:\n" +
18
+ (e.fieldErrors as Array<{ field: string; message: string }>)
19
+ .map((f) => ` ${f.field}: ${f.message}`)
20
+ .join("\n");
21
+ }
22
+ return msg;
23
+ }
24
+ }
25
+ return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
26
+ }
27
+
28
+ export function registerOrchestrationTools(server: McpServer, sdk: CentraliSDK) {
29
+ server.tool(
30
+ "list_orchestrations",
31
+ "List all orchestrations in the workspace. Orchestrations are multi-step workflows that chain compute functions together.",
32
+ {
33
+ offset: z.number().optional().describe("Number of results to skip (for pagination)"),
34
+ limit: z.number().optional().describe("Results per page"),
35
+ status: z
36
+ .enum(["draft", "active", "paused"])
37
+ .optional()
38
+ .describe("Filter by orchestration status"),
39
+ },
40
+ async ({ offset, limit, status }) => {
41
+ try {
42
+ const options: Record<string, any> = {};
43
+ if (offset !== undefined) options.offset = offset;
44
+ if (limit !== undefined) options.limit = limit;
45
+ if (status) options.status = status;
46
+
47
+ const result = await sdk.orchestrations.list(
48
+ Object.keys(options).length > 0 ? options : undefined
49
+ );
50
+ return {
51
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
52
+ };
53
+ } catch (error: unknown) {
54
+ return {
55
+ content: [{ type: "text", text: formatError(error, "listing orchestrations") }],
56
+ isError: true,
57
+ };
58
+ }
59
+ }
60
+ );
61
+
62
+ server.tool(
63
+ "get_orchestration",
64
+ "Get full details of a single orchestration by ID, including its step definitions.",
65
+ {
66
+ orchestrationId: z.string().describe("The orchestration ID (UUID)"),
67
+ },
68
+ async ({ orchestrationId }) => {
69
+ try {
70
+ const result = await sdk.orchestrations.get(orchestrationId);
71
+ return {
72
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
73
+ };
74
+ } catch (error: unknown) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: formatError(error, `getting orchestration '${orchestrationId}'`),
80
+ },
81
+ ],
82
+ isError: true,
83
+ };
84
+ }
85
+ }
86
+ );
87
+
88
+ server.tool(
89
+ "trigger_orchestration",
90
+ "Trigger an orchestration run. Creates a new run instance and starts executing the workflow. Optionally pass input data to the run.",
91
+ {
92
+ orchestrationId: z.string().describe("The orchestration ID (UUID) to trigger"),
93
+ input: z
94
+ .record(z.string(), z.any())
95
+ .optional()
96
+ .describe("Input data passed to the first step of the orchestration"),
97
+ correlationId: z
98
+ .string()
99
+ .optional()
100
+ .describe("Optional correlation ID for tracing this run"),
101
+ },
102
+ async ({ orchestrationId, input, correlationId }) => {
103
+ try {
104
+ const options: { input?: Record<string, any>; correlationId?: string } = {};
105
+ if (input) options.input = input;
106
+ if (correlationId) options.correlationId = correlationId;
107
+
108
+ const result = await sdk.orchestrations.trigger(
109
+ orchestrationId,
110
+ Object.keys(options).length > 0 ? options : undefined
111
+ );
112
+ return {
113
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
114
+ };
115
+ } catch (error: unknown) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: formatError(error, `triggering orchestration '${orchestrationId}'`),
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
125
+ }
126
+ }
127
+ );
128
+
129
+ server.tool(
130
+ "list_orchestration_runs",
131
+ "List all runs for an orchestration. Runs represent individual executions of the workflow.",
132
+ {
133
+ orchestrationId: z.string().describe("The orchestration ID (UUID)"),
134
+ offset: z.number().optional().describe("Number of results to skip (for pagination)"),
135
+ limit: z.number().optional().describe("Results per page"),
136
+ status: z
137
+ .enum(["pending", "running", "waiting", "completed", "failed"])
138
+ .optional()
139
+ .describe("Filter by run status"),
140
+ },
141
+ async ({ orchestrationId, offset, limit, status }) => {
142
+ try {
143
+ const options: Record<string, any> = {};
144
+ if (offset !== undefined) options.offset = offset;
145
+ if (limit !== undefined) options.limit = limit;
146
+ if (status) options.status = status;
147
+
148
+ const result = await sdk.orchestrations.listRuns(
149
+ orchestrationId,
150
+ Object.keys(options).length > 0 ? options : undefined
151
+ );
152
+ return {
153
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
154
+ };
155
+ } catch (error: unknown) {
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: formatError(
161
+ error,
162
+ `listing runs for orchestration '${orchestrationId}'`
163
+ ),
164
+ },
165
+ ],
166
+ isError: true,
167
+ };
168
+ }
169
+ }
170
+ );
171
+
172
+ server.tool(
173
+ "get_orchestration_run",
174
+ "Get details of a specific orchestration run. Set includeSteps=true to see step-by-step execution history.",
175
+ {
176
+ orchestrationId: z.string().describe("The orchestration ID (UUID)"),
177
+ runId: z.string().describe("The run ID (UUID)"),
178
+ includeSteps: z
179
+ .boolean()
180
+ .optional()
181
+ .describe("Include step execution history in the response (default: false)"),
182
+ },
183
+ async ({ orchestrationId, runId, includeSteps }) => {
184
+ try {
185
+ const result = await sdk.orchestrations.getRun(
186
+ orchestrationId,
187
+ runId,
188
+ includeSteps
189
+ );
190
+ return {
191
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
192
+ };
193
+ } catch (error: unknown) {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: formatError(
199
+ error,
200
+ `getting run '${runId}' for orchestration '${orchestrationId}'`
201
+ ),
202
+ },
203
+ ],
204
+ isError: true,
205
+ };
206
+ }
207
+ }
208
+ );
209
+ }
@@ -2,6 +2,27 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
3
  import { z } from "zod";
4
4
 
5
+ function formatError(error: unknown, context: string): string {
6
+ if (error && typeof error === 'object') {
7
+ const e = error as Record<string, any>;
8
+ if ('message' in e) {
9
+ let msg = `Error ${context}`;
10
+ if ('code' in e || 'status' in e) {
11
+ msg += `: [${e.code ?? e.status ?? 'ERROR'}] ${e.message}`;
12
+ } else {
13
+ msg += `: ${e.message}`;
14
+ }
15
+ if (Array.isArray(e.fieldErrors) && e.fieldErrors.length > 0) {
16
+ msg += '\nField errors:\n' + (e.fieldErrors as Array<{field: string; message: string}>)
17
+ .map(f => ` ${f.field}: ${f.message}`)
18
+ .join('\n');
19
+ }
20
+ return msg;
21
+ }
22
+ }
23
+ return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
24
+ }
25
+
5
26
  export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
6
27
  server.tool(
7
28
  "query_records",
@@ -54,12 +75,12 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
54
75
  { type: "text", text: JSON.stringify(result, null, 2) },
55
76
  ],
56
77
  };
57
- } catch (error: any) {
78
+ } catch (error: unknown) {
58
79
  return {
59
80
  content: [
60
81
  {
61
82
  type: "text",
62
- text: `Error querying records from '${recordSlug}': ${error.message}`,
83
+ text: formatError(error, `querying records from '${recordSlug}'`),
63
84
  },
64
85
  ],
65
86
  isError: true,
@@ -88,12 +109,12 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
88
109
  { type: "text", text: JSON.stringify(result.data, null, 2) },
89
110
  ],
90
111
  };
91
- } catch (error: any) {
112
+ } catch (error: unknown) {
92
113
  return {
93
114
  content: [
94
115
  {
95
116
  type: "text",
96
- text: `Error getting record '${id}' from '${recordSlug}': ${error.message}`,
117
+ text: formatError(error, `getting record '${id}' from '${recordSlug}'`),
97
118
  },
98
119
  ],
99
120
  isError: true,
@@ -121,12 +142,12 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
121
142
  { type: "text", text: JSON.stringify(result.data, null, 2) },
122
143
  ],
123
144
  };
124
- } catch (error: any) {
145
+ } catch (error: unknown) {
125
146
  return {
126
147
  content: [
127
148
  {
128
149
  type: "text",
129
- text: `Error creating record in '${recordSlug}': ${error.message}`,
150
+ text: formatError(error, `creating record in '${recordSlug}'`),
130
151
  },
131
152
  ],
132
153
  isError: true,
@@ -153,12 +174,12 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
153
174
  { type: "text", text: JSON.stringify(result.data, null, 2) },
154
175
  ],
155
176
  };
156
- } catch (error: any) {
177
+ } catch (error: unknown) {
157
178
  return {
158
179
  content: [
159
180
  {
160
181
  type: "text",
161
- text: `Error updating record '${id}' in '${recordSlug}': ${error.message}`,
182
+ text: formatError(error, `updating record '${id}' in '${recordSlug}'`),
162
183
  },
163
184
  ],
164
185
  isError: true,
@@ -193,14 +214,102 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
193
214
  },
194
215
  ],
195
216
  };
196
- } catch (error: any) {
217
+ } catch (error: unknown) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: formatError(error, `deleting record '${id}' from '${recordSlug}'`),
223
+ },
224
+ ],
225
+ isError: true,
226
+ };
227
+ }
228
+ }
229
+ );
230
+
231
+ server.tool(
232
+ "upsert_record",
233
+ "Create or update a record atomically. Matches on the provided fields to find an existing record — updates it if found, creates it if not. Returns the record and whether it was 'created' or 'updated'.",
234
+ {
235
+ recordSlug: z.string().describe("The structure's record slug"),
236
+ match: z
237
+ .record(z.string(), z.any())
238
+ .describe("Fields to match on when looking for an existing record (e.g. { sku: 'WIDGET-001' })"),
239
+ data: z
240
+ .record(z.string(), z.any())
241
+ .describe("Full record data to create or update with"),
242
+ },
243
+ async ({ recordSlug, match, data }) => {
244
+ try {
245
+ const result = await sdk.upsertRecord(recordSlug, { match, data });
197
246
  return {
198
247
  content: [
199
248
  {
200
249
  type: "text",
201
- text: `Error deleting record '${id}' from '${recordSlug}': ${error.message}`,
250
+ text: JSON.stringify(
251
+ { operation: result.operation, record: result.data },
252
+ null,
253
+ 2
254
+ ),
202
255
  },
203
256
  ],
257
+ };
258
+ } catch (error: unknown) {
259
+ return {
260
+ content: [
261
+ { type: "text", text: formatError(error, `upserting record in '${recordSlug}'`) },
262
+ ],
263
+ isError: true,
264
+ };
265
+ }
266
+ }
267
+ );
268
+
269
+ server.tool(
270
+ "get_records_by_ids",
271
+ "Fetch multiple records by their IDs in a single request. Returns an array of records.",
272
+ {
273
+ recordSlug: z.string().describe("The structure's record slug"),
274
+ ids: z.array(z.string()).describe("Array of record IDs (UUIDs) to fetch"),
275
+ },
276
+ async ({ recordSlug, ids }) => {
277
+ try {
278
+ const result = await sdk.getRecordsByIds(recordSlug, ids);
279
+ return {
280
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
281
+ };
282
+ } catch (error: unknown) {
283
+ return {
284
+ content: [
285
+ { type: "text", text: formatError(error, `fetching records by IDs from '${recordSlug}'`) },
286
+ ],
287
+ isError: true,
288
+ };
289
+ }
290
+ }
291
+ );
292
+
293
+ server.tool(
294
+ "restore_record",
295
+ "Restore a soft-deleted record by ID. Only works on records deleted without hard=true.",
296
+ {
297
+ recordSlug: z.string().describe("The structure's record slug"),
298
+ id: z.string().describe("The record ID (UUID) to restore"),
299
+ },
300
+ async ({ recordSlug, id }) => {
301
+ try {
302
+ await sdk.restoreRecord(recordSlug, id);
303
+ return {
304
+ content: [
305
+ { type: "text", text: `Record '${id}' restored in '${recordSlug}'.` },
306
+ ],
307
+ };
308
+ } catch (error: unknown) {
309
+ return {
310
+ content: [
311
+ { type: "text", text: formatError(error, `restoring record '${id}' in '${recordSlug}'`) },
312
+ ],
204
313
  isError: true,
205
314
  };
206
315
  }