@centrali-io/centrali-mcp 5.4.0 → 5.5.1
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 +12 -9
- package/dist/tools/_register.d.ts +9 -0
- package/dist/tools/_register.js +9 -0
- package/dist/tools/describe.js +102 -86
- package/dist/tools/records.d.ts +35 -0
- package/dist/tools/records.js +226 -30
- package/dist/tools/smart-queries.js +58 -36
- package/package.json +3 -2
- package/src/tools/_register.ts +9 -0
- package/src/tools/describe.ts +106 -93
- package/src/tools/records.ts +273 -52
- package/src/tools/smart-queries.ts +70 -43
- package/tests/records.translator.test.cjs +177 -0
package/src/tools/records.ts
CHANGED
|
@@ -3,6 +3,187 @@ 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
|
+
|
|
7
|
+
// ── Legacy 5.4.0 query_records translator (Sunset 2026-10-28) ──────────────
|
|
8
|
+
//
|
|
9
|
+
// 5.4.0 MCP clients call query_records with:
|
|
10
|
+
// { recordSlug, filters: { 'data.x[gte]': 1 }, sort: '-createdAt',
|
|
11
|
+
// page: 2, pageSize: 50, expand: 'customer', dateWindow: {...},
|
|
12
|
+
// includeDeleted, includeTotal }
|
|
13
|
+
//
|
|
14
|
+
// Canonical (Phase 1) accepts:
|
|
15
|
+
// { resource, where, sort: [{field, direction}], page: { limit, offset } }
|
|
16
|
+
//
|
|
17
|
+
// We accept BOTH on the wire and rewrite legacy → canonical before calling the
|
|
18
|
+
// SDK, matching the data-service contract promise of long-lived legacy reads.
|
|
19
|
+
|
|
20
|
+
const LEGACY_OP_MAP: Record<string, string> = {
|
|
21
|
+
$eq: "eq", $ne: "ne", $gt: "gt", $gte: "gte", $lt: "lt", $lte: "lte",
|
|
22
|
+
$in: "in", $nin: "nin", $contains: "contains",
|
|
23
|
+
$startsWith: "startsWith", $endsWith: "endsWith", $exists: "exists",
|
|
24
|
+
eq: "eq", ne: "ne", gt: "gt", gte: "gte", lt: "lt", lte: "lte",
|
|
25
|
+
in: "in", nin: "nin", contains: "contains",
|
|
26
|
+
startswith: "startsWith", endswith: "endsWith",
|
|
27
|
+
hasany: "hasAny", hasall: "hasAll",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let legacyWarned = false;
|
|
31
|
+
function warnLegacyOnce() {
|
|
32
|
+
if (!legacyWarned) {
|
|
33
|
+
legacyWarned = true;
|
|
34
|
+
console.warn(
|
|
35
|
+
"[centrali-mcp] query_records called with the legacy 5.4.0 schema " +
|
|
36
|
+
"(recordSlug/filters/sort:string/pageSize). It still works but will be " +
|
|
37
|
+
"removed after 2026-10-28. Migrate to: { resource, where, " +
|
|
38
|
+
"sort: [{field,direction}], page: { limit, offset } }.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseLegacyFilters(filters: unknown): unknown {
|
|
44
|
+
if (filters == null || typeof filters !== "object" || Array.isArray(filters)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const conditions: Array<Record<string, unknown>> = [];
|
|
48
|
+
for (const [rawKey, value] of Object.entries(filters as Record<string, unknown>)) {
|
|
49
|
+
const m = rawKey.match(/^(.+?)\[\$?([a-zA-Z]+)\]$/);
|
|
50
|
+
if (m) {
|
|
51
|
+
const [, field, op] = m;
|
|
52
|
+
const canonOp = LEGACY_OP_MAP[op] ?? LEGACY_OP_MAP[op.toLowerCase()] ?? op;
|
|
53
|
+
conditions.push({ [field]: { [canonOp]: value } });
|
|
54
|
+
} else {
|
|
55
|
+
conditions.push({ [rawKey]: { eq: value } });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (conditions.length === 0) return undefined;
|
|
59
|
+
if (conditions.length === 1) return conditions[0];
|
|
60
|
+
return { and: conditions };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseLegacySort(sort: string): Array<{ field: string; direction: "asc" | "desc" }> {
|
|
64
|
+
return sort
|
|
65
|
+
.split(",")
|
|
66
|
+
.map((s) => s.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((s) =>
|
|
69
|
+
s.startsWith("-")
|
|
70
|
+
? { field: s.slice(1), direction: "desc" as const }
|
|
71
|
+
: { field: s, direction: "asc" as const },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function dateWindowToWhere(dw: { field: string; from?: string; to?: string }): unknown {
|
|
76
|
+
const conds: Array<Record<string, unknown>> = [];
|
|
77
|
+
if (dw.from !== undefined) conds.push({ [dw.field]: { gte: dw.from } });
|
|
78
|
+
if (dw.to !== undefined) conds.push({ [dw.field]: { lte: dw.to } });
|
|
79
|
+
if (conds.length === 0) return undefined;
|
|
80
|
+
if (conds.length === 1) return conds[0];
|
|
81
|
+
return { and: conds };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type QueryRecordsArgs = {
|
|
85
|
+
resource?: string;
|
|
86
|
+
recordSlug?: string;
|
|
87
|
+
where?: unknown;
|
|
88
|
+
text?: unknown;
|
|
89
|
+
sort?: unknown;
|
|
90
|
+
page?: unknown;
|
|
91
|
+
pageSize?: number;
|
|
92
|
+
select?: unknown;
|
|
93
|
+
include?: unknown;
|
|
94
|
+
filters?: Record<string, unknown>;
|
|
95
|
+
expand?: string;
|
|
96
|
+
dateWindow?: { field: string; from?: string; to?: string };
|
|
97
|
+
includeDeleted?: boolean;
|
|
98
|
+
includeTotal?: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function translateQueryRecordsArgs(args: QueryRecordsArgs): {
|
|
102
|
+
resource: string;
|
|
103
|
+
definition: Record<string, unknown>;
|
|
104
|
+
} {
|
|
105
|
+
const isLegacy =
|
|
106
|
+
args.recordSlug !== undefined ||
|
|
107
|
+
args.filters !== undefined ||
|
|
108
|
+
typeof args.sort === "string" ||
|
|
109
|
+
typeof args.page === "number" ||
|
|
110
|
+
args.pageSize !== undefined ||
|
|
111
|
+
args.expand !== undefined ||
|
|
112
|
+
args.dateWindow !== undefined ||
|
|
113
|
+
args.includeDeleted !== undefined ||
|
|
114
|
+
args.includeTotal !== undefined;
|
|
115
|
+
|
|
116
|
+
if (isLegacy) warnLegacyOnce();
|
|
117
|
+
|
|
118
|
+
const resource = args.resource ?? args.recordSlug;
|
|
119
|
+
if (!resource) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"query_records requires either `resource` (canonical) or `recordSlug` (legacy)",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// POST /records/query Phase 1 has no soft-delete plumbing — silently dropping
|
|
126
|
+
// `includeDeleted: true` would be a privacy regression vs 5.4.0 GET behavior
|
|
127
|
+
// (caller asks for tombstones, gets only live rows). Surface a clear error.
|
|
128
|
+
if (args.includeDeleted === true) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"query_records `includeDeleted: true` is not supported in Phase 1. " +
|
|
131
|
+
"Use the GET /records/slug/:rs endpoint with ?includeDeleted=true until Phase 2 lands.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const definition: Record<string, unknown> = { resource };
|
|
136
|
+
|
|
137
|
+
const whereParts: unknown[] = [];
|
|
138
|
+
if (args.where !== undefined) whereParts.push(args.where);
|
|
139
|
+
if (args.filters !== undefined) {
|
|
140
|
+
const fw = parseLegacyFilters(args.filters);
|
|
141
|
+
if (fw !== undefined) whereParts.push(fw);
|
|
142
|
+
}
|
|
143
|
+
if (args.dateWindow !== undefined) {
|
|
144
|
+
const dw = dateWindowToWhere(args.dateWindow);
|
|
145
|
+
if (dw !== undefined) whereParts.push(dw);
|
|
146
|
+
}
|
|
147
|
+
if (whereParts.length === 1) definition.where = whereParts[0];
|
|
148
|
+
else if (whereParts.length > 1) definition.where = { and: whereParts };
|
|
149
|
+
|
|
150
|
+
if (args.text !== undefined) definition.text = args.text;
|
|
151
|
+
|
|
152
|
+
if (args.sort !== undefined) {
|
|
153
|
+
definition.sort = typeof args.sort === "string" ? parseLegacySort(args.sort) : args.sort;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (args.page !== undefined || args.pageSize !== undefined) {
|
|
157
|
+
if (typeof args.page === "object" && args.page !== null) {
|
|
158
|
+
definition.page = args.page;
|
|
159
|
+
} else {
|
|
160
|
+
const limit = args.pageSize ?? 50;
|
|
161
|
+
const pageNum = typeof args.page === "number" ? args.page : 1;
|
|
162
|
+
const offset = (pageNum - 1) * limit;
|
|
163
|
+
definition.page = offset > 0 ? { limit, offset } : { limit };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (args.select !== undefined) definition.select = args.select;
|
|
168
|
+
|
|
169
|
+
if (args.include !== undefined) {
|
|
170
|
+
definition.include = args.include;
|
|
171
|
+
} else if (args.expand !== undefined) {
|
|
172
|
+
const relations = args.expand
|
|
173
|
+
.split(",")
|
|
174
|
+
.map((s) => ({ relation: s.trim() }))
|
|
175
|
+
.filter((r) => r.relation.length > 0);
|
|
176
|
+
if (relations.length > 0) definition.include = relations;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// `includeTotal` is dropped — `meta.total` is always returned for offset
|
|
180
|
+
// pagination on POST /records/query. `includeDeleted` is rejected above.
|
|
181
|
+
|
|
182
|
+
return { resource, definition };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Exposed for tests
|
|
186
|
+
export const _internal = { translateQueryRecordsArgs, parseLegacyFilters, parseLegacySort };
|
|
6
187
|
/**
|
|
7
188
|
* Ensures the SDK has a valid token.
|
|
8
189
|
*/
|
|
@@ -10,7 +191,7 @@ async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
|
|
|
10
191
|
let token = sdk.getToken();
|
|
11
192
|
if (token) return token;
|
|
12
193
|
try {
|
|
13
|
-
await sdk.
|
|
194
|
+
await sdk.records.query("__noop__", { resource: "__noop__", page: { limit: 1 } });
|
|
14
195
|
} catch { /* token refresh side effect */ }
|
|
15
196
|
return sdk.getToken();
|
|
16
197
|
}
|
|
@@ -44,7 +225,7 @@ function createRecordsClient(sdk: CentraliSDK, centraliUrl: string, workspaceId:
|
|
|
44
225
|
if (isAuthError && !originalRequest._hasRetried) {
|
|
45
226
|
originalRequest._hasRetried = true;
|
|
46
227
|
try {
|
|
47
|
-
await sdk.
|
|
228
|
+
await sdk.records.query("__noop__", { resource: "__noop__", page: { limit: 1 } });
|
|
48
229
|
} catch { /* token refresh side effect */ }
|
|
49
230
|
|
|
50
231
|
const token = sdk.getToken();
|
|
@@ -61,73 +242,113 @@ function createRecordsClient(sdk: CentraliSDK, centraliUrl: string, workspaceId:
|
|
|
61
242
|
}
|
|
62
243
|
|
|
63
244
|
export function registerRecordTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string) {
|
|
64
|
-
registerTool<any>(server,
|
|
245
|
+
registerTool<any>(server,
|
|
65
246
|
"query_records",
|
|
66
|
-
|
|
247
|
+
`Query records from a collection using the canonical Centrali query language (POST /records/query).
|
|
248
|
+
|
|
249
|
+
The body is a QueryDefinition. Field paths use dotted strings ('data.status', 'data.customer.email'); the field operators (no '$' prefix) are: eq, ne, gt, gte, lt, lte, in, nin, contains, startsWith, endsWith, hasAny, hasAll, exists. Boolean trees use 'and', 'or', 'not'.
|
|
250
|
+
|
|
251
|
+
Example body:
|
|
252
|
+
{
|
|
253
|
+
"where": {
|
|
254
|
+
"and": [
|
|
255
|
+
{ "data.status": { "eq": "open" } },
|
|
256
|
+
{ "data.amount": { "gte": 100 } }
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
"sort": [{ "field": "createdAt", "direction": "desc" }],
|
|
260
|
+
"page": { "limit": 50 },
|
|
261
|
+
"select": { "fields": ["id", "data.status", "data.amount"] }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Returns the canonical { data, meta } envelope. 'select', 'text', and 'include' are all accepted by the engine.`,
|
|
67
265
|
{
|
|
68
|
-
|
|
266
|
+
// Canonical fields (Phase 1)
|
|
267
|
+
resource: z
|
|
69
268
|
.string()
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
269
|
+
.optional()
|
|
270
|
+
.describe("Collection slug to query (e.g. 'orders'). Required unless legacy `recordSlug` is provided."),
|
|
271
|
+
where: z
|
|
272
|
+
.any()
|
|
73
273
|
.optional()
|
|
74
274
|
.describe(
|
|
75
|
-
"
|
|
275
|
+
"WhereExpression — either a FieldConditionMap ({ 'data.status': { eq: 'open' } }) or a boolean tree ({ and: [...] } | { or: [...] } | { not: ... }). Each FieldCondition must use exactly one operator."
|
|
76
276
|
),
|
|
277
|
+
text: z
|
|
278
|
+
.object({
|
|
279
|
+
query: z.string().describe("Search string"),
|
|
280
|
+
fields: z.array(z.string()).optional().describe("Restrict to these fields"),
|
|
281
|
+
typoTolerance: z.boolean().optional(),
|
|
282
|
+
})
|
|
283
|
+
.optional()
|
|
284
|
+
.describe("Full-text search clause. Routes to the search executor when set."),
|
|
77
285
|
sort: z
|
|
286
|
+
.any()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe(
|
|
289
|
+
"Canonical: array of { field, direction: 'asc'|'desc' }. Legacy: string '-createdAt' (deprecated, translated automatically)."
|
|
290
|
+
),
|
|
291
|
+
page: z
|
|
292
|
+
.any()
|
|
293
|
+
.optional()
|
|
294
|
+
.describe(
|
|
295
|
+
"Canonical: { limit, offset|cursor }. Legacy: page number (deprecated; combine with `pageSize`)."
|
|
296
|
+
),
|
|
297
|
+
select: z
|
|
298
|
+
.object({
|
|
299
|
+
fields: z.array(z.string()).describe("Field paths to return"),
|
|
300
|
+
})
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Field projection. Example: { fields: ['id', 'data.status'] }."),
|
|
303
|
+
include: z
|
|
304
|
+
.array(z.object({ relation: z.string() }))
|
|
305
|
+
.optional()
|
|
306
|
+
.describe("Relation expansion. Each entry names a relation declared on the collection."),
|
|
307
|
+
|
|
308
|
+
// Legacy 5.4.0 fields — accepted, translated to canonical, removed after 2026-10-28
|
|
309
|
+
recordSlug: z
|
|
78
310
|
.string()
|
|
79
311
|
.optional()
|
|
312
|
+
.describe("[Deprecated 2026-10-28] Legacy alias for `resource`."),
|
|
313
|
+
filters: z
|
|
314
|
+
.record(z.string(), z.any())
|
|
315
|
+
.optional()
|
|
80
316
|
.describe(
|
|
81
|
-
"
|
|
317
|
+
"[Deprecated 2026-10-28] Legacy filter map (e.g. { 'data.status': 'open', 'data.price[lte]': 100 }). Translated to `where`."
|
|
82
318
|
),
|
|
83
|
-
page: z.number().optional().describe("Page number (1-indexed, default: 1)"),
|
|
84
319
|
pageSize: z
|
|
85
320
|
.number()
|
|
86
321
|
.optional()
|
|
87
|
-
.describe("
|
|
322
|
+
.describe("[Deprecated 2026-10-28] Legacy page size. Translated to `page.limit`."),
|
|
88
323
|
expand: z
|
|
89
324
|
.string()
|
|
90
325
|
.optional()
|
|
91
326
|
.describe(
|
|
92
|
-
"Comma-separated
|
|
327
|
+
"[Deprecated 2026-10-28] Comma-separated relation names (e.g. 'customer,product'). Translated to `include`."
|
|
93
328
|
),
|
|
94
329
|
dateWindow: z
|
|
95
330
|
.object({
|
|
96
|
-
field: z.string()
|
|
97
|
-
from: z.string().optional()
|
|
98
|
-
to: z.string().optional()
|
|
331
|
+
field: z.string(),
|
|
332
|
+
from: z.string().optional(),
|
|
333
|
+
to: z.string().optional(),
|
|
99
334
|
})
|
|
100
335
|
.optional()
|
|
101
|
-
.describe(
|
|
102
|
-
"Date range filter. Restricts results to records where the specified date field falls within the given range."
|
|
103
|
-
),
|
|
336
|
+
.describe("[Deprecated 2026-10-28] Date range filter. Translated to `where` with gte/lte."),
|
|
104
337
|
includeDeleted: z
|
|
105
338
|
.boolean()
|
|
106
339
|
.optional()
|
|
107
|
-
.describe("
|
|
340
|
+
.describe("[Deprecated 2026-10-28] Ignored — POST /records/query does not surface deleted rows in Phase 1."),
|
|
108
341
|
includeTotal: z
|
|
109
342
|
.boolean()
|
|
110
343
|
.optional()
|
|
111
|
-
.describe("
|
|
344
|
+
.describe("[Deprecated 2026-10-28] Ignored — `meta.total` is always returned for offset pagination."),
|
|
112
345
|
},
|
|
113
|
-
async (
|
|
346
|
+
async (args) => {
|
|
347
|
+
let resource = "<unknown>";
|
|
114
348
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
page,
|
|
119
|
-
pageSize,
|
|
120
|
-
expand,
|
|
121
|
-
dateWindow,
|
|
122
|
-
includeDeleted,
|
|
123
|
-
includeTotal,
|
|
124
|
-
};
|
|
125
|
-
// Remove undefined values
|
|
126
|
-
Object.keys(params).forEach(
|
|
127
|
-
(key) => params[key] === undefined && delete params[key]
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const result = await sdk.queryRecords(recordSlug, params);
|
|
349
|
+
const translated = translateQueryRecordsArgs(args as QueryRecordsArgs);
|
|
350
|
+
resource = translated.resource;
|
|
351
|
+
const result = await sdk.records.query(resource, translated.definition as any);
|
|
131
352
|
return {
|
|
132
353
|
content: [
|
|
133
354
|
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
@@ -138,7 +359,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
138
359
|
content: [
|
|
139
360
|
{
|
|
140
361
|
type: "text",
|
|
141
|
-
text: formatError(error, `querying records from '${
|
|
362
|
+
text: formatError(error, `querying records from '${resource}'`),
|
|
142
363
|
},
|
|
143
364
|
],
|
|
144
365
|
isError: true,
|
|
@@ -147,7 +368,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
147
368
|
}
|
|
148
369
|
);
|
|
149
370
|
|
|
150
|
-
registerTool<any>(server,
|
|
371
|
+
registerTool<any>(server,
|
|
151
372
|
"get_record",
|
|
152
373
|
"Get a single record by its ID from a collection. Optionally expand reference fields.",
|
|
153
374
|
{
|
|
@@ -181,7 +402,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
181
402
|
}
|
|
182
403
|
);
|
|
183
404
|
|
|
184
|
-
registerTool<any>(server,
|
|
405
|
+
registerTool<any>(server,
|
|
185
406
|
"create_record",
|
|
186
407
|
"Create a new record in a collection. Pass the record data as a JSON object with field names matching the collection's properties.",
|
|
187
408
|
{
|
|
@@ -214,7 +435,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
214
435
|
}
|
|
215
436
|
);
|
|
216
437
|
|
|
217
|
-
registerTool<any>(server,
|
|
438
|
+
registerTool<any>(server,
|
|
218
439
|
"update_record",
|
|
219
440
|
"Update an existing record by ID. Only include the fields you want to change.",
|
|
220
441
|
{
|
|
@@ -246,7 +467,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
246
467
|
}
|
|
247
468
|
);
|
|
248
469
|
|
|
249
|
-
registerTool<any>(server,
|
|
470
|
+
registerTool<any>(server,
|
|
250
471
|
"delete_record",
|
|
251
472
|
"Delete a record by ID. Performs a soft delete by default (can be restored). Set hard=true for permanent deletion.",
|
|
252
473
|
{
|
|
@@ -286,7 +507,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
286
507
|
}
|
|
287
508
|
);
|
|
288
509
|
|
|
289
|
-
registerTool<any>(server,
|
|
510
|
+
registerTool<any>(server,
|
|
290
511
|
"upsert_record",
|
|
291
512
|
"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'.",
|
|
292
513
|
{
|
|
@@ -324,7 +545,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
324
545
|
}
|
|
325
546
|
);
|
|
326
547
|
|
|
327
|
-
registerTool<any>(server,
|
|
548
|
+
registerTool<any>(server,
|
|
328
549
|
"get_records_by_ids",
|
|
329
550
|
"Fetch multiple records by their IDs in a single request. Returns an array of records.",
|
|
330
551
|
{
|
|
@@ -348,7 +569,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
348
569
|
}
|
|
349
570
|
);
|
|
350
571
|
|
|
351
|
-
registerTool<any>(server,
|
|
572
|
+
registerTool<any>(server,
|
|
352
573
|
"restore_record",
|
|
353
574
|
"Restore a soft-deleted record by ID. Only works on records deleted without hard=true.",
|
|
354
575
|
{
|
|
@@ -376,7 +597,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
376
597
|
|
|
377
598
|
// ── Bulk Operations (1 aggregate event per operation) ───────────────
|
|
378
599
|
|
|
379
|
-
registerTool<any>(server,
|
|
600
|
+
registerTool<any>(server,
|
|
380
601
|
"bulk_create_records",
|
|
381
602
|
`Bulk create multiple records in a collection. All records are created in a single transaction. Publishes ONE aggregate 'records_bulk_created' event (not one per record). Use this when downstream triggers should process all records together as a batch. Max 1000 records per call. For individual events per record, use batch_create_records instead.`,
|
|
382
603
|
{
|
|
@@ -401,7 +622,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
401
622
|
}
|
|
402
623
|
);
|
|
403
624
|
|
|
404
|
-
registerTool<any>(server,
|
|
625
|
+
registerTool<any>(server,
|
|
405
626
|
"bulk_update_records",
|
|
406
627
|
`Bulk update multiple records with the same data. All records are updated in a single transaction. Publishes ONE aggregate 'records_bulk_updated' event (not one per record). Use this when all records need the same change and downstream triggers should process them together. For different data per record or individual events, use batch_update_records instead.`,
|
|
407
628
|
{
|
|
@@ -427,7 +648,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
427
648
|
}
|
|
428
649
|
);
|
|
429
650
|
|
|
430
|
-
registerTool<any>(server,
|
|
651
|
+
registerTool<any>(server,
|
|
431
652
|
"bulk_delete_records",
|
|
432
653
|
`Bulk delete multiple records. All records are deleted in a single transaction. Publishes ONE aggregate 'records_bulk_deleted' event (not one per record). Use this when downstream triggers should process all deletions together. For individual events per deletion, use batch_delete_records instead.`,
|
|
433
654
|
{
|
|
@@ -464,7 +685,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
464
685
|
|
|
465
686
|
// ── Batch Operations (1 event per record) ──────────────────────────
|
|
466
687
|
|
|
467
|
-
registerTool<any>(server,
|
|
688
|
+
registerTool<any>(server,
|
|
468
689
|
"batch_create_records",
|
|
469
690
|
`Create multiple records individually. Each record gets its own 'record_created' event. Use this when downstream triggers (functions, webhooks, orchestrations) should process each record separately. Supports partial failure — some records can succeed even if others fail. For a single aggregate event, use bulk_create_records instead.`,
|
|
470
691
|
{
|
|
@@ -495,7 +716,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
495
716
|
}
|
|
496
717
|
);
|
|
497
718
|
|
|
498
|
-
registerTool<any>(server,
|
|
719
|
+
registerTool<any>(server,
|
|
499
720
|
"batch_update_records",
|
|
500
721
|
`Update multiple records individually with different data per record. Each record gets its own 'record_updated' event. Use this when each record needs different changes and downstream triggers should process each update separately. For the same change applied to all records with a single event, use bulk_update_records instead.`,
|
|
501
722
|
{
|
|
@@ -527,7 +748,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
527
748
|
}
|
|
528
749
|
);
|
|
529
750
|
|
|
530
|
-
registerTool<any>(server,
|
|
751
|
+
registerTool<any>(server,
|
|
531
752
|
"batch_delete_records",
|
|
532
753
|
`Delete multiple records individually. Each record gets its own 'record_deleted' event. Use this when downstream triggers should process each deletion separately. For a single aggregate event, use bulk_delete_records instead.`,
|
|
533
754
|
{
|