@centrali-io/centrali-mcp 5.3.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.
- 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/dist/tools/records.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
|
73
|
-
|
|
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("
|
|
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("
|
|
84
|
-
|
|
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("
|
|
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
|
|
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()
|
|
96
|
-
from: zod_1.z.string().optional()
|
|
97
|
-
to: zod_1.z.string().optional()
|
|
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.
|
|
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("
|
|
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("
|
|
109
|
-
}, (
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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 '${
|
|
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.
|
|
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
|
|
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.
|
|
25
|
-
: yield sdk.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
66
|
+
.record(zod_1.z.string(), zod_1.z.any())
|
|
51
67
|
.optional()
|
|
52
|
-
.describe("Variables to substitute
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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",
|
|
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
|
|
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("
|
|
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.
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
192
|
+
yield sdk.savedQueries.delete(recordSlug, queryId);
|
|
173
193
|
return {
|
|
174
194
|
content: [
|
|
175
195
|
{
|
|
176
196
|
type: "text",
|
|
177
|
-
text: `
|
|
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
|
|
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",
|
|
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("
|
|
220
|
+
.describe("Canonical inner QueryDefinition to test (without 'resource')."),
|
|
199
221
|
variables: zod_1.z
|
|
200
|
-
.record(zod_1.z.string(), zod_1.z.
|
|
222
|
+
.record(zod_1.z.string(), zod_1.z.any())
|
|
201
223
|
.optional()
|
|
202
|
-
.describe("Optional variables to substitute
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
28
|
+
"@centrali-io/centrali-sdk": "^5.5.0",
|
|
28
29
|
"@modelcontextprotocol/sdk": "^1.28.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
package/src/tools/_register.ts
CHANGED
|
@@ -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,
|