@centrali-io/centrali-mcp 4.4.8 → 4.4.9
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 +17 -0
- package/dist/index.js +1 -1
- package/dist/tools/compute.js +167 -0
- package/dist/tools/describe.js +5 -5
- package/dist/tools/records.d.ts +1 -1
- package/dist/tools/records.js +206 -1
- package/dist/tools/structures.js +139 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/tools/compute.ts +172 -0
- package/src/tools/describe.ts +5 -5
- package/src/tools/records.ts +240 -1
- package/src/tools/structures.ts +140 -0
package/README.md
CHANGED
|
@@ -36,6 +36,23 @@ Add to your MCP client configuration (e.g., Claude Desktop, Cursor):
|
|
|
36
36
|
| `CENTRALI_CLIENT_SECRET` | Yes | Service account client secret |
|
|
37
37
|
| `CENTRALI_WORKSPACE` | Yes | Workspace slug to operate in |
|
|
38
38
|
|
|
39
|
+
### Service Account Permissions
|
|
40
|
+
|
|
41
|
+
A freshly created service account **has no permissions by default**. You must assign it a role before the MCP server can do anything useful, otherwise every tool call will return `403 Forbidden`.
|
|
42
|
+
|
|
43
|
+
**Quickest setup — full admin access:**
|
|
44
|
+
|
|
45
|
+
1. In the Console, go to **Settings → Service Accounts**
|
|
46
|
+
2. Create your service account and save the client secret
|
|
47
|
+
3. Open the service account, go to the **Groups** tab
|
|
48
|
+
4. Add it to the **workspace_administrators** group
|
|
49
|
+
|
|
50
|
+
This gives the MCP server full access to all workspace resources.
|
|
51
|
+
|
|
52
|
+
**Least-privilege setup — custom permissions:**
|
|
53
|
+
|
|
54
|
+
If you only want the MCP to manage specific resources (e.g., collections and records but not billing), create a custom group with a policy scoped to the resources you need, then add the service account to that group. See the [Policies and Permissions guide](https://docs.centrali.io/authentication/policies-and-permissions/) for details.
|
|
55
|
+
|
|
39
56
|
## Getting Started
|
|
40
57
|
|
|
41
58
|
After connecting, call `describe_centrali` first — it returns the full capability map, feature matrix, and SDK integration guidance. Then use the specific `describe_*` tools for deeper schema details on any domain.
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ function main() {
|
|
|
53
53
|
// Register all tools
|
|
54
54
|
(0, structures_js_1.registerCollectionTools)(server, sdk, baseUrl, workspaceId);
|
|
55
55
|
(0, structures_js_1.registerStructureTools)(server, sdk);
|
|
56
|
-
(0, records_js_1.registerRecordTools)(server, sdk);
|
|
56
|
+
(0, records_js_1.registerRecordTools)(server, sdk, baseUrl, workspaceId);
|
|
57
57
|
(0, search_js_1.registerSearchTools)(server, sdk);
|
|
58
58
|
(0, compute_js_1.registerComputeTools)(server, sdk, baseUrl, workspaceId);
|
|
59
59
|
(0, smart_queries_js_1.registerSmartQueryTools)(server, sdk);
|
package/dist/tools/compute.js
CHANGED
|
@@ -735,4 +735,171 @@ function registerComputeTools(server, sdk, centraliUrl, workspaceId) {
|
|
|
735
735
|
};
|
|
736
736
|
}
|
|
737
737
|
}));
|
|
738
|
+
// ── Scaffold (function + trigger in one step) ─────────────────────
|
|
739
|
+
server.tool("scaffold_function", "Create a compute function with boilerplate code and optionally wire a trigger — all in one step. The function uses the required `async function run()` pattern with `api.*` globals, `triggerParams`, and `executionParams` available.", {
|
|
740
|
+
name: zod_1.z.string().describe("Function name"),
|
|
741
|
+
description: zod_1.z.string().optional().describe("Function description"),
|
|
742
|
+
code: zod_1.z.string().describe("Function code. Must use `async function run() { ... }` pattern. Has access to `api` (data operations), `triggerParams` (static config), and `executionParams` (runtime parameters)."),
|
|
743
|
+
trigger: zod_1.z.object({
|
|
744
|
+
type: zod_1.z.enum(["on_demand", "record", "schedule", "endpoint"]).describe("Trigger type"),
|
|
745
|
+
name: zod_1.z.string().optional().describe("Trigger name (auto-generated if omitted)"),
|
|
746
|
+
description: zod_1.z.string().optional(),
|
|
747
|
+
// Record trigger fields
|
|
748
|
+
recordSlug: zod_1.z.string().optional().describe("Collection slug (required for record triggers)"),
|
|
749
|
+
event: zod_1.z.string().optional().describe("Record event: afterCreate, afterUpdate, afterDelete (required for record triggers)"),
|
|
750
|
+
// Schedule trigger fields
|
|
751
|
+
cronExpression: zod_1.z.string().optional().describe("Cron expression (required for schedule triggers)"),
|
|
752
|
+
// Endpoint trigger fields
|
|
753
|
+
endpointPath: zod_1.z.string().optional().describe("URL path segment (required for endpoint triggers)"),
|
|
754
|
+
// Common
|
|
755
|
+
params: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional().describe("Static trigger parameters available as triggerParams in the function"),
|
|
756
|
+
}).optional().describe("Trigger configuration. If omitted, creates function without a trigger."),
|
|
757
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ name, description, code, trigger }) {
|
|
758
|
+
try {
|
|
759
|
+
// Validate code pattern
|
|
760
|
+
if (/module\.exports\s*=/.test(code)) {
|
|
761
|
+
return {
|
|
762
|
+
content: [
|
|
763
|
+
{
|
|
764
|
+
type: "text",
|
|
765
|
+
text: "Error: Do not use module.exports. Functions must define 'async function run() { ... }'. Globals available: api, triggerParams, executionParams.",
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
isError: true,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
// Step 1: Create the function
|
|
772
|
+
const fnInput = { name, code };
|
|
773
|
+
if (description !== undefined)
|
|
774
|
+
fnInput.description = description;
|
|
775
|
+
const fnResult = yield sdk.functions.create(fnInput);
|
|
776
|
+
const fn = fnResult.data;
|
|
777
|
+
const fnId = fn.id;
|
|
778
|
+
// If no trigger requested, return function only
|
|
779
|
+
if (!trigger) {
|
|
780
|
+
return {
|
|
781
|
+
content: [
|
|
782
|
+
{
|
|
783
|
+
type: "text",
|
|
784
|
+
text: [
|
|
785
|
+
`Function created:`,
|
|
786
|
+
` ID: ${fnId}`,
|
|
787
|
+
` Name: ${name}`,
|
|
788
|
+
``,
|
|
789
|
+
`No trigger requested. Use create_trigger to wire one later.`,
|
|
790
|
+
].join("\n"),
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// Step 2: Build trigger input from the simplified config
|
|
796
|
+
const typeMap = {
|
|
797
|
+
on_demand: "on-demand",
|
|
798
|
+
record: "event-driven",
|
|
799
|
+
schedule: "scheduled",
|
|
800
|
+
endpoint: "endpoint",
|
|
801
|
+
};
|
|
802
|
+
const executionType = typeMap[trigger.type];
|
|
803
|
+
const triggerName = trigger.name || `${name} trigger`;
|
|
804
|
+
const triggerInput = {
|
|
805
|
+
name: triggerName,
|
|
806
|
+
functionId: fnId,
|
|
807
|
+
executionType,
|
|
808
|
+
};
|
|
809
|
+
if (trigger.description !== undefined)
|
|
810
|
+
triggerInput.description = trigger.description;
|
|
811
|
+
// Build triggerMetadata based on type
|
|
812
|
+
const meta = {};
|
|
813
|
+
if (trigger.params)
|
|
814
|
+
meta.params = trigger.params;
|
|
815
|
+
if (trigger.type === "record") {
|
|
816
|
+
if (trigger.recordSlug)
|
|
817
|
+
meta.recordSlug = trigger.recordSlug;
|
|
818
|
+
if (trigger.event)
|
|
819
|
+
meta.eventType = trigger.event;
|
|
820
|
+
}
|
|
821
|
+
else if (trigger.type === "schedule") {
|
|
822
|
+
meta.scheduleType = "cron";
|
|
823
|
+
if (trigger.cronExpression)
|
|
824
|
+
meta.cronExpression = trigger.cronExpression;
|
|
825
|
+
}
|
|
826
|
+
else if (trigger.type === "endpoint") {
|
|
827
|
+
if (trigger.endpointPath)
|
|
828
|
+
meta.path = trigger.endpointPath;
|
|
829
|
+
}
|
|
830
|
+
if (Object.keys(meta).length > 0)
|
|
831
|
+
triggerInput.triggerMetadata = meta;
|
|
832
|
+
// Step 3: Create the trigger
|
|
833
|
+
let triggerData;
|
|
834
|
+
try {
|
|
835
|
+
const triggerResult = yield sdk.triggers.create(triggerInput);
|
|
836
|
+
triggerData = triggerResult.data;
|
|
837
|
+
}
|
|
838
|
+
catch (triggerError) {
|
|
839
|
+
// Function succeeded but trigger failed — report both
|
|
840
|
+
return {
|
|
841
|
+
content: [
|
|
842
|
+
{
|
|
843
|
+
type: "text",
|
|
844
|
+
text: [
|
|
845
|
+
`Function created:`,
|
|
846
|
+
` ID: ${fnId}`,
|
|
847
|
+
` Name: ${name}`,
|
|
848
|
+
``,
|
|
849
|
+
formatError(triggerError, "creating trigger"),
|
|
850
|
+
``,
|
|
851
|
+
`The function was created successfully but the trigger failed. You can retry with create_trigger using functionId '${fnId}'.`,
|
|
852
|
+
].join("\n"),
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// Build a human-friendly trigger type label
|
|
858
|
+
let typeLabel = trigger.type;
|
|
859
|
+
if (trigger.type === "record" && trigger.event && trigger.recordSlug) {
|
|
860
|
+
typeLabel = `record (${trigger.event} on ${trigger.recordSlug})`;
|
|
861
|
+
}
|
|
862
|
+
else if (trigger.type === "schedule" && trigger.cronExpression) {
|
|
863
|
+
typeLabel = `schedule (${trigger.cronExpression})`;
|
|
864
|
+
}
|
|
865
|
+
else if (trigger.type === "endpoint" && trigger.endpointPath) {
|
|
866
|
+
typeLabel = `endpoint (/${trigger.endpointPath})`;
|
|
867
|
+
}
|
|
868
|
+
const lines = [
|
|
869
|
+
`Function created:`,
|
|
870
|
+
` ID: ${fnId}`,
|
|
871
|
+
` Name: ${name}`,
|
|
872
|
+
``,
|
|
873
|
+
`Trigger created:`,
|
|
874
|
+
` ID: ${triggerData.id}`,
|
|
875
|
+
` Type: ${typeLabel}`,
|
|
876
|
+
``,
|
|
877
|
+
];
|
|
878
|
+
if (trigger.type === "on_demand") {
|
|
879
|
+
lines.push(`Your function is ready. Invoke it via the trigger or use invoke_trigger with ID ${triggerData.id}.`);
|
|
880
|
+
}
|
|
881
|
+
else if (trigger.type === "endpoint") {
|
|
882
|
+
lines.push(`Your function is ready. Invoke it via invoke_endpoint with path '${trigger.endpointPath}' or use invoke_trigger with ID ${triggerData.id}.`);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
lines.push(`Your function is ready. Invoke it via the trigger or use invoke_trigger with ID ${triggerData.id}.`);
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
content: [
|
|
889
|
+
{ type: "text", text: lines.join("\n") },
|
|
890
|
+
],
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
return {
|
|
895
|
+
content: [
|
|
896
|
+
{
|
|
897
|
+
type: "text",
|
|
898
|
+
text: formatError(error, `scaffolding function '${name}'`),
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
isError: true,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
}));
|
|
738
905
|
}
|
package/dist/tools/describe.js
CHANGED
|
@@ -987,7 +987,7 @@ function registerDescribeTools(server) {
|
|
|
987
987
|
domain: "Orchestrations",
|
|
988
988
|
description: "Orchestrations are multi-step workflows that chain compute functions together. Each step executes a function and passes its output to the next step.",
|
|
989
989
|
orchestration_shape: {
|
|
990
|
-
id: "
|
|
990
|
+
id: "Prefixed xid (e.g. orch_ctb5h4l6n7m8p9q0r1s2)",
|
|
991
991
|
name: "string",
|
|
992
992
|
description: "string | null",
|
|
993
993
|
status: "'draft' | 'active' | 'paused'",
|
|
@@ -1058,8 +1058,8 @@ function registerDescribeTools(server) {
|
|
|
1058
1058
|
},
|
|
1059
1059
|
},
|
|
1060
1060
|
run_shape: {
|
|
1061
|
-
id: "
|
|
1062
|
-
orchestrationId: "
|
|
1061
|
+
id: "Prefixed xid (e.g. orun_ctb5h4l6n7m8p9q0r1s2)",
|
|
1062
|
+
orchestrationId: "Prefixed xid (e.g. orch_ctb5h4l6n7m8p9q0r1s2)",
|
|
1063
1063
|
status: "'pending' | 'running' | 'waiting' | 'completed' | 'failed'",
|
|
1064
1064
|
input: "object — the input data provided when the run was triggered",
|
|
1065
1065
|
output: "object | null — final output after all steps complete",
|
|
@@ -1068,7 +1068,7 @@ function registerDescribeTools(server) {
|
|
|
1068
1068
|
correlationId: "string | null — optional tracing ID",
|
|
1069
1069
|
},
|
|
1070
1070
|
run_step_shape: {
|
|
1071
|
-
stepId: "
|
|
1071
|
+
stepId: "Prefixed xid (e.g. ostep_ctb5h4l6n7m8p9q0r1s2)",
|
|
1072
1072
|
status: "'pending' | 'running' | 'completed' | 'failed' | 'skipped'",
|
|
1073
1073
|
input: "object — input passed to this step",
|
|
1074
1074
|
output: "object | null — output returned by this step",
|
|
@@ -1903,7 +1903,7 @@ function registerDescribeTools(server) {
|
|
|
1903
1903
|
},
|
|
1904
1904
|
"start-orchestration": {
|
|
1905
1905
|
description: "Start an orchestration workflow run. Use for multi-step processes.",
|
|
1906
|
-
targetRef: "Orchestration ID (
|
|
1906
|
+
targetRef: "Orchestration ID (prefixed xid, e.g. orch_...)",
|
|
1907
1907
|
typical_activation: "button",
|
|
1908
1908
|
executionMode: "'fire-and-poll' recommended — orchestrations are long-running",
|
|
1909
1909
|
},
|
package/dist/tools/records.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
-
export declare function registerRecordTools(server: McpServer, sdk: CentraliSDK): void;
|
|
3
|
+
export declare function registerRecordTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string): void;
|
package/dist/tools/records.js
CHANGED
|
@@ -8,8 +8,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.registerRecordTools = registerRecordTools;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
13
17
|
const zod_1 = require("zod");
|
|
14
18
|
function formatError(error, context) {
|
|
15
19
|
var _a, _b;
|
|
@@ -33,7 +37,59 @@ function formatError(error, context) {
|
|
|
33
37
|
}
|
|
34
38
|
return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Ensures the SDK has a valid token.
|
|
42
|
+
*/
|
|
43
|
+
function ensureToken(sdk) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
let token = sdk.getToken();
|
|
46
|
+
if (token)
|
|
47
|
+
return token;
|
|
48
|
+
try {
|
|
49
|
+
yield sdk.queryRecords("__noop__", { limit: 1 });
|
|
50
|
+
}
|
|
51
|
+
catch ( /* token refresh side effect */_a) { /* token refresh side effect */ }
|
|
52
|
+
return sdk.getToken();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates an axios instance pointing at the data service records API for a given recordSlug.
|
|
57
|
+
*/
|
|
58
|
+
function createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug) {
|
|
59
|
+
const url = new URL(centraliUrl);
|
|
60
|
+
const hostname = url.hostname.startsWith("api.")
|
|
61
|
+
? url.hostname
|
|
62
|
+
: `api.${url.hostname}`;
|
|
63
|
+
const baseURL = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/records/slug/${recordSlug}`;
|
|
64
|
+
const client = axios_1.default.create({ baseURL, proxy: false });
|
|
65
|
+
client.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
const token = yield ensureToken(sdk);
|
|
67
|
+
if (token) {
|
|
68
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
69
|
+
}
|
|
70
|
+
return config;
|
|
71
|
+
}));
|
|
72
|
+
client.interceptors.response.use((response) => response, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
const originalRequest = error.config;
|
|
75
|
+
const isAuthError = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403;
|
|
76
|
+
if (isAuthError && !originalRequest._hasRetried) {
|
|
77
|
+
originalRequest._hasRetried = true;
|
|
78
|
+
try {
|
|
79
|
+
yield sdk.queryRecords("__noop__", { limit: 1 });
|
|
80
|
+
}
|
|
81
|
+
catch ( /* token refresh side effect */_c) { /* token refresh side effect */ }
|
|
82
|
+
const token = sdk.getToken();
|
|
83
|
+
if (token) {
|
|
84
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
85
|
+
return client.request(originalRequest);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Promise.reject(error);
|
|
89
|
+
}));
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
function registerRecordTools(server, sdk, centraliUrl, workspaceId) {
|
|
37
93
|
server.tool("query_records", "Query records from a collection with optional filters, sorting, and pagination. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100).", {
|
|
38
94
|
recordSlug: zod_1.z
|
|
39
95
|
.string()
|
|
@@ -268,4 +324,153 @@ function registerRecordTools(server, sdk) {
|
|
|
268
324
|
};
|
|
269
325
|
}
|
|
270
326
|
}));
|
|
327
|
+
// ── Bulk Operations (1 aggregate event per operation) ───────────────
|
|
328
|
+
server.tool("bulk_create_records", `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.`, {
|
|
329
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug (e.g., 'orders')"),
|
|
330
|
+
records: zod_1.z
|
|
331
|
+
.array(zod_1.z.record(zod_1.z.string(), zod_1.z.any()))
|
|
332
|
+
.describe("Array of record data objects to create (max 1000). Each object has field names as keys."),
|
|
333
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, records }) {
|
|
334
|
+
try {
|
|
335
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
336
|
+
const result = yield client.post("/bulk", records);
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: formatError(error, `bulk creating records in '${recordSlug}'`) }],
|
|
344
|
+
isError: true,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}));
|
|
348
|
+
server.tool("bulk_update_records", `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.`, {
|
|
349
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug"),
|
|
350
|
+
ids: zod_1.z.array(zod_1.z.string()).describe("Array of record IDs (UUIDs) to update"),
|
|
351
|
+
data: zod_1.z
|
|
352
|
+
.record(zod_1.z.string(), zod_1.z.any())
|
|
353
|
+
.describe("Object with fields to set on ALL specified records (same data applied to each)"),
|
|
354
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, ids, data }) {
|
|
355
|
+
try {
|
|
356
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
357
|
+
const result = yield client.patch("/bulk", { ids, data });
|
|
358
|
+
return {
|
|
359
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text", text: formatError(error, `bulk updating records in '${recordSlug}'`) }],
|
|
365
|
+
isError: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
server.tool("bulk_delete_records", `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.`, {
|
|
370
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug"),
|
|
371
|
+
ids: zod_1.z.array(zod_1.z.string()).describe("Array of record IDs (UUIDs) to delete"),
|
|
372
|
+
hard: zod_1.z
|
|
373
|
+
.boolean()
|
|
374
|
+
.optional()
|
|
375
|
+
.describe("If true, permanently delete. Default: false (soft delete, can be restored)"),
|
|
376
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, ids, hard }) {
|
|
377
|
+
try {
|
|
378
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
379
|
+
const body = { ids };
|
|
380
|
+
if (hard !== undefined)
|
|
381
|
+
body.hard = hard;
|
|
382
|
+
const result = yield client.delete("/bulk", { data: body });
|
|
383
|
+
const deleteType = hard ? "permanently deleted" : "soft-deleted";
|
|
384
|
+
return {
|
|
385
|
+
content: [
|
|
386
|
+
{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: `${ids.length} records ${deleteType} from '${recordSlug}'.\n\n${JSON.stringify(result.data, null, 2)}`,
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: "text", text: formatError(error, `bulk deleting records from '${recordSlug}'`) }],
|
|
396
|
+
isError: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}));
|
|
400
|
+
// ── Batch Operations (1 event per record) ──────────────────────────
|
|
401
|
+
server.tool("batch_create_records", `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.`, {
|
|
402
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug (e.g., 'orders')"),
|
|
403
|
+
records: zod_1.z
|
|
404
|
+
.array(zod_1.z.record(zod_1.z.string(), zod_1.z.any()))
|
|
405
|
+
.describe("Array of record data objects to create. Each object has field names as keys."),
|
|
406
|
+
stopOnError: zod_1.z
|
|
407
|
+
.boolean()
|
|
408
|
+
.optional()
|
|
409
|
+
.describe("If true, stop processing on the first error. Default: false (continue and report failures)"),
|
|
410
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, records, stopOnError }) {
|
|
411
|
+
try {
|
|
412
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
413
|
+
const params = {};
|
|
414
|
+
if (stopOnError !== undefined)
|
|
415
|
+
params.stopOnError = stopOnError;
|
|
416
|
+
const result = yield client.post("/batch", records, { params });
|
|
417
|
+
return {
|
|
418
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: "text", text: formatError(error, `batch creating records in '${recordSlug}'`) }],
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
server.tool("batch_update_records", `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.`, {
|
|
429
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug"),
|
|
430
|
+
updates: zod_1.z
|
|
431
|
+
.array(zod_1.z.object({
|
|
432
|
+
id: zod_1.z.string().describe("The record ID (UUID) to update"),
|
|
433
|
+
data: zod_1.z
|
|
434
|
+
.record(zod_1.z.string(), zod_1.z.any())
|
|
435
|
+
.describe("Object with fields to update for this specific record"),
|
|
436
|
+
}))
|
|
437
|
+
.describe("Array of update objects, each with an 'id' and 'data' to apply to that record"),
|
|
438
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, updates }) {
|
|
439
|
+
try {
|
|
440
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
441
|
+
const result = yield client.patch("/batch", updates);
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
return {
|
|
448
|
+
content: [{ type: "text", text: formatError(error, `batch updating records in '${recordSlug}'`) }],
|
|
449
|
+
isError: true,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}));
|
|
453
|
+
server.tool("batch_delete_records", `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.`, {
|
|
454
|
+
recordSlug: zod_1.z.string().describe("The collection's record slug"),
|
|
455
|
+
ids: zod_1.z.array(zod_1.z.string()).describe("Array of record IDs (UUIDs) to delete"),
|
|
456
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, ids }) {
|
|
457
|
+
try {
|
|
458
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
459
|
+
const result = yield client.delete("/batch", { data: { ids } });
|
|
460
|
+
return {
|
|
461
|
+
content: [
|
|
462
|
+
{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: `${ids.length} records batch-deleted from '${recordSlug}'.\n\n${JSON.stringify(result.data, null, 2)}`,
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
return {
|
|
471
|
+
content: [{ type: "text", text: formatError(error, `batch deleting records from '${recordSlug}'`) }],
|
|
472
|
+
isError: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}));
|
|
271
476
|
}
|
package/dist/tools/structures.js
CHANGED
|
@@ -500,4 +500,143 @@ Each fix is an object with: { fieldName, fixType, value?, expression?, applyToAl
|
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
502
|
}));
|
|
503
|
+
server.tool("explore_collections", "Get a compact overview of ALL collections in the workspace with their field names, types, and constraints. Returns everything in one call — no need to list collections and then get each one separately.", {
|
|
504
|
+
includeRecordCounts: zod_1.z.boolean().optional().describe("Include record counts for each collection (slower, requires an extra query per collection). Defaults to false."),
|
|
505
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ includeRecordCounts }) {
|
|
506
|
+
var _b, _c, _d, _e;
|
|
507
|
+
try {
|
|
508
|
+
// Fetch all collections (paginate to get everything)
|
|
509
|
+
const firstPage = yield sdk.collections.list({ page: 1, limit: 100 });
|
|
510
|
+
let allCollections = (_b = firstPage.data) !== null && _b !== void 0 ? _b : [];
|
|
511
|
+
const meta = firstPage.meta;
|
|
512
|
+
if (meta && meta.total > allCollections.length) {
|
|
513
|
+
const totalPages = Math.ceil(meta.total / 100);
|
|
514
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
515
|
+
const nextPage = yield sdk.collections.list({ page, limit: 100 });
|
|
516
|
+
allCollections = allCollections.concat((_c = nextPage.data) !== null && _c !== void 0 ? _c : []);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (allCollections.length === 0) {
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: "text", text: "No collections found in this workspace." }],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Fetch full schema for each collection
|
|
525
|
+
const details = yield Promise.all(allCollections.map((col) => __awaiter(this, void 0, void 0, function* () {
|
|
526
|
+
var _a, _b, _c;
|
|
527
|
+
try {
|
|
528
|
+
const full = yield sdk.collections.getBySlug(col.recordSlug);
|
|
529
|
+
const detail = Object.assign({}, ((_a = full.data) !== null && _a !== void 0 ? _a : col));
|
|
530
|
+
if (includeRecordCounts) {
|
|
531
|
+
try {
|
|
532
|
+
const records = yield sdk.queryRecords(col.recordSlug, { pageSize: 1 });
|
|
533
|
+
detail._recordCount = (_c = (_b = records.meta) === null || _b === void 0 ? void 0 : _b.total) !== null && _c !== void 0 ? _c : "unknown";
|
|
534
|
+
}
|
|
535
|
+
catch (_d) {
|
|
536
|
+
detail._recordCount = "unknown";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return detail;
|
|
540
|
+
}
|
|
541
|
+
catch (_e) {
|
|
542
|
+
return Object.assign(Object.assign({}, col), { _fetchError: true });
|
|
543
|
+
}
|
|
544
|
+
})));
|
|
545
|
+
// Format as compact readable text
|
|
546
|
+
const lines = [];
|
|
547
|
+
for (const col of details) {
|
|
548
|
+
lines.push(`=== Collection: ${col.name} (slug: ${col.recordSlug}) ===`);
|
|
549
|
+
if (col.description) {
|
|
550
|
+
lines.push(`Description: ${col.description}`);
|
|
551
|
+
}
|
|
552
|
+
lines.push(`Schema mode: ${(_d = col.schemaDiscoveryMode) !== null && _d !== void 0 ? _d : "strict"}`);
|
|
553
|
+
if (col.tags && col.tags.length > 0) {
|
|
554
|
+
lines.push(`Tags: ${col.tags.join(", ")}`);
|
|
555
|
+
}
|
|
556
|
+
if (col._fetchError) {
|
|
557
|
+
lines.push("Fields: (unable to fetch full schema)");
|
|
558
|
+
}
|
|
559
|
+
else if (col.properties && col.properties.length > 0) {
|
|
560
|
+
lines.push("Fields:");
|
|
561
|
+
for (const prop of col.properties) {
|
|
562
|
+
const parts = [];
|
|
563
|
+
// Base type
|
|
564
|
+
let typeStr = prop.type;
|
|
565
|
+
if (prop.type === "string" && prop.renderAs) {
|
|
566
|
+
typeStr = prop.renderAs;
|
|
567
|
+
}
|
|
568
|
+
if (prop.type === "array" && ((_e = prop.items) === null || _e === void 0 ? void 0 : _e.type)) {
|
|
569
|
+
typeStr = `array of ${prop.items.type}`;
|
|
570
|
+
}
|
|
571
|
+
if (prop.type === "reference" && prop.target) {
|
|
572
|
+
typeStr = `reference -> ${prop.target}`;
|
|
573
|
+
if (prop.relationship)
|
|
574
|
+
typeStr += ` (${prop.relationship})`;
|
|
575
|
+
}
|
|
576
|
+
// Enum / select values
|
|
577
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
578
|
+
const vals = prop.enum.map(String);
|
|
579
|
+
if (vals.length <= 8) {
|
|
580
|
+
typeStr = `select: ${vals.join("|")}`;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
typeStr = `select: ${vals.slice(0, 6).join("|")}|... (${vals.length} values)`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
parts.push(typeStr);
|
|
587
|
+
// Constraints
|
|
588
|
+
if (prop.required)
|
|
589
|
+
parts.push("required");
|
|
590
|
+
if (prop.isUnique)
|
|
591
|
+
parts.push("unique");
|
|
592
|
+
if (prop.immutable)
|
|
593
|
+
parts.push("immutable");
|
|
594
|
+
if (prop.isSecret)
|
|
595
|
+
parts.push("secret");
|
|
596
|
+
if (prop.nullable)
|
|
597
|
+
parts.push("nullable");
|
|
598
|
+
if (prop.default !== undefined)
|
|
599
|
+
parts.push(`default: ${JSON.stringify(prop.default)}`);
|
|
600
|
+
if (prop.minimum !== undefined)
|
|
601
|
+
parts.push(`min: ${prop.minimum}`);
|
|
602
|
+
if (prop.maximum !== undefined)
|
|
603
|
+
parts.push(`max: ${prop.maximum}`);
|
|
604
|
+
if (prop.minLength !== undefined)
|
|
605
|
+
parts.push(`minLen: ${prop.minLength}`);
|
|
606
|
+
if (prop.maxLength !== undefined)
|
|
607
|
+
parts.push(`maxLen: ${prop.maxLength}`);
|
|
608
|
+
if (prop.pattern)
|
|
609
|
+
parts.push(`pattern: ${prop.pattern}`);
|
|
610
|
+
if (prop.expression)
|
|
611
|
+
parts.push("computed");
|
|
612
|
+
if (prop.autoIncrement)
|
|
613
|
+
parts.push("auto-increment");
|
|
614
|
+
lines.push(` - ${prop.name} (${parts.join(", ")})`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
lines.push("Fields: (none)");
|
|
619
|
+
}
|
|
620
|
+
if (includeRecordCounts && col._recordCount !== undefined) {
|
|
621
|
+
lines.push(`Records: ${col._recordCount}`);
|
|
622
|
+
}
|
|
623
|
+
lines.push("");
|
|
624
|
+
}
|
|
625
|
+
lines.push(`Total: ${details.length} collection(s)`);
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
return {
|
|
632
|
+
content: [
|
|
633
|
+
{
|
|
634
|
+
type: "text",
|
|
635
|
+
text: formatError(error, "exploring collections"),
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
isError: true,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}));
|
|
503
642
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ async function main() {
|
|
|
47
47
|
// Register all tools
|
|
48
48
|
registerCollectionTools(server, sdk, baseUrl, workspaceId);
|
|
49
49
|
registerStructureTools(server, sdk);
|
|
50
|
-
registerRecordTools(server, sdk);
|
|
50
|
+
registerRecordTools(server, sdk, baseUrl, workspaceId);
|
|
51
51
|
registerSearchTools(server, sdk);
|
|
52
52
|
registerComputeTools(server, sdk, baseUrl, workspaceId);
|
|
53
53
|
registerSmartQueryTools(server, sdk);
|
package/src/tools/compute.ts
CHANGED
|
@@ -828,4 +828,176 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centra
|
|
|
828
828
|
}
|
|
829
829
|
}
|
|
830
830
|
);
|
|
831
|
+
|
|
832
|
+
// ── Scaffold (function + trigger in one step) ─────────────────────
|
|
833
|
+
|
|
834
|
+
server.tool(
|
|
835
|
+
"scaffold_function",
|
|
836
|
+
"Create a compute function with boilerplate code and optionally wire a trigger — all in one step. The function uses the required `async function run()` pattern with `api.*` globals, `triggerParams`, and `executionParams` available.",
|
|
837
|
+
{
|
|
838
|
+
name: z.string().describe("Function name"),
|
|
839
|
+
description: z.string().optional().describe("Function description"),
|
|
840
|
+
code: z.string().describe("Function code. Must use `async function run() { ... }` pattern. Has access to `api` (data operations), `triggerParams` (static config), and `executionParams` (runtime parameters)."),
|
|
841
|
+
trigger: z.object({
|
|
842
|
+
type: z.enum(["on_demand", "record", "schedule", "endpoint"]).describe("Trigger type"),
|
|
843
|
+
name: z.string().optional().describe("Trigger name (auto-generated if omitted)"),
|
|
844
|
+
description: z.string().optional(),
|
|
845
|
+
// Record trigger fields
|
|
846
|
+
recordSlug: z.string().optional().describe("Collection slug (required for record triggers)"),
|
|
847
|
+
event: z.string().optional().describe("Record event: afterCreate, afterUpdate, afterDelete (required for record triggers)"),
|
|
848
|
+
// Schedule trigger fields
|
|
849
|
+
cronExpression: z.string().optional().describe("Cron expression (required for schedule triggers)"),
|
|
850
|
+
// Endpoint trigger fields
|
|
851
|
+
endpointPath: z.string().optional().describe("URL path segment (required for endpoint triggers)"),
|
|
852
|
+
// Common
|
|
853
|
+
params: z.record(z.string(), z.any()).optional().describe("Static trigger parameters available as triggerParams in the function"),
|
|
854
|
+
}).optional().describe("Trigger configuration. If omitted, creates function without a trigger."),
|
|
855
|
+
},
|
|
856
|
+
async ({ name, description, code, trigger }) => {
|
|
857
|
+
try {
|
|
858
|
+
// Validate code pattern
|
|
859
|
+
if (/module\.exports\s*=/.test(code)) {
|
|
860
|
+
return {
|
|
861
|
+
content: [
|
|
862
|
+
{
|
|
863
|
+
type: "text",
|
|
864
|
+
text: "Error: Do not use module.exports. Functions must define 'async function run() { ... }'. Globals available: api, triggerParams, executionParams.",
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
isError: true,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Step 1: Create the function
|
|
872
|
+
const fnInput: Record<string, any> = { name, code };
|
|
873
|
+
if (description !== undefined) fnInput.description = description;
|
|
874
|
+
|
|
875
|
+
const fnResult = await sdk.functions.create(fnInput as any);
|
|
876
|
+
const fn = fnResult.data;
|
|
877
|
+
const fnId = fn.id;
|
|
878
|
+
|
|
879
|
+
// If no trigger requested, return function only
|
|
880
|
+
if (!trigger) {
|
|
881
|
+
return {
|
|
882
|
+
content: [
|
|
883
|
+
{
|
|
884
|
+
type: "text",
|
|
885
|
+
text: [
|
|
886
|
+
`Function created:`,
|
|
887
|
+
` ID: ${fnId}`,
|
|
888
|
+
` Name: ${name}`,
|
|
889
|
+
``,
|
|
890
|
+
`No trigger requested. Use create_trigger to wire one later.`,
|
|
891
|
+
].join("\n"),
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Step 2: Build trigger input from the simplified config
|
|
898
|
+
const typeMap: Record<string, string> = {
|
|
899
|
+
on_demand: "on-demand",
|
|
900
|
+
record: "event-driven",
|
|
901
|
+
schedule: "scheduled",
|
|
902
|
+
endpoint: "endpoint",
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const executionType = typeMap[trigger.type];
|
|
906
|
+
const triggerName = trigger.name || `${name} trigger`;
|
|
907
|
+
|
|
908
|
+
const triggerInput: Record<string, any> = {
|
|
909
|
+
name: triggerName,
|
|
910
|
+
functionId: fnId,
|
|
911
|
+
executionType,
|
|
912
|
+
};
|
|
913
|
+
if (trigger.description !== undefined) triggerInput.description = trigger.description;
|
|
914
|
+
|
|
915
|
+
// Build triggerMetadata based on type
|
|
916
|
+
const meta: Record<string, any> = {};
|
|
917
|
+
if (trigger.params) meta.params = trigger.params;
|
|
918
|
+
|
|
919
|
+
if (trigger.type === "record") {
|
|
920
|
+
if (trigger.recordSlug) meta.recordSlug = trigger.recordSlug;
|
|
921
|
+
if (trigger.event) meta.eventType = trigger.event;
|
|
922
|
+
} else if (trigger.type === "schedule") {
|
|
923
|
+
meta.scheduleType = "cron";
|
|
924
|
+
if (trigger.cronExpression) meta.cronExpression = trigger.cronExpression;
|
|
925
|
+
} else if (trigger.type === "endpoint") {
|
|
926
|
+
if (trigger.endpointPath) meta.path = trigger.endpointPath;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (Object.keys(meta).length > 0) triggerInput.triggerMetadata = meta;
|
|
930
|
+
|
|
931
|
+
// Step 3: Create the trigger
|
|
932
|
+
let triggerData: any;
|
|
933
|
+
try {
|
|
934
|
+
const triggerResult = await sdk.triggers.create(triggerInput as any);
|
|
935
|
+
triggerData = triggerResult.data;
|
|
936
|
+
} catch (triggerError: unknown) {
|
|
937
|
+
// Function succeeded but trigger failed — report both
|
|
938
|
+
return {
|
|
939
|
+
content: [
|
|
940
|
+
{
|
|
941
|
+
type: "text",
|
|
942
|
+
text: [
|
|
943
|
+
`Function created:`,
|
|
944
|
+
` ID: ${fnId}`,
|
|
945
|
+
` Name: ${name}`,
|
|
946
|
+
``,
|
|
947
|
+
formatError(triggerError, "creating trigger"),
|
|
948
|
+
``,
|
|
949
|
+
`The function was created successfully but the trigger failed. You can retry with create_trigger using functionId '${fnId}'.`,
|
|
950
|
+
].join("\n"),
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Build a human-friendly trigger type label
|
|
957
|
+
let typeLabel = trigger.type as string;
|
|
958
|
+
if (trigger.type === "record" && trigger.event && trigger.recordSlug) {
|
|
959
|
+
typeLabel = `record (${trigger.event} on ${trigger.recordSlug})`;
|
|
960
|
+
} else if (trigger.type === "schedule" && trigger.cronExpression) {
|
|
961
|
+
typeLabel = `schedule (${trigger.cronExpression})`;
|
|
962
|
+
} else if (trigger.type === "endpoint" && trigger.endpointPath) {
|
|
963
|
+
typeLabel = `endpoint (/${trigger.endpointPath})`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const lines = [
|
|
967
|
+
`Function created:`,
|
|
968
|
+
` ID: ${fnId}`,
|
|
969
|
+
` Name: ${name}`,
|
|
970
|
+
``,
|
|
971
|
+
`Trigger created:`,
|
|
972
|
+
` ID: ${triggerData.id}`,
|
|
973
|
+
` Type: ${typeLabel}`,
|
|
974
|
+
``,
|
|
975
|
+
];
|
|
976
|
+
|
|
977
|
+
if (trigger.type === "on_demand") {
|
|
978
|
+
lines.push(`Your function is ready. Invoke it via the trigger or use invoke_trigger with ID ${triggerData.id}.`);
|
|
979
|
+
} else if (trigger.type === "endpoint") {
|
|
980
|
+
lines.push(`Your function is ready. Invoke it via invoke_endpoint with path '${trigger.endpointPath}' or use invoke_trigger with ID ${triggerData.id}.`);
|
|
981
|
+
} else {
|
|
982
|
+
lines.push(`Your function is ready. Invoke it via the trigger or use invoke_trigger with ID ${triggerData.id}.`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
content: [
|
|
987
|
+
{ type: "text", text: lines.join("\n") },
|
|
988
|
+
],
|
|
989
|
+
};
|
|
990
|
+
} catch (error: unknown) {
|
|
991
|
+
return {
|
|
992
|
+
content: [
|
|
993
|
+
{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: formatError(error, `scaffolding function '${name}'`),
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
isError: true,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
831
1003
|
}
|
package/src/tools/describe.ts
CHANGED
|
@@ -1083,7 +1083,7 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1083
1083
|
description:
|
|
1084
1084
|
"Orchestrations are multi-step workflows that chain compute functions together. Each step executes a function and passes its output to the next step.",
|
|
1085
1085
|
orchestration_shape: {
|
|
1086
|
-
id: "
|
|
1086
|
+
id: "Prefixed xid (e.g. orch_ctb5h4l6n7m8p9q0r1s2)",
|
|
1087
1087
|
name: "string",
|
|
1088
1088
|
description: "string | null",
|
|
1089
1089
|
status: "'draft' | 'active' | 'paused'",
|
|
@@ -1154,8 +1154,8 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1154
1154
|
},
|
|
1155
1155
|
},
|
|
1156
1156
|
run_shape: {
|
|
1157
|
-
id: "
|
|
1158
|
-
orchestrationId: "
|
|
1157
|
+
id: "Prefixed xid (e.g. orun_ctb5h4l6n7m8p9q0r1s2)",
|
|
1158
|
+
orchestrationId: "Prefixed xid (e.g. orch_ctb5h4l6n7m8p9q0r1s2)",
|
|
1159
1159
|
status: "'pending' | 'running' | 'waiting' | 'completed' | 'failed'",
|
|
1160
1160
|
input: "object — the input data provided when the run was triggered",
|
|
1161
1161
|
output: "object | null — final output after all steps complete",
|
|
@@ -1164,7 +1164,7 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1164
1164
|
correlationId: "string | null — optional tracing ID",
|
|
1165
1165
|
},
|
|
1166
1166
|
run_step_shape: {
|
|
1167
|
-
stepId: "
|
|
1167
|
+
stepId: "Prefixed xid (e.g. ostep_ctb5h4l6n7m8p9q0r1s2)",
|
|
1168
1168
|
status: "'pending' | 'running' | 'completed' | 'failed' | 'skipped'",
|
|
1169
1169
|
input: "object — input passed to this step",
|
|
1170
1170
|
output: "object | null — output returned by this step",
|
|
@@ -2152,7 +2152,7 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
2152
2152
|
"start-orchestration": {
|
|
2153
2153
|
description:
|
|
2154
2154
|
"Start an orchestration workflow run. Use for multi-step processes.",
|
|
2155
|
-
targetRef: "Orchestration ID (
|
|
2155
|
+
targetRef: "Orchestration ID (prefixed xid, e.g. orch_...)",
|
|
2156
2156
|
typical_activation: "button",
|
|
2157
2157
|
executionMode:
|
|
2158
2158
|
"'fire-and-poll' recommended — orchestrations are long-running",
|
package/src/tools/records.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
+
import axios, { AxiosInstance } from "axios";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
function formatError(error: unknown, context: string): string {
|
|
@@ -23,7 +24,64 @@ function formatError(error: unknown, context: string): string {
|
|
|
23
24
|
return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Ensures the SDK has a valid token.
|
|
29
|
+
*/
|
|
30
|
+
async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
|
|
31
|
+
let token = sdk.getToken();
|
|
32
|
+
if (token) return token;
|
|
33
|
+
try {
|
|
34
|
+
await sdk.queryRecords("__noop__", { limit: 1 });
|
|
35
|
+
} catch { /* token refresh side effect */ }
|
|
36
|
+
return sdk.getToken();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates an axios instance pointing at the data service records API for a given recordSlug.
|
|
41
|
+
*/
|
|
42
|
+
function createRecordsClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: string, recordSlug: string): AxiosInstance {
|
|
43
|
+
const url = new URL(centraliUrl);
|
|
44
|
+
const hostname = url.hostname.startsWith("api.")
|
|
45
|
+
? url.hostname
|
|
46
|
+
: `api.${url.hostname}`;
|
|
47
|
+
const baseURL = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/records/slug/${recordSlug}`;
|
|
48
|
+
|
|
49
|
+
const client = axios.create({ baseURL, proxy: false });
|
|
50
|
+
|
|
51
|
+
client.interceptors.request.use(async (config) => {
|
|
52
|
+
const token = await ensureToken(sdk);
|
|
53
|
+
if (token) {
|
|
54
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
55
|
+
}
|
|
56
|
+
return config;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
client.interceptors.response.use(
|
|
60
|
+
(response) => response,
|
|
61
|
+
async (error) => {
|
|
62
|
+
const originalRequest = error.config;
|
|
63
|
+
const isAuthError = error.response?.status === 401 || error.response?.status === 403;
|
|
64
|
+
|
|
65
|
+
if (isAuthError && !originalRequest._hasRetried) {
|
|
66
|
+
originalRequest._hasRetried = true;
|
|
67
|
+
try {
|
|
68
|
+
await sdk.queryRecords("__noop__", { limit: 1 });
|
|
69
|
+
} catch { /* token refresh side effect */ }
|
|
70
|
+
|
|
71
|
+
const token = sdk.getToken();
|
|
72
|
+
if (token) {
|
|
73
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
74
|
+
return client.request(originalRequest);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Promise.reject(error);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function registerRecordTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string) {
|
|
27
85
|
server.tool(
|
|
28
86
|
"query_records",
|
|
29
87
|
"Query records from a collection with optional filters, sorting, and pagination. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100).",
|
|
@@ -315,4 +373,185 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
315
373
|
}
|
|
316
374
|
}
|
|
317
375
|
);
|
|
376
|
+
|
|
377
|
+
// ── Bulk Operations (1 aggregate event per operation) ───────────────
|
|
378
|
+
|
|
379
|
+
server.tool(
|
|
380
|
+
"bulk_create_records",
|
|
381
|
+
`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
|
+
{
|
|
383
|
+
recordSlug: z.string().describe("The collection's record slug (e.g., 'orders')"),
|
|
384
|
+
records: z
|
|
385
|
+
.array(z.record(z.string(), z.any()))
|
|
386
|
+
.describe("Array of record data objects to create (max 1000). Each object has field names as keys."),
|
|
387
|
+
},
|
|
388
|
+
async ({ recordSlug, records }) => {
|
|
389
|
+
try {
|
|
390
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
391
|
+
const result = await client.post("/bulk", records);
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
394
|
+
};
|
|
395
|
+
} catch (error: unknown) {
|
|
396
|
+
return {
|
|
397
|
+
content: [{ type: "text", text: formatError(error, `bulk creating records in '${recordSlug}'`) }],
|
|
398
|
+
isError: true,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
server.tool(
|
|
405
|
+
"bulk_update_records",
|
|
406
|
+
`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
|
+
{
|
|
408
|
+
recordSlug: z.string().describe("The collection's record slug"),
|
|
409
|
+
ids: z.array(z.string()).describe("Array of record IDs (UUIDs) to update"),
|
|
410
|
+
data: z
|
|
411
|
+
.record(z.string(), z.any())
|
|
412
|
+
.describe("Object with fields to set on ALL specified records (same data applied to each)"),
|
|
413
|
+
},
|
|
414
|
+
async ({ recordSlug, ids, data }) => {
|
|
415
|
+
try {
|
|
416
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
417
|
+
const result = await client.patch("/bulk", { ids, data });
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
420
|
+
};
|
|
421
|
+
} catch (error: unknown) {
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: "text", text: formatError(error, `bulk updating records in '${recordSlug}'`) }],
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
server.tool(
|
|
431
|
+
"bulk_delete_records",
|
|
432
|
+
`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
|
+
{
|
|
434
|
+
recordSlug: z.string().describe("The collection's record slug"),
|
|
435
|
+
ids: z.array(z.string()).describe("Array of record IDs (UUIDs) to delete"),
|
|
436
|
+
hard: z
|
|
437
|
+
.boolean()
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("If true, permanently delete. Default: false (soft delete, can be restored)"),
|
|
440
|
+
},
|
|
441
|
+
async ({ recordSlug, ids, hard }) => {
|
|
442
|
+
try {
|
|
443
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
444
|
+
const body: Record<string, any> = { ids };
|
|
445
|
+
if (hard !== undefined) body.hard = hard;
|
|
446
|
+
const result = await client.delete("/bulk", { data: body });
|
|
447
|
+
const deleteType = hard ? "permanently deleted" : "soft-deleted";
|
|
448
|
+
return {
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `${ids.length} records ${deleteType} from '${recordSlug}'.\n\n${JSON.stringify(result.data, null, 2)}`,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
} catch (error: unknown) {
|
|
457
|
+
return {
|
|
458
|
+
content: [{ type: "text", text: formatError(error, `bulk deleting records from '${recordSlug}'`) }],
|
|
459
|
+
isError: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// ── Batch Operations (1 event per record) ──────────────────────────
|
|
466
|
+
|
|
467
|
+
server.tool(
|
|
468
|
+
"batch_create_records",
|
|
469
|
+
`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
|
+
{
|
|
471
|
+
recordSlug: z.string().describe("The collection's record slug (e.g., 'orders')"),
|
|
472
|
+
records: z
|
|
473
|
+
.array(z.record(z.string(), z.any()))
|
|
474
|
+
.describe("Array of record data objects to create. Each object has field names as keys."),
|
|
475
|
+
stopOnError: z
|
|
476
|
+
.boolean()
|
|
477
|
+
.optional()
|
|
478
|
+
.describe("If true, stop processing on the first error. Default: false (continue and report failures)"),
|
|
479
|
+
},
|
|
480
|
+
async ({ recordSlug, records, stopOnError }) => {
|
|
481
|
+
try {
|
|
482
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
483
|
+
const params: Record<string, any> = {};
|
|
484
|
+
if (stopOnError !== undefined) params.stopOnError = stopOnError;
|
|
485
|
+
const result = await client.post("/batch", records, { params });
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
488
|
+
};
|
|
489
|
+
} catch (error: unknown) {
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: "text", text: formatError(error, `batch creating records in '${recordSlug}'`) }],
|
|
492
|
+
isError: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
server.tool(
|
|
499
|
+
"batch_update_records",
|
|
500
|
+
`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
|
+
{
|
|
502
|
+
recordSlug: z.string().describe("The collection's record slug"),
|
|
503
|
+
updates: z
|
|
504
|
+
.array(
|
|
505
|
+
z.object({
|
|
506
|
+
id: z.string().describe("The record ID (UUID) to update"),
|
|
507
|
+
data: z
|
|
508
|
+
.record(z.string(), z.any())
|
|
509
|
+
.describe("Object with fields to update for this specific record"),
|
|
510
|
+
})
|
|
511
|
+
)
|
|
512
|
+
.describe("Array of update objects, each with an 'id' and 'data' to apply to that record"),
|
|
513
|
+
},
|
|
514
|
+
async ({ recordSlug, updates }) => {
|
|
515
|
+
try {
|
|
516
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
517
|
+
const result = await client.patch("/batch", updates);
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
520
|
+
};
|
|
521
|
+
} catch (error: unknown) {
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: "text", text: formatError(error, `batch updating records in '${recordSlug}'`) }],
|
|
524
|
+
isError: true,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
server.tool(
|
|
531
|
+
"batch_delete_records",
|
|
532
|
+
`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
|
+
{
|
|
534
|
+
recordSlug: z.string().describe("The collection's record slug"),
|
|
535
|
+
ids: z.array(z.string()).describe("Array of record IDs (UUIDs) to delete"),
|
|
536
|
+
},
|
|
537
|
+
async ({ recordSlug, ids }) => {
|
|
538
|
+
try {
|
|
539
|
+
const client = createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug);
|
|
540
|
+
const result = await client.delete("/batch", { data: { ids } });
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
text: `${ids.length} records batch-deleted from '${recordSlug}'.\n\n${JSON.stringify(result.data, null, 2)}`,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
} catch (error: unknown) {
|
|
550
|
+
return {
|
|
551
|
+
content: [{ type: "text", text: formatError(error, `batch deleting records from '${recordSlug}'`) }],
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
);
|
|
318
557
|
}
|
package/src/tools/structures.ts
CHANGED
|
@@ -540,4 +540,144 @@ Each fix is an object with: { fieldName, fixType, value?, expression?, applyToAl
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
);
|
|
543
|
+
|
|
544
|
+
server.tool(
|
|
545
|
+
"explore_collections",
|
|
546
|
+
"Get a compact overview of ALL collections in the workspace with their field names, types, and constraints. Returns everything in one call — no need to list collections and then get each one separately.",
|
|
547
|
+
{
|
|
548
|
+
includeRecordCounts: z.boolean().optional().describe("Include record counts for each collection (slower, requires an extra query per collection). Defaults to false."),
|
|
549
|
+
},
|
|
550
|
+
async ({ includeRecordCounts }) => {
|
|
551
|
+
try {
|
|
552
|
+
// Fetch all collections (paginate to get everything)
|
|
553
|
+
const firstPage = await sdk.collections.list({ page: 1, limit: 100 });
|
|
554
|
+
let allCollections = firstPage.data ?? [];
|
|
555
|
+
const meta = (firstPage as any).meta;
|
|
556
|
+
if (meta && meta.total > allCollections.length) {
|
|
557
|
+
const totalPages = Math.ceil(meta.total / 100);
|
|
558
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
559
|
+
const nextPage = await sdk.collections.list({ page, limit: 100 });
|
|
560
|
+
allCollections = allCollections.concat(nextPage.data ?? []);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (allCollections.length === 0) {
|
|
565
|
+
return {
|
|
566
|
+
content: [{ type: "text", text: "No collections found in this workspace." }],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Fetch full schema for each collection
|
|
571
|
+
const details = await Promise.all(
|
|
572
|
+
allCollections.map(async (col: any) => {
|
|
573
|
+
try {
|
|
574
|
+
const full = await sdk.collections.getBySlug(col.recordSlug);
|
|
575
|
+
const detail: any = { ...(full.data ?? col) };
|
|
576
|
+
|
|
577
|
+
if (includeRecordCounts) {
|
|
578
|
+
try {
|
|
579
|
+
const records = await sdk.queryRecords(col.recordSlug, { pageSize: 1 } as any);
|
|
580
|
+
detail._recordCount = (records as any).meta?.total ?? "unknown";
|
|
581
|
+
} catch {
|
|
582
|
+
detail._recordCount = "unknown";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return detail;
|
|
587
|
+
} catch {
|
|
588
|
+
return { ...col, _fetchError: true };
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Format as compact readable text
|
|
594
|
+
const lines: string[] = [];
|
|
595
|
+
for (const col of details) {
|
|
596
|
+
lines.push(`=== Collection: ${col.name} (slug: ${col.recordSlug}) ===`);
|
|
597
|
+
if (col.description) {
|
|
598
|
+
lines.push(`Description: ${col.description}`);
|
|
599
|
+
}
|
|
600
|
+
lines.push(`Schema mode: ${col.schemaDiscoveryMode ?? "strict"}`);
|
|
601
|
+
if (col.tags && col.tags.length > 0) {
|
|
602
|
+
lines.push(`Tags: ${col.tags.join(", ")}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (col._fetchError) {
|
|
606
|
+
lines.push("Fields: (unable to fetch full schema)");
|
|
607
|
+
} else if (col.properties && col.properties.length > 0) {
|
|
608
|
+
lines.push("Fields:");
|
|
609
|
+
for (const prop of col.properties) {
|
|
610
|
+
const parts: string[] = [];
|
|
611
|
+
|
|
612
|
+
// Base type
|
|
613
|
+
let typeStr = prop.type;
|
|
614
|
+
if (prop.type === "string" && prop.renderAs) {
|
|
615
|
+
typeStr = prop.renderAs;
|
|
616
|
+
}
|
|
617
|
+
if (prop.type === "array" && prop.items?.type) {
|
|
618
|
+
typeStr = `array of ${prop.items.type}`;
|
|
619
|
+
}
|
|
620
|
+
if (prop.type === "reference" && prop.target) {
|
|
621
|
+
typeStr = `reference -> ${prop.target}`;
|
|
622
|
+
if (prop.relationship) typeStr += ` (${prop.relationship})`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Enum / select values
|
|
626
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
627
|
+
const vals = prop.enum.map(String);
|
|
628
|
+
if (vals.length <= 8) {
|
|
629
|
+
typeStr = `select: ${vals.join("|")}`;
|
|
630
|
+
} else {
|
|
631
|
+
typeStr = `select: ${vals.slice(0, 6).join("|")}|... (${vals.length} values)`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
parts.push(typeStr);
|
|
636
|
+
|
|
637
|
+
// Constraints
|
|
638
|
+
if (prop.required) parts.push("required");
|
|
639
|
+
if (prop.isUnique) parts.push("unique");
|
|
640
|
+
if (prop.immutable) parts.push("immutable");
|
|
641
|
+
if (prop.isSecret) parts.push("secret");
|
|
642
|
+
if (prop.nullable) parts.push("nullable");
|
|
643
|
+
if (prop.default !== undefined) parts.push(`default: ${JSON.stringify(prop.default)}`);
|
|
644
|
+
if (prop.minimum !== undefined) parts.push(`min: ${prop.minimum}`);
|
|
645
|
+
if (prop.maximum !== undefined) parts.push(`max: ${prop.maximum}`);
|
|
646
|
+
if (prop.minLength !== undefined) parts.push(`minLen: ${prop.minLength}`);
|
|
647
|
+
if (prop.maxLength !== undefined) parts.push(`maxLen: ${prop.maxLength}`);
|
|
648
|
+
if (prop.pattern) parts.push(`pattern: ${prop.pattern}`);
|
|
649
|
+
if (prop.expression) parts.push("computed");
|
|
650
|
+
if (prop.autoIncrement) parts.push("auto-increment");
|
|
651
|
+
|
|
652
|
+
lines.push(` - ${prop.name} (${parts.join(", ")})`);
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
lines.push("Fields: (none)");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (includeRecordCounts && col._recordCount !== undefined) {
|
|
659
|
+
lines.push(`Records: ${col._recordCount}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
lines.push("");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
lines.push(`Total: ${details.length} collection(s)`);
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
669
|
+
};
|
|
670
|
+
} catch (error: unknown) {
|
|
671
|
+
return {
|
|
672
|
+
content: [
|
|
673
|
+
{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: formatError(error, "exploring collections"),
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
isError: true,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
);
|
|
543
683
|
}
|