@centrali-io/centrali-mcp 5.5.1 → 6.0.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 +104 -10
- package/dist/index.js +2 -2
- package/dist/tools/describe.js +116 -40
- package/dist/tools/records.d.ts +19 -0
- package/dist/tools/records.js +91 -5
- package/dist/tools/saved-queries.d.ts +14 -0
- package/dist/tools/saved-queries.js +457 -0
- package/package.json +2 -2
- package/src/index.ts +2 -2
- package/src/tools/describe.ts +130 -41
- package/src/tools/records.ts +90 -5
- package/src/tools/saved-queries.ts +497 -0
- package/tests/savedQueriesRouting.test.cjs +148 -0
- package/tests/typedVariables.test.cjs +113 -0
- package/dist/tools/smart-queries.d.ts +0 -3
- package/dist/tools/smart-queries.js +0 -249
- package/src/tools/smart-queries.ts +0 -284
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 / WS5 — typed `${var}` placeholder helpers.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - records.ts: `substituteCanonicalVariables` (client-side templating
|
|
6
|
+
* for ad-hoc `query_records`).
|
|
7
|
+
* - saved-queries.ts: `extractCanonicalVariableNames` (used in
|
|
8
|
+
* `execute_saved_query` for best-effort preflight).
|
|
9
|
+
*/
|
|
10
|
+
const test = require("node:test");
|
|
11
|
+
const assert = require("node:assert/strict");
|
|
12
|
+
|
|
13
|
+
const { _internal: recordsInternal } = require("../dist/tools/records.js");
|
|
14
|
+
const { _internal: savedInternal } = require("../dist/tools/saved-queries.js");
|
|
15
|
+
|
|
16
|
+
const { substituteCanonicalVariables } = recordsInternal;
|
|
17
|
+
const { extractCanonicalVariableNames } = savedInternal;
|
|
18
|
+
|
|
19
|
+
test("substitute: whole-string placeholder round-trips typed value", () => {
|
|
20
|
+
const out = substituteCanonicalVariables(
|
|
21
|
+
{ where: { "data.tags": { in: "${tags}" } } },
|
|
22
|
+
{ tags: ["red", "blue"] },
|
|
23
|
+
);
|
|
24
|
+
assert.deepEqual(out, { where: { "data.tags": { in: ["red", "blue"] } } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("substitute: mixed-string placeholder stringifies inline", () => {
|
|
28
|
+
const out = substituteCanonicalVariables(
|
|
29
|
+
{ where: { "data.label": { eq: "prefix-${suffix}" } } },
|
|
30
|
+
{ suffix: 42 },
|
|
31
|
+
);
|
|
32
|
+
assert.deepEqual(out, { where: { "data.label": { eq: "prefix-42" } } });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("substitute: walks nested arrays and boolean trees", () => {
|
|
36
|
+
const out = substituteCanonicalVariables(
|
|
37
|
+
{
|
|
38
|
+
where: {
|
|
39
|
+
and: [
|
|
40
|
+
{ "data.status": { eq: "${status}" } },
|
|
41
|
+
{ "data.amount": { gte: "${minAmount}" } },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{ status: "open", minAmount: 100 },
|
|
46
|
+
);
|
|
47
|
+
assert.deepEqual(out, {
|
|
48
|
+
where: {
|
|
49
|
+
and: [
|
|
50
|
+
{ "data.status": { eq: "open" } },
|
|
51
|
+
{ "data.amount": { gte: 100 } },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("substitute: missing variable throws missing_required_variable", () => {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => substituteCanonicalVariables(
|
|
60
|
+
{ where: { "data.id": { eq: "${recordId}" } } },
|
|
61
|
+
{},
|
|
62
|
+
),
|
|
63
|
+
/missing_required_variable: recordId/,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("substitute: leaves non-placeholder strings untouched", () => {
|
|
68
|
+
const out = substituteCanonicalVariables(
|
|
69
|
+
{ where: { "data.status": { eq: "open" } } },
|
|
70
|
+
{ irrelevant: "x" },
|
|
71
|
+
);
|
|
72
|
+
assert.deepEqual(out, { where: { "data.status": { eq: "open" } } });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("substitute: handles null / numbers / booleans without recursion", () => {
|
|
76
|
+
const out = substituteCanonicalVariables(
|
|
77
|
+
{ where: { "data.score": { gte: 0 } }, page: { limit: 50 } },
|
|
78
|
+
{},
|
|
79
|
+
);
|
|
80
|
+
assert.deepEqual(out, { where: { "data.score": { gte: 0 } }, page: { limit: 50 } });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("extract: collects unique variable names across the body", () => {
|
|
84
|
+
const out = extractCanonicalVariableNames({
|
|
85
|
+
where: {
|
|
86
|
+
and: [
|
|
87
|
+
{ "data.status": { eq: "${statusFilter}" } },
|
|
88
|
+
{ "data.amount": { gte: "${minAmount}" } },
|
|
89
|
+
{ "data.tags": { in: "${tags}" } },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
sort: [{ field: "${sortField}", direction: "desc" }],
|
|
93
|
+
});
|
|
94
|
+
assert.deepEqual(
|
|
95
|
+
Array.from(out).sort(),
|
|
96
|
+
["minAmount", "sortField", "statusFilter", "tags"],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("extract: ignores legacy {{var}} placeholders (Phase 4 = '${var}' only)", () => {
|
|
101
|
+
const out = extractCanonicalVariableNames({
|
|
102
|
+
where: { "data.status": { eq: "{{statusFilter}}" } },
|
|
103
|
+
});
|
|
104
|
+
assert.equal(out.size, 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("extract: handles bodies with no placeholders", () => {
|
|
108
|
+
const out = extractCanonicalVariableNames({
|
|
109
|
+
where: { "data.status": { eq: "open" } },
|
|
110
|
+
page: { limit: 100 },
|
|
111
|
+
});
|
|
112
|
+
assert.equal(out.size, 0);
|
|
113
|
+
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.registerSmartQueryTools = registerSmartQueryTools;
|
|
13
|
-
const zod_1 = require("zod");
|
|
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.`;
|
|
31
|
-
function registerSmartQueryTools(server, sdk) {
|
|
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.", {
|
|
33
|
-
recordSlug: zod_1.z
|
|
34
|
-
.string()
|
|
35
|
-
.optional()
|
|
36
|
-
.describe("Filter by collection record slug. If omitted, lists all saved queries in the workspace"),
|
|
37
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug }) {
|
|
38
|
-
try {
|
|
39
|
-
const result = recordSlug
|
|
40
|
-
? yield sdk.savedQueries.list(recordSlug)
|
|
41
|
-
: yield sdk.savedQueries.listAll();
|
|
42
|
-
return {
|
|
43
|
-
content: [
|
|
44
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
45
|
-
],
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
return {
|
|
50
|
-
content: [
|
|
51
|
-
{
|
|
52
|
-
type: "text",
|
|
53
|
-
text: (0, _register_js_1.formatError)(error, "listing saved queries"),
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
isError: true,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}));
|
|
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.", {
|
|
61
|
-
recordSlug: zod_1.z
|
|
62
|
-
.string()
|
|
63
|
-
.describe("The collection's record slug the query belongs to"),
|
|
64
|
-
queryId: zod_1.z.string().describe("The saved query ID (UUID) to execute"),
|
|
65
|
-
variables: zod_1.z
|
|
66
|
-
.record(zod_1.z.string(), zod_1.z.any())
|
|
67
|
-
.optional()
|
|
68
|
-
.describe("Variables to substitute (key-value pairs matching '{{varName}}' placeholders). Values must match the variable's declared type."),
|
|
69
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId, variables }) {
|
|
70
|
-
try {
|
|
71
|
-
const options = variables ? { variables } : undefined;
|
|
72
|
-
const result = yield sdk.savedQueries.execute(recordSlug, queryId, options);
|
|
73
|
-
return {
|
|
74
|
-
content: [
|
|
75
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: "text",
|
|
84
|
-
text: (0, _register_js_1.formatError)(error, `executing saved query '${queryId}'`),
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
isError: true,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}));
|
|
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.", {
|
|
92
|
-
recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
|
|
93
|
-
queryId: zod_1.z.string().describe("The saved query ID (UUID)"),
|
|
94
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId }) {
|
|
95
|
-
try {
|
|
96
|
-
const result = yield sdk.savedQueries.get(recordSlug, queryId);
|
|
97
|
-
return {
|
|
98
|
-
content: [
|
|
99
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
return {
|
|
105
|
-
content: [
|
|
106
|
-
{
|
|
107
|
-
type: "text",
|
|
108
|
-
text: (0, _register_js_1.formatError)(error, `getting saved query '${queryId}'`),
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
isError: true,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}));
|
|
115
|
-
(0, _register_js_1.registerTool)(server, "create_smart_query", `Create a new saved query for a collection.
|
|
116
|
-
|
|
117
|
-
${CANONICAL_QUERY_DEFINITION_HINT}`, {
|
|
118
|
-
recordSlug: zod_1.z.string().describe("The collection's record slug to create the query for"),
|
|
119
|
-
name: zod_1.z.string().describe("Display name for the saved query"),
|
|
120
|
-
description: zod_1.z.string().optional().describe("Optional description"),
|
|
121
|
-
queryDefinition: zod_1.z
|
|
122
|
-
.record(zod_1.z.string(), zod_1.z.any())
|
|
123
|
-
.describe("Canonical inner QueryDefinition (without 'resource'). Use 'where', 'text', 'sort', 'page', 'select'. Variables are inferred from '{{varName}}' placeholders inside operator values."),
|
|
124
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, name, description, queryDefinition }) {
|
|
125
|
-
try {
|
|
126
|
-
const input = { name, queryDefinition };
|
|
127
|
-
if (description !== undefined)
|
|
128
|
-
input.description = description;
|
|
129
|
-
const result = yield sdk.savedQueries.create(recordSlug, input);
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
133
|
-
],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
return {
|
|
138
|
-
content: [
|
|
139
|
-
{
|
|
140
|
-
type: "text",
|
|
141
|
-
text: (0, _register_js_1.formatError)(error, `creating saved query '${name}' for '${recordSlug}'`),
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
isError: true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
}));
|
|
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}`, {
|
|
151
|
-
recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
|
|
152
|
-
queryId: zod_1.z.string().describe("The saved query ID (UUID) to update"),
|
|
153
|
-
name: zod_1.z.string().optional().describe("Updated display name"),
|
|
154
|
-
description: zod_1.z.string().optional().describe("Updated description"),
|
|
155
|
-
queryDefinition: zod_1.z
|
|
156
|
-
.record(zod_1.z.string(), zod_1.z.any())
|
|
157
|
-
.optional()
|
|
158
|
-
.describe("Updated canonical inner QueryDefinition (without 'resource')."),
|
|
159
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId, name, description, queryDefinition }) {
|
|
160
|
-
try {
|
|
161
|
-
const input = {};
|
|
162
|
-
if (name !== undefined)
|
|
163
|
-
input.name = name;
|
|
164
|
-
if (description !== undefined)
|
|
165
|
-
input.description = description;
|
|
166
|
-
if (queryDefinition !== undefined)
|
|
167
|
-
input.queryDefinition = queryDefinition;
|
|
168
|
-
const result = yield sdk.savedQueries.update(recordSlug, queryId, input);
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
172
|
-
],
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
return {
|
|
177
|
-
content: [
|
|
178
|
-
{
|
|
179
|
-
type: "text",
|
|
180
|
-
text: (0, _register_js_1.formatError)(error, `updating saved query '${queryId}'`),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
isError: true,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}));
|
|
187
|
-
(0, _register_js_1.registerTool)(server, "delete_smart_query", "Delete a saved query by ID.", {
|
|
188
|
-
recordSlug: zod_1.z.string().describe("The collection's record slug the query belongs to"),
|
|
189
|
-
queryId: zod_1.z.string().describe("The saved query ID (UUID) to delete"),
|
|
190
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryId }) {
|
|
191
|
-
try {
|
|
192
|
-
yield sdk.savedQueries.delete(recordSlug, queryId);
|
|
193
|
-
return {
|
|
194
|
-
content: [
|
|
195
|
-
{
|
|
196
|
-
type: "text",
|
|
197
|
-
text: `Saved query '${queryId}' deleted from '${recordSlug}'.`,
|
|
198
|
-
},
|
|
199
|
-
],
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
return {
|
|
204
|
-
content: [
|
|
205
|
-
{
|
|
206
|
-
type: "text",
|
|
207
|
-
text: (0, _register_js_1.formatError)(error, `deleting saved query '${queryId}'`),
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
isError: true,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
}));
|
|
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}`, {
|
|
217
|
-
recordSlug: zod_1.z.string().describe("The collection's record slug to test against"),
|
|
218
|
-
queryDefinition: zod_1.z
|
|
219
|
-
.record(zod_1.z.string(), zod_1.z.any())
|
|
220
|
-
.describe("Canonical inner QueryDefinition to test (without 'resource')."),
|
|
221
|
-
variables: zod_1.z
|
|
222
|
-
.record(zod_1.z.string(), zod_1.z.any())
|
|
223
|
-
.optional()
|
|
224
|
-
.describe("Optional variables to substitute."),
|
|
225
|
-
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, queryDefinition, variables }) {
|
|
226
|
-
try {
|
|
227
|
-
const input = { queryDefinition };
|
|
228
|
-
if (variables !== undefined)
|
|
229
|
-
input.variables = variables;
|
|
230
|
-
const result = yield sdk.savedQueries.test(recordSlug, input);
|
|
231
|
-
return {
|
|
232
|
-
content: [
|
|
233
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
234
|
-
],
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
return {
|
|
239
|
-
content: [
|
|
240
|
-
{
|
|
241
|
-
type: "text",
|
|
242
|
-
text: (0, _register_js_1.formatError)(error, `test-executing saved query for '${recordSlug}'`),
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
isError: true,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { registerTool, formatError } from "./_register.js";
|
|
5
|
-
|
|
6
|
-
// Tool names below stay as `*_smart_query` to keep existing AI assistant
|
|
7
|
-
// configurations working — renaming would break public clients. Internally
|
|
8
|
-
// they route through `sdk.savedQueries.*` (the canonical namespace) and
|
|
9
|
-
// accept canonical query bodies.
|
|
10
|
-
|
|
11
|
-
const CANONICAL_QUERY_DEFINITION_HINT = `Canonical QueryDefinition body (saved queries store the inner shape — 'resource' is filled in from the collection slug arg).
|
|
12
|
-
|
|
13
|
-
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'.
|
|
14
|
-
|
|
15
|
-
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:
|
|
16
|
-
{
|
|
17
|
-
"where": { "data.status": { "eq": "{{statusFilter}}" } },
|
|
18
|
-
"sort": [{ "field": "createdAt", "direction": "desc" }],
|
|
19
|
-
"page": { "limit": 100 }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
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.`;
|
|
23
|
-
|
|
24
|
-
export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
25
|
-
registerTool<any>(server,
|
|
26
|
-
"list_smart_queries",
|
|
27
|
-
"List saved (a.k.a. smart) queries. Saved queries are reusable, parameterized queries defined in the Centrali console. Optionally filter by collection record slug.",
|
|
28
|
-
{
|
|
29
|
-
recordSlug: z
|
|
30
|
-
.string()
|
|
31
|
-
.optional()
|
|
32
|
-
.describe(
|
|
33
|
-
"Filter by collection record slug. If omitted, lists all saved queries in the workspace"
|
|
34
|
-
),
|
|
35
|
-
},
|
|
36
|
-
async ({ recordSlug }) => {
|
|
37
|
-
try {
|
|
38
|
-
const result = recordSlug
|
|
39
|
-
? await sdk.savedQueries.list(recordSlug)
|
|
40
|
-
: await sdk.savedQueries.listAll();
|
|
41
|
-
return {
|
|
42
|
-
content: [
|
|
43
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
} catch (error: unknown) {
|
|
47
|
-
return {
|
|
48
|
-
content: [
|
|
49
|
-
{
|
|
50
|
-
type: "text",
|
|
51
|
-
text: formatError(error, "listing saved queries"),
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
isError: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
registerTool<any>(server,
|
|
61
|
-
"execute_smart_query",
|
|
62
|
-
"Execute a saved query by ID and return the canonical { data, meta } envelope. Saved queries can declare variables referenced as '{{varName}}' inside operator values.",
|
|
63
|
-
{
|
|
64
|
-
recordSlug: z
|
|
65
|
-
.string()
|
|
66
|
-
.describe("The collection's record slug the query belongs to"),
|
|
67
|
-
queryId: z.string().describe("The saved query ID (UUID) to execute"),
|
|
68
|
-
variables: z
|
|
69
|
-
.record(z.string(), z.any())
|
|
70
|
-
.optional()
|
|
71
|
-
.describe(
|
|
72
|
-
"Variables to substitute (key-value pairs matching '{{varName}}' placeholders). Values must match the variable's declared type."
|
|
73
|
-
),
|
|
74
|
-
},
|
|
75
|
-
async ({ recordSlug, queryId, variables }) => {
|
|
76
|
-
try {
|
|
77
|
-
const options = variables ? { variables } : undefined;
|
|
78
|
-
const result = await sdk.savedQueries.execute(
|
|
79
|
-
recordSlug,
|
|
80
|
-
queryId,
|
|
81
|
-
options
|
|
82
|
-
);
|
|
83
|
-
return {
|
|
84
|
-
content: [
|
|
85
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
} catch (error: unknown) {
|
|
89
|
-
return {
|
|
90
|
-
content: [
|
|
91
|
-
{
|
|
92
|
-
type: "text",
|
|
93
|
-
text: formatError(error, `executing saved query '${queryId}'`),
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
isError: true,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
registerTool<any>(server,
|
|
103
|
-
"get_smart_query",
|
|
104
|
-
"Get a saved query by ID. Returns the full query definition including canonical 'where', 'sort', 'page', 'select', and variable declarations.",
|
|
105
|
-
{
|
|
106
|
-
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
107
|
-
queryId: z.string().describe("The saved query ID (UUID)"),
|
|
108
|
-
},
|
|
109
|
-
async ({ recordSlug, queryId }) => {
|
|
110
|
-
try {
|
|
111
|
-
const result = await sdk.savedQueries.get(recordSlug, queryId);
|
|
112
|
-
return {
|
|
113
|
-
content: [
|
|
114
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
115
|
-
],
|
|
116
|
-
};
|
|
117
|
-
} catch (error: unknown) {
|
|
118
|
-
return {
|
|
119
|
-
content: [
|
|
120
|
-
{
|
|
121
|
-
type: "text",
|
|
122
|
-
text: formatError(error, `getting saved query '${queryId}'`),
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
isError: true,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
registerTool<any>(server,
|
|
132
|
-
"create_smart_query",
|
|
133
|
-
`Create a new saved query for a collection.
|
|
134
|
-
|
|
135
|
-
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
136
|
-
{
|
|
137
|
-
recordSlug: z.string().describe("The collection's record slug to create the query for"),
|
|
138
|
-
name: z.string().describe("Display name for the saved query"),
|
|
139
|
-
description: z.string().optional().describe("Optional description"),
|
|
140
|
-
queryDefinition: z
|
|
141
|
-
.record(z.string(), z.any())
|
|
142
|
-
.describe(
|
|
143
|
-
"Canonical inner QueryDefinition (without 'resource'). Use 'where', 'text', 'sort', 'page', 'select'. Variables are inferred from '{{varName}}' placeholders inside operator values."
|
|
144
|
-
),
|
|
145
|
-
},
|
|
146
|
-
async ({ recordSlug, name, description, queryDefinition }) => {
|
|
147
|
-
try {
|
|
148
|
-
const input: Record<string, any> = { name, queryDefinition };
|
|
149
|
-
if (description !== undefined) input.description = description;
|
|
150
|
-
|
|
151
|
-
const result = await sdk.savedQueries.create(recordSlug, input as any);
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
155
|
-
],
|
|
156
|
-
};
|
|
157
|
-
} catch (error: unknown) {
|
|
158
|
-
return {
|
|
159
|
-
content: [
|
|
160
|
-
{
|
|
161
|
-
type: "text",
|
|
162
|
-
text: formatError(error, `creating saved query '${name}' for '${recordSlug}'`),
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
isError: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
registerTool<any>(server,
|
|
172
|
-
"update_smart_query",
|
|
173
|
-
`Update an existing saved query. Only include the fields you want to change.
|
|
174
|
-
|
|
175
|
-
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
176
|
-
{
|
|
177
|
-
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
178
|
-
queryId: z.string().describe("The saved query ID (UUID) to update"),
|
|
179
|
-
name: z.string().optional().describe("Updated display name"),
|
|
180
|
-
description: z.string().optional().describe("Updated description"),
|
|
181
|
-
queryDefinition: z
|
|
182
|
-
.record(z.string(), z.any())
|
|
183
|
-
.optional()
|
|
184
|
-
.describe("Updated canonical inner QueryDefinition (without 'resource')."),
|
|
185
|
-
},
|
|
186
|
-
async ({ recordSlug, queryId, name, description, queryDefinition }) => {
|
|
187
|
-
try {
|
|
188
|
-
const input: Record<string, any> = {};
|
|
189
|
-
if (name !== undefined) input.name = name;
|
|
190
|
-
if (description !== undefined) input.description = description;
|
|
191
|
-
if (queryDefinition !== undefined) input.queryDefinition = queryDefinition;
|
|
192
|
-
|
|
193
|
-
const result = await sdk.savedQueries.update(recordSlug, queryId, input as any);
|
|
194
|
-
return {
|
|
195
|
-
content: [
|
|
196
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
197
|
-
],
|
|
198
|
-
};
|
|
199
|
-
} catch (error: unknown) {
|
|
200
|
-
return {
|
|
201
|
-
content: [
|
|
202
|
-
{
|
|
203
|
-
type: "text",
|
|
204
|
-
text: formatError(error, `updating saved query '${queryId}'`),
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
isError: true,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
registerTool<any>(server,
|
|
214
|
-
"delete_smart_query",
|
|
215
|
-
"Delete a saved query by ID.",
|
|
216
|
-
{
|
|
217
|
-
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
218
|
-
queryId: z.string().describe("The saved query ID (UUID) to delete"),
|
|
219
|
-
},
|
|
220
|
-
async ({ recordSlug, queryId }) => {
|
|
221
|
-
try {
|
|
222
|
-
await sdk.savedQueries.delete(recordSlug, queryId);
|
|
223
|
-
return {
|
|
224
|
-
content: [
|
|
225
|
-
{
|
|
226
|
-
type: "text",
|
|
227
|
-
text: `Saved query '${queryId}' deleted from '${recordSlug}'.`,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
};
|
|
231
|
-
} catch (error: unknown) {
|
|
232
|
-
return {
|
|
233
|
-
content: [
|
|
234
|
-
{
|
|
235
|
-
type: "text",
|
|
236
|
-
text: formatError(error, `deleting saved query '${queryId}'`),
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
isError: true,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
registerTool<any>(server,
|
|
246
|
-
"test_smart_query",
|
|
247
|
-
`Test execute a query definition without saving it. Useful for validating syntax and previewing results before creating a saved query.
|
|
248
|
-
|
|
249
|
-
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
250
|
-
{
|
|
251
|
-
recordSlug: z.string().describe("The collection's record slug to test against"),
|
|
252
|
-
queryDefinition: z
|
|
253
|
-
.record(z.string(), z.any())
|
|
254
|
-
.describe("Canonical inner QueryDefinition to test (without 'resource')."),
|
|
255
|
-
variables: z
|
|
256
|
-
.record(z.string(), z.any())
|
|
257
|
-
.optional()
|
|
258
|
-
.describe("Optional variables to substitute."),
|
|
259
|
-
},
|
|
260
|
-
async ({ recordSlug, queryDefinition, variables }) => {
|
|
261
|
-
try {
|
|
262
|
-
const input: Record<string, any> = { queryDefinition };
|
|
263
|
-
if (variables !== undefined) input.variables = variables;
|
|
264
|
-
|
|
265
|
-
const result = await sdk.savedQueries.test(recordSlug, input as any);
|
|
266
|
-
return {
|
|
267
|
-
content: [
|
|
268
|
-
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
269
|
-
],
|
|
270
|
-
};
|
|
271
|
-
} catch (error: unknown) {
|
|
272
|
-
return {
|
|
273
|
-
content: [
|
|
274
|
-
{
|
|
275
|
-
type: "text",
|
|
276
|
-
text: formatError(error, `test-executing saved query for '${recordSlug}'`),
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
isError: true,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
);
|
|
284
|
-
}
|