@centrali-io/centrali-mcp 5.4.0 → 5.5.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.
@@ -12,10 +12,163 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports._internal = void 0;
15
16
  exports.registerRecordTools = registerRecordTools;
16
17
  const axios_1 = __importDefault(require("axios"));
17
18
  const zod_1 = require("zod");
18
19
  const _register_js_1 = require("./_register.js");
20
+ // ── Legacy 5.4.0 query_records translator (Sunset 2026-10-28) ──────────────
21
+ //
22
+ // 5.4.0 MCP clients call query_records with:
23
+ // { recordSlug, filters: { 'data.x[gte]': 1 }, sort: '-createdAt',
24
+ // page: 2, pageSize: 50, expand: 'customer', dateWindow: {...},
25
+ // includeDeleted, includeTotal }
26
+ //
27
+ // Canonical (Phase 1) accepts:
28
+ // { resource, where, sort: [{field, direction}], page: { limit, offset } }
29
+ //
30
+ // We accept BOTH on the wire and rewrite legacy → canonical before calling the
31
+ // SDK, matching the data-service contract promise of long-lived legacy reads.
32
+ const LEGACY_OP_MAP = {
33
+ $eq: "eq", $ne: "ne", $gt: "gt", $gte: "gte", $lt: "lt", $lte: "lte",
34
+ $in: "in", $nin: "nin", $contains: "contains",
35
+ $startsWith: "startsWith", $endsWith: "endsWith", $exists: "exists",
36
+ eq: "eq", ne: "ne", gt: "gt", gte: "gte", lt: "lt", lte: "lte",
37
+ in: "in", nin: "nin", contains: "contains",
38
+ startswith: "startsWith", endswith: "endsWith",
39
+ hasany: "hasAny", hasall: "hasAll",
40
+ };
41
+ let legacyWarned = false;
42
+ function warnLegacyOnce() {
43
+ if (!legacyWarned) {
44
+ legacyWarned = true;
45
+ console.warn("[centrali-mcp] query_records called with the legacy 5.4.0 schema " +
46
+ "(recordSlug/filters/sort:string/pageSize). It still works but will be " +
47
+ "removed after 2026-10-28. Migrate to: { resource, where, " +
48
+ "sort: [{field,direction}], page: { limit, offset } }.");
49
+ }
50
+ }
51
+ function parseLegacyFilters(filters) {
52
+ var _a, _b;
53
+ if (filters == null || typeof filters !== "object" || Array.isArray(filters)) {
54
+ return undefined;
55
+ }
56
+ const conditions = [];
57
+ for (const [rawKey, value] of Object.entries(filters)) {
58
+ const m = rawKey.match(/^(.+?)\[\$?([a-zA-Z]+)\]$/);
59
+ if (m) {
60
+ const [, field, op] = m;
61
+ const canonOp = (_b = (_a = LEGACY_OP_MAP[op]) !== null && _a !== void 0 ? _a : LEGACY_OP_MAP[op.toLowerCase()]) !== null && _b !== void 0 ? _b : op;
62
+ conditions.push({ [field]: { [canonOp]: value } });
63
+ }
64
+ else {
65
+ conditions.push({ [rawKey]: { eq: value } });
66
+ }
67
+ }
68
+ if (conditions.length === 0)
69
+ return undefined;
70
+ if (conditions.length === 1)
71
+ return conditions[0];
72
+ return { and: conditions };
73
+ }
74
+ function parseLegacySort(sort) {
75
+ return sort
76
+ .split(",")
77
+ .map((s) => s.trim())
78
+ .filter(Boolean)
79
+ .map((s) => s.startsWith("-")
80
+ ? { field: s.slice(1), direction: "desc" }
81
+ : { field: s, direction: "asc" });
82
+ }
83
+ function dateWindowToWhere(dw) {
84
+ const conds = [];
85
+ if (dw.from !== undefined)
86
+ conds.push({ [dw.field]: { gte: dw.from } });
87
+ if (dw.to !== undefined)
88
+ conds.push({ [dw.field]: { lte: dw.to } });
89
+ if (conds.length === 0)
90
+ return undefined;
91
+ if (conds.length === 1)
92
+ return conds[0];
93
+ return { and: conds };
94
+ }
95
+ function translateQueryRecordsArgs(args) {
96
+ var _a, _b;
97
+ const isLegacy = args.recordSlug !== undefined ||
98
+ args.filters !== undefined ||
99
+ typeof args.sort === "string" ||
100
+ typeof args.page === "number" ||
101
+ args.pageSize !== undefined ||
102
+ args.expand !== undefined ||
103
+ args.dateWindow !== undefined ||
104
+ args.includeDeleted !== undefined ||
105
+ args.includeTotal !== undefined;
106
+ if (isLegacy)
107
+ warnLegacyOnce();
108
+ const resource = (_a = args.resource) !== null && _a !== void 0 ? _a : args.recordSlug;
109
+ if (!resource) {
110
+ throw new Error("query_records requires either `resource` (canonical) or `recordSlug` (legacy)");
111
+ }
112
+ // POST /records/query Phase 1 has no soft-delete plumbing — silently dropping
113
+ // `includeDeleted: true` would be a privacy regression vs 5.4.0 GET behavior
114
+ // (caller asks for tombstones, gets only live rows). Surface a clear error.
115
+ if (args.includeDeleted === true) {
116
+ throw new Error("query_records `includeDeleted: true` is not supported in Phase 1. " +
117
+ "Use the GET /records/slug/:rs endpoint with ?includeDeleted=true until Phase 2 lands.");
118
+ }
119
+ const definition = { resource };
120
+ const whereParts = [];
121
+ if (args.where !== undefined)
122
+ whereParts.push(args.where);
123
+ if (args.filters !== undefined) {
124
+ const fw = parseLegacyFilters(args.filters);
125
+ if (fw !== undefined)
126
+ whereParts.push(fw);
127
+ }
128
+ if (args.dateWindow !== undefined) {
129
+ const dw = dateWindowToWhere(args.dateWindow);
130
+ if (dw !== undefined)
131
+ whereParts.push(dw);
132
+ }
133
+ if (whereParts.length === 1)
134
+ definition.where = whereParts[0];
135
+ else if (whereParts.length > 1)
136
+ definition.where = { and: whereParts };
137
+ if (args.text !== undefined)
138
+ definition.text = args.text;
139
+ if (args.sort !== undefined) {
140
+ definition.sort = typeof args.sort === "string" ? parseLegacySort(args.sort) : args.sort;
141
+ }
142
+ if (args.page !== undefined || args.pageSize !== undefined) {
143
+ if (typeof args.page === "object" && args.page !== null) {
144
+ definition.page = args.page;
145
+ }
146
+ else {
147
+ const limit = (_b = args.pageSize) !== null && _b !== void 0 ? _b : 50;
148
+ const pageNum = typeof args.page === "number" ? args.page : 1;
149
+ const offset = (pageNum - 1) * limit;
150
+ definition.page = offset > 0 ? { limit, offset } : { limit };
151
+ }
152
+ }
153
+ if (args.select !== undefined)
154
+ definition.select = args.select;
155
+ if (args.include !== undefined) {
156
+ definition.include = args.include;
157
+ }
158
+ else if (args.expand !== undefined) {
159
+ const relations = args.expand
160
+ .split(",")
161
+ .map((s) => ({ relation: s.trim() }))
162
+ .filter((r) => r.relation.length > 0);
163
+ if (relations.length > 0)
164
+ definition.include = relations;
165
+ }
166
+ // `includeTotal` is dropped — `meta.total` is always returned for offset
167
+ // pagination on POST /records/query. `includeDeleted` is rejected above.
168
+ return { resource, definition };
169
+ }
170
+ // Exposed for tests
171
+ exports._internal = { translateQueryRecordsArgs, parseLegacyFilters, parseLegacySort };
19
172
  /**
20
173
  * Ensures the SDK has a valid token.
21
174
  */
@@ -25,7 +178,7 @@ function ensureToken(sdk) {
25
178
  if (token)
26
179
  return token;
27
180
  try {
28
- yield sdk.queryRecords("__noop__", { limit: 1 });
181
+ yield sdk.records.query("__noop__", { resource: "__noop__", page: { limit: 1 } });
29
182
  }
30
183
  catch ( /* token refresh side effect */_a) { /* token refresh side effect */ }
31
184
  return sdk.getToken();
@@ -55,7 +208,7 @@ function createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug) {
55
208
  if (isAuthError && !originalRequest._hasRetried) {
56
209
  originalRequest._hasRetried = true;
57
210
  try {
58
- yield sdk.queryRecords("__noop__", { limit: 1 });
211
+ yield sdk.records.query("__noop__", { resource: "__noop__", page: { limit: 1 } });
59
212
  }
60
213
  catch ( /* token refresh side effect */_c) { /* token refresh side effect */ }
61
214
  const token = sdk.getToken();
@@ -69,55 +222,98 @@ function createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug) {
69
222
  return client;
70
223
  }
71
224
  function registerRecordTools(server, sdk, centraliUrl, workspaceId) {
72
- (0, _register_js_1.registerTool)(server, "query_records", "Query records from a collection with optional filters, sorting, pagination, and date range filtering. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100). Use dateWindow for date range queries.", {
73
- recordSlug: zod_1.z
225
+ (0, _register_js_1.registerTool)(server, "query_records", `Query records from a collection using the canonical Centrali query language (POST /records/query).
226
+
227
+ 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'.
228
+
229
+ Example body:
230
+ {
231
+ "where": {
232
+ "and": [
233
+ { "data.status": { "eq": "open" } },
234
+ { "data.amount": { "gte": 100 } }
235
+ ]
236
+ },
237
+ "sort": [{ "field": "createdAt", "direction": "desc" }],
238
+ "page": { "limit": 50 },
239
+ "select": { "fields": ["id", "data.status", "data.amount"] }
240
+ }
241
+
242
+ Returns the canonical { data, meta } envelope. 'select', 'text', and 'include' are all accepted by the engine.`, {
243
+ // Canonical fields (Phase 1)
244
+ resource: zod_1.z
74
245
  .string()
75
- .describe("The collection's record slug (e.g., 'orders')"),
76
- filters: zod_1.z
77
- .record(zod_1.z.string(), zod_1.z.any())
78
246
  .optional()
79
- .describe("Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith, hasAny, hasAll"),
247
+ .describe("Collection slug to query (e.g. 'orders'). Required unless legacy `recordSlug` is provided."),
248
+ where: zod_1.z
249
+ .any()
250
+ .optional()
251
+ .describe("WhereExpression — either a FieldConditionMap ({ 'data.status': { eq: 'open' } }) or a boolean tree ({ and: [...] } | { or: [...] } | { not: ... }). Each FieldCondition must use exactly one operator."),
252
+ text: zod_1.z
253
+ .object({
254
+ query: zod_1.z.string().describe("Search string"),
255
+ fields: zod_1.z.array(zod_1.z.string()).optional().describe("Restrict to these fields"),
256
+ typoTolerance: zod_1.z.boolean().optional(),
257
+ })
258
+ .optional()
259
+ .describe("Full-text search clause. Routes to the search executor when set."),
80
260
  sort: zod_1.z
261
+ .any()
262
+ .optional()
263
+ .describe("Canonical: array of { field, direction: 'asc'|'desc' }. Legacy: string '-createdAt' (deprecated, translated automatically)."),
264
+ page: zod_1.z
265
+ .any()
266
+ .optional()
267
+ .describe("Canonical: { limit, offset|cursor }. Legacy: page number (deprecated; combine with `pageSize`)."),
268
+ select: zod_1.z
269
+ .object({
270
+ fields: zod_1.z.array(zod_1.z.string()).describe("Field paths to return"),
271
+ })
272
+ .optional()
273
+ .describe("Field projection. Example: { fields: ['id', 'data.status'] }."),
274
+ include: zod_1.z
275
+ .array(zod_1.z.object({ relation: zod_1.z.string() }))
276
+ .optional()
277
+ .describe("Relation expansion. Each entry names a relation declared on the collection."),
278
+ // Legacy 5.4.0 fields — accepted, translated to canonical, removed after 2026-10-28
279
+ recordSlug: zod_1.z
81
280
  .string()
82
281
  .optional()
83
- .describe("Sort field with optional '-' prefix for descending (e.g., '-createdAt')"),
84
- page: zod_1.z.number().optional().describe("Page number (1-indexed, default: 1)"),
282
+ .describe("[Deprecated 2026-10-28] Legacy alias for `resource`."),
283
+ filters: zod_1.z
284
+ .record(zod_1.z.string(), zod_1.z.any())
285
+ .optional()
286
+ .describe("[Deprecated 2026-10-28] Legacy filter map (e.g. { 'data.status': 'open', 'data.price[lte]': 100 }). Translated to `where`."),
85
287
  pageSize: zod_1.z
86
288
  .number()
87
289
  .optional()
88
- .describe("Records per page (default: 50, max: 500)"),
290
+ .describe("[Deprecated 2026-10-28] Legacy page size. Translated to `page.limit`."),
89
291
  expand: zod_1.z
90
292
  .string()
91
293
  .optional()
92
- .describe("Comma-separated reference fields to expand (e.g., 'customer,product')"),
294
+ .describe("[Deprecated 2026-10-28] Comma-separated relation names (e.g. 'customer,product'). Translated to `include`."),
93
295
  dateWindow: zod_1.z
94
296
  .object({
95
- field: zod_1.z.string().describe("Date field to filter on (e.g., 'createdAt', 'updatedAt')"),
96
- from: zod_1.z.string().optional().describe("ISO 8601 lower bound (inclusive)"),
97
- to: zod_1.z.string().optional().describe("ISO 8601 upper bound (inclusive)"),
297
+ field: zod_1.z.string(),
298
+ from: zod_1.z.string().optional(),
299
+ to: zod_1.z.string().optional(),
98
300
  })
99
301
  .optional()
100
- .describe("Date range filter. Restricts results to records where the specified date field falls within the given range."),
302
+ .describe("[Deprecated 2026-10-28] Date range filter. Translated to `where` with gte/lte."),
101
303
  includeDeleted: zod_1.z
102
304
  .boolean()
103
305
  .optional()
104
- .describe("Include soft-deleted records (default: false)"),
306
+ .describe("[Deprecated 2026-10-28] Ignored — POST /records/query does not surface deleted rows in Phase 1."),
105
307
  includeTotal: zod_1.z
106
308
  .boolean()
107
309
  .optional()
108
- .describe("Include total record count in response metadata (default: false)"),
109
- }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, filters, sort, page, pageSize, expand, dateWindow, includeDeleted, includeTotal }) {
310
+ .describe("[Deprecated 2026-10-28] Ignored — `meta.total` is always returned for offset pagination."),
311
+ }, (args) => __awaiter(this, void 0, void 0, function* () {
312
+ let resource = "<unknown>";
110
313
  try {
111
- const params = Object.assign(Object.assign({}, filters), { sort,
112
- page,
113
- pageSize,
114
- expand,
115
- dateWindow,
116
- includeDeleted,
117
- includeTotal });
118
- // Remove undefined values
119
- Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
120
- const result = yield sdk.queryRecords(recordSlug, params);
314
+ const translated = translateQueryRecordsArgs(args);
315
+ resource = translated.resource;
316
+ const result = yield sdk.records.query(resource, translated.definition);
121
317
  return {
122
318
  content: [
123
319
  { type: "text", text: JSON.stringify(result, null, 2) },
@@ -129,7 +325,7 @@ function registerRecordTools(server, sdk, centraliUrl, workspaceId) {
129
325
  content: [
130
326
  {
131
327
  type: "text",
132
- text: (0, _register_js_1.formatError)(error, `querying records from '${recordSlug}'`),
328
+ text: (0, _register_js_1.formatError)(error, `querying records from '${resource}'`),
133
329
  },
134
330
  ],
135
331
  isError: true,
@@ -12,17 +12,33 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.registerSmartQueryTools = registerSmartQueryTools;
13
13
  const zod_1 = require("zod");
14
14
  const _register_js_1 = require("./_register.js");
15
+ // Tool names below stay as `*_smart_query` to keep existing AI assistant
16
+ // configurations working — renaming would break public clients. Internally
17
+ // they route through `sdk.savedQueries.*` (the canonical namespace) and
18
+ // accept canonical query bodies.
19
+ const CANONICAL_QUERY_DEFINITION_HINT = `Canonical QueryDefinition body (saved queries store the inner shape — 'resource' is filled in from the collection slug arg).
20
+
21
+ Field paths use dotted strings ('data.status'); operators (no '$' prefix): eq, ne, gt, gte, lt, lte, in, nin, contains, startsWith, endsWith, hasAny, hasAll, exists. Boolean trees use 'and', 'or', 'not'.
22
+
23
+ Variables are referenced as '{{varName}}' inside operator values; the engine infers the variable list from the placeholders in the body. Callers pass concrete values via the 'variables' arg on execute_smart_query / test_smart_query. Example:
24
+ {
25
+ "where": { "data.status": { "eq": "{{statusFilter}}" } },
26
+ "sort": [{ "field": "createdAt", "direction": "desc" }],
27
+ "page": { "limit": 100 }
28
+ }
29
+
30
+ Do NOT use legacy '\$'-prefixed operators ('\$eq', '\$gte', …). The data service still translates them server-side during the deprecation window, but new saved queries must author canonical operators.`;
15
31
  function registerSmartQueryTools(server, sdk) {
16
- (0, _register_js_1.registerTool)(server, "list_smart_queries", "List smart queries. Smart queries are reusable, parameterized queries defined in the Centrali console. Optionally filter by collection record slug.", {
32
+ (0, _register_js_1.registerTool)(server, "list_smart_queries", "List saved (a.k.a. smart) queries. Saved queries are reusable, parameterized queries defined in the Centrali console. Optionally filter by collection record slug.", {
17
33
  recordSlug: zod_1.z
18
34
  .string()
19
35
  .optional()
20
- .describe("Filter by collection record slug. If omitted, lists all smart queries in the workspace"),
36
+ .describe("Filter by collection record slug. If omitted, lists all saved queries in the workspace"),
21
37
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug }) {
22
38
  try {
23
39
  const result = recordSlug
24
- ? yield sdk.smartQueries.list(recordSlug)
25
- : yield sdk.smartQueries.listAll();
40
+ ? yield sdk.savedQueries.list(recordSlug)
41
+ : yield sdk.savedQueries.listAll();
26
42
  return {
27
43
  content: [
28
44
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -34,26 +50,26 @@ function registerSmartQueryTools(server, sdk) {
34
50
  content: [
35
51
  {
36
52
  type: "text",
37
- text: (0, _register_js_1.formatError)(error, "listing smart queries"),
53
+ text: (0, _register_js_1.formatError)(error, "listing saved queries"),
38
54
  },
39
55
  ],
40
56
  isError: true,
41
57
  };
42
58
  }
43
59
  }));
44
- (0, _register_js_1.registerTool)(server, "execute_smart_query", "Execute a smart query by ID and return the results. Smart queries can have parameterized variables using {{variableName}} syntax.", {
60
+ (0, _register_js_1.registerTool)(server, "execute_smart_query", "Execute a saved query by ID and return the canonical { data, meta } envelope. Saved queries can declare variables referenced as '{{varName}}' inside operator values.", {
45
61
  recordSlug: zod_1.z
46
62
  .string()
47
63
  .describe("The collection's record slug the query belongs to"),
48
- queryId: zod_1.z.string().describe("The smart query ID (UUID) to execute"),
64
+ queryId: zod_1.z.string().describe("The saved query ID (UUID) to execute"),
49
65
  variables: zod_1.z
50
- .record(zod_1.z.string(), zod_1.z.string())
66
+ .record(zod_1.z.string(), zod_1.z.any())
51
67
  .optional()
52
- .describe("Variables to substitute in the query (key-value pairs matching {{variableName}} placeholders)"),
68
+ .describe("Variables to substitute (key-value pairs matching '{{varName}}' placeholders). Values must match the variable's declared type."),
53
69
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId, variables }) {
54
70
  try {
55
71
  const options = variables ? { variables } : undefined;
56
- const result = yield sdk.smartQueries.execute(recordSlug, queryId, options);
72
+ const result = yield sdk.savedQueries.execute(recordSlug, queryId, options);
57
73
  return {
58
74
  content: [
59
75
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -65,19 +81,19 @@ function registerSmartQueryTools(server, sdk) {
65
81
  content: [
66
82
  {
67
83
  type: "text",
68
- text: (0, _register_js_1.formatError)(error, `executing smart query '${queryId}'`),
84
+ text: (0, _register_js_1.formatError)(error, `executing saved query '${queryId}'`),
69
85
  },
70
86
  ],
71
87
  isError: true,
72
88
  };
73
89
  }
74
90
  }));
75
- (0, _register_js_1.registerTool)(server, "get_smart_query", "Get a smart query by ID. Returns the full query definition including filters, sort, and variable declarations.", {
91
+ (0, _register_js_1.registerTool)(server, "get_smart_query", "Get a saved query by ID. Returns the full query definition including canonical 'where', 'sort', 'page', 'select', and variable declarations.", {
76
92
  recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
77
- queryId: zod_1.z.string().describe("The smart query ID (UUID)"),
93
+ queryId: zod_1.z.string().describe("The saved query ID (UUID)"),
78
94
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId }) {
79
95
  try {
80
- const result = yield sdk.smartQueries.get(recordSlug, queryId);
96
+ const result = yield sdk.savedQueries.get(recordSlug, queryId);
81
97
  return {
82
98
  content: [
83
99
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -89,26 +105,28 @@ function registerSmartQueryTools(server, sdk) {
89
105
  content: [
90
106
  {
91
107
  type: "text",
92
- text: (0, _register_js_1.formatError)(error, `getting smart query '${queryId}'`),
108
+ text: (0, _register_js_1.formatError)(error, `getting saved query '${queryId}'`),
93
109
  },
94
110
  ],
95
111
  isError: true,
96
112
  };
97
113
  }
98
114
  }));
99
- (0, _register_js_1.registerTool)(server, "create_smart_query", "Create a new smart query for a collection. Smart queries are reusable, parameterized queries with filter, sort, and variable support.", {
115
+ (0, _register_js_1.registerTool)(server, "create_smart_query", `Create a new saved query for a collection.
116
+
117
+ ${CANONICAL_QUERY_DEFINITION_HINT}`, {
100
118
  recordSlug: zod_1.z.string().describe("The collection's record slug to create the query for"),
101
- name: zod_1.z.string().describe("Display name for the smart query"),
119
+ name: zod_1.z.string().describe("Display name for the saved query"),
102
120
  description: zod_1.z.string().optional().describe("Optional description"),
103
121
  queryDefinition: zod_1.z
104
122
  .record(zod_1.z.string(), zod_1.z.any())
105
- .describe("The query definition object with where, sort, limit, select, join, etc."),
123
+ .describe("Canonical inner QueryDefinition (without 'resource'). Use 'where', 'text', 'sort', 'page', 'select'. Variables are inferred from '{{varName}}' placeholders inside operator values."),
106
124
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, name, description, queryDefinition }) {
107
125
  try {
108
126
  const input = { name, queryDefinition };
109
127
  if (description !== undefined)
110
128
  input.description = description;
111
- const result = yield sdk.smartQueries.create(recordSlug, input);
129
+ const result = yield sdk.savedQueries.create(recordSlug, input);
112
130
  return {
113
131
  content: [
114
132
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -120,22 +138,24 @@ function registerSmartQueryTools(server, sdk) {
120
138
  content: [
121
139
  {
122
140
  type: "text",
123
- text: (0, _register_js_1.formatError)(error, `creating smart query '${name}' for '${recordSlug}'`),
141
+ text: (0, _register_js_1.formatError)(error, `creating saved query '${name}' for '${recordSlug}'`),
124
142
  },
125
143
  ],
126
144
  isError: true,
127
145
  };
128
146
  }
129
147
  }));
130
- (0, _register_js_1.registerTool)(server, "update_smart_query", "Update an existing smart query. Only include the fields you want to change.", {
148
+ (0, _register_js_1.registerTool)(server, "update_smart_query", `Update an existing saved query. Only include the fields you want to change.
149
+
150
+ ${CANONICAL_QUERY_DEFINITION_HINT}`, {
131
151
  recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
132
- queryId: zod_1.z.string().describe("The smart query ID (UUID) to update"),
152
+ queryId: zod_1.z.string().describe("The saved query ID (UUID) to update"),
133
153
  name: zod_1.z.string().optional().describe("Updated display name"),
134
154
  description: zod_1.z.string().optional().describe("Updated description"),
135
155
  queryDefinition: zod_1.z
136
156
  .record(zod_1.z.string(), zod_1.z.any())
137
157
  .optional()
138
- .describe("Updated query definition object"),
158
+ .describe("Updated canonical inner QueryDefinition (without 'resource')."),
139
159
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId, name, description, queryDefinition }) {
140
160
  try {
141
161
  const input = {};
@@ -145,7 +165,7 @@ function registerSmartQueryTools(server, sdk) {
145
165
  input.description = description;
146
166
  if (queryDefinition !== undefined)
147
167
  input.queryDefinition = queryDefinition;
148
- const result = yield sdk.smartQueries.update(recordSlug, queryId, input);
168
+ const result = yield sdk.savedQueries.update(recordSlug, queryId, input);
149
169
  return {
150
170
  content: [
151
171
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -157,24 +177,24 @@ function registerSmartQueryTools(server, sdk) {
157
177
  content: [
158
178
  {
159
179
  type: "text",
160
- text: (0, _register_js_1.formatError)(error, `updating smart query '${queryId}'`),
180
+ text: (0, _register_js_1.formatError)(error, `updating saved query '${queryId}'`),
161
181
  },
162
182
  ],
163
183
  isError: true,
164
184
  };
165
185
  }
166
186
  }));
167
- (0, _register_js_1.registerTool)(server, "delete_smart_query", "Delete a smart query by ID.", {
187
+ (0, _register_js_1.registerTool)(server, "delete_smart_query", "Delete a saved query by ID.", {
168
188
  recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
169
- queryId: zod_1.z.string().describe("The smart query ID (UUID) to delete"),
189
+ queryId: zod_1.z.string().describe("The saved query ID (UUID) to delete"),
170
190
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId }) {
171
191
  try {
172
- yield sdk.smartQueries.delete(recordSlug, queryId);
192
+ yield sdk.savedQueries.delete(recordSlug, queryId);
173
193
  return {
174
194
  content: [
175
195
  {
176
196
  type: "text",
177
- text: `Smart query '${queryId}' deleted from '${recordSlug}'.`,
197
+ text: `Saved query '${queryId}' deleted from '${recordSlug}'.`,
178
198
  },
179
199
  ],
180
200
  };
@@ -184,28 +204,30 @@ function registerSmartQueryTools(server, sdk) {
184
204
  content: [
185
205
  {
186
206
  type: "text",
187
- text: (0, _register_js_1.formatError)(error, `deleting smart query '${queryId}'`),
207
+ text: (0, _register_js_1.formatError)(error, `deleting saved query '${queryId}'`),
188
208
  },
189
209
  ],
190
210
  isError: true,
191
211
  };
192
212
  }
193
213
  }));
194
- (0, _register_js_1.registerTool)(server, "test_smart_query", "Test execute a query definition without saving it. Useful for validating query syntax and previewing results before creating a smart query.", {
214
+ (0, _register_js_1.registerTool)(server, "test_smart_query", `Test execute a query definition without saving it. Useful for validating syntax and previewing results before creating a saved query.
215
+
216
+ ${CANONICAL_QUERY_DEFINITION_HINT}`, {
195
217
  recordSlug: zod_1.z.string().describe("The collection's record slug to test against"),
196
218
  queryDefinition: zod_1.z
197
219
  .record(zod_1.z.string(), zod_1.z.any())
198
- .describe("The query definition to test (where, sort, limit, select, etc.)"),
220
+ .describe("Canonical inner QueryDefinition to test (without 'resource')."),
199
221
  variables: zod_1.z
200
- .record(zod_1.z.string(), zod_1.z.string())
222
+ .record(zod_1.z.string(), zod_1.z.any())
201
223
  .optional()
202
- .describe("Optional variables to substitute in the query"),
224
+ .describe("Optional variables to substitute."),
203
225
  }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryDefinition, variables }) {
204
226
  try {
205
227
  const input = { queryDefinition };
206
228
  if (variables !== undefined)
207
229
  input.variables = variables;
208
- const result = yield sdk.smartQueries.test(recordSlug, input);
230
+ const result = yield sdk.savedQueries.test(recordSlug, input);
209
231
  return {
210
232
  content: [
211
233
  { type: "text", text: JSON.stringify(result.data, null, 2) },
@@ -217,7 +239,7 @@ function registerSmartQueryTools(server, sdk) {
217
239
  content: [
218
240
  {
219
241
  type: "text",
220
- text: (0, _register_js_1.formatError)(error, `test-executing smart query for '${recordSlug}'`),
242
+ text: (0, _register_js_1.formatError)(error, `test-executing saved query for '${recordSlug}'`),
221
243
  },
222
244
  ],
223
245
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "build": "tsc --project tsconfig.json",
16
+ "test": "npm run build && node --test 'tests/**/*.test.cjs'",
16
17
  "prepublishOnly": "npm run build"
17
18
  },
18
19
  "keywords": [
@@ -24,7 +25,7 @@
24
25
  "author": "Blueinit",
25
26
  "license": "ISC",
26
27
  "dependencies": {
27
- "@centrali-io/centrali-sdk": "^5.4.0",
28
+ "@centrali-io/centrali-sdk": "^5.5.0",
28
29
  "@modelcontextprotocol/sdk": "^1.28.0"
29
30
  },
30
31
  "devDependencies": {
@@ -18,6 +18,15 @@ export type ToolResult = {
18
18
  *
19
19
  * Call sites annotate the arg shape explicitly via the `Args` generic so the
20
20
  * handler body stays fully type-safe.
21
+ *
22
+ * CEN-1099 / CEN-1186 — re-investigated when query tool inputs were tightened
23
+ * to the canonical `QueryDefinition` shape (small, finite). Direct
24
+ * `server.tool(...)` still trips `TS2589 Type instantiation is excessively
25
+ * deep and possibly infinite` against the smaller schema, so this wrapper
26
+ * stays. Repro:
27
+ * import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+ * server.tool("x", "x", { resource: z.string() }, async (a) => ({ content: [] }));
29
+ * // → TS2589 from `ShapeOutput` over `ZodRawShape`.
21
30
  */
22
31
  export function registerTool<Args>(
23
32
  server: McpServer,