@cmssy/mcp-server 0.10.0 → 0.16.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 +68 -2
- package/dist/ai-tools-binder.d.ts +4 -0
- package/dist/ai-tools-binder.d.ts.map +1 -0
- package/dist/ai-tools-binder.js +42 -0
- package/dist/ai-tools-binder.js.map +1 -0
- package/dist/ai-tools-ops.d.ts +4 -0
- package/dist/ai-tools-ops.d.ts.map +1 -0
- package/dist/ai-tools-ops.js +204 -0
- package/dist/ai-tools-ops.js.map +1 -0
- package/dist/graphql-client.d.ts +1 -1
- package/dist/graphql-client.d.ts.map +1 -1
- package/dist/graphql-client.js.map +1 -1
- package/dist/queries.d.ts +29 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +371 -0
- package/dist/queries.js.map +1 -1
- package/dist/responses.d.ts +32 -11
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +30 -12
- package/dist/responses.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +603 -418
- package/dist/server.js.map +1 -1
- package/package.json +4 -3
package/dist/server.js
CHANGED
|
@@ -3,7 +3,9 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import {
|
|
6
|
+
import { createRecordTool, createDiscountTool, createPageTool, createFormTool, createModelTool, updateModelTool, updateRecordTool, } from "@cmssy/ai-tools";
|
|
7
|
+
import { createMcpWorkspaceOps } from "./ai-tools-ops.js";
|
|
8
|
+
import { bindSharedTool } from "./ai-tools-binder.js";
|
|
7
9
|
// Read our own version from package.json so the MCP handshake
|
|
8
10
|
// advertises what the user actually installed, instead of drifting
|
|
9
11
|
// whenever we bump the package.
|
|
@@ -34,13 +36,14 @@ const jsonPreprocess = (val) => {
|
|
|
34
36
|
return val;
|
|
35
37
|
}
|
|
36
38
|
};
|
|
37
|
-
import { PAGES_QUERY, PAGE_BY_ID_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, MEDIA_ASSETS_QUERY, SAVE_PAGE_MUTATION, PATCH_BLOCK_CONTENT_MUTATION, UPDATE_PAGE_SETTINGS_MUTATION, PAGE_TYPES_QUERY, CREATE_PAGE_TYPE_MUTATION, TOGGLE_PUBLISH_MUTATION, PUBLISH_PAGE_CONTENT_MUTATION, PUBLISH_PAGE_LAYOUT_MUTATION, REVERT_CONTENT_TO_PUBLISHED_MUTATION, REVERT_LAYOUT_TO_PUBLISHED_MUTATION, REMOVE_PAGE_MUTATION, UPDATE_PAGE_LAYOUT_MUTATION, FORMS_QUERY, FORM_BY_ID_QUERY, FORM_SUBMISSIONS_QUERY, FORM_SUBMISSION_BY_ID_QUERY,
|
|
38
|
-
import { responseModeSchema, pageMinimal, pageBlockMinimal, formMinimal,
|
|
39
|
+
import { PAGES_QUERY, PAGE_BY_ID_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, MEDIA_ASSETS_QUERY, SAVE_PAGE_MUTATION, PATCH_BLOCK_CONTENT_MUTATION, UPDATE_PAGE_SETTINGS_MUTATION, PAGE_TYPES_QUERY, CREATE_PAGE_TYPE_MUTATION, TOGGLE_PUBLISH_MUTATION, PUBLISH_PAGE_CONTENT_MUTATION, PUBLISH_PAGE_LAYOUT_MUTATION, REVERT_CONTENT_TO_PUBLISHED_MUTATION, REVERT_LAYOUT_TO_PUBLISHED_MUTATION, REMOVE_PAGE_MUTATION, UPDATE_PAGE_LAYOUT_MUTATION, FORMS_QUERY, FORM_BY_ID_QUERY, FORM_SUBMISSIONS_QUERY, FORM_SUBMISSION_BY_ID_QUERY, UPDATE_FORM_MUTATION, DELETE_FORM_MUTATION, UPDATE_FORM_SUBMISSION_STATUS_MUTATION, DELETE_FORM_SUBMISSION_MUTATION, MODEL_DEFINITIONS_QUERY, MODEL_DEFINITIONS_BY_SLUG_INDEX_QUERY, MODEL_DEFINITION_BY_ID_QUERY, MODEL_RECORDS_QUERY, MODEL_RECORD_BY_ID_QUERY, DELETE_MODEL_DEFINITION_MUTATION, DELETE_MODEL_RECORD_MUTATION, IMPORT_MODEL_RECORDS_MUTATION, ORDERS_QUERY, ORDER_BY_ID_QUERY, ORDER_PIPELINE_QUERY, CREATE_MANUAL_ORDER_MUTATION, EDIT_ORDER_MUTATION, UPDATE_ORDER_DETAILS_MUTATION, MARK_ORDER_PAID_MUTATION, RECORD_ORDER_PAYMENT_MUTATION, REFUND_ORDER_MUTATION, CANCEL_ORDER_MUTATION, TRANSITION_ORDER_FULFILLMENT_MUTATION, SET_ORDER_PIPELINE_STAGE_MUTATION, RECORD_ORDER_INVOICE_MUTATION, ADMIN_CARTS_QUERY, DISCOUNTS_QUERY, DISCOUNT_BY_ID_QUERY, UPDATE_DISCOUNT_MUTATION, SET_DISCOUNT_ENABLED_MUTATION, WEBHOOK_ENDPOINTS_QUERY, WEBHOOK_DELIVERIES_QUERY, WEBHOOK_EVENT_TYPES_QUERY, CREATE_WEBHOOK_ENDPOINT_MUTATION, UPDATE_WEBHOOK_ENDPOINT_MUTATION, ROTATE_WEBHOOK_SECRET_MUTATION, DELETE_WEBHOOK_ENDPOINT_MUTATION, PRODUCT_CATALOG_QUERY, BULK_UPDATE_PRODUCT_RECORDS_MUTATION, BULK_DELETE_PRODUCT_RECORDS_MUTATION, } from "./queries.js";
|
|
40
|
+
import { responseModeSchema, pageMinimal, pageBlockMinimal, formMinimal, orderMinimal, discountMinimal, jsonText, } from "./responses.js";
|
|
39
41
|
export function createServer(client) {
|
|
40
42
|
const server = new McpServer({
|
|
41
43
|
name: "cmssy",
|
|
42
44
|
version: PACKAGE_VERSION,
|
|
43
45
|
});
|
|
46
|
+
const sharedOps = createMcpWorkspaceOps(client);
|
|
44
47
|
/** Check if value is null, undefined, or empty object */
|
|
45
48
|
const isEmpty = (obj) => obj == null ||
|
|
46
49
|
(typeof obj === "object" && Object.keys(obj).length === 0);
|
|
@@ -165,54 +168,7 @@ export function createServer(client) {
|
|
|
165
168
|
};
|
|
166
169
|
});
|
|
167
170
|
// ─── Write Tools ─────────────────────────────────────────────
|
|
168
|
-
server
|
|
169
|
-
name: z.string().describe("Internal page name"),
|
|
170
|
-
slug: z.string().describe("URL slug (e.g. 'about', 'features')"),
|
|
171
|
-
parentId: z
|
|
172
|
-
.string()
|
|
173
|
-
.optional()
|
|
174
|
-
.describe("Parent page ID for nested pages"),
|
|
175
|
-
pageType: z
|
|
176
|
-
.string()
|
|
177
|
-
.optional()
|
|
178
|
-
.default("page")
|
|
179
|
-
.describe("Page type (default: 'page')"),
|
|
180
|
-
displayName: z
|
|
181
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
182
|
-
.optional()
|
|
183
|
-
.describe("Multilingual display name, e.g. { en: 'About', pl: 'O nas' }"),
|
|
184
|
-
seoTitle: z
|
|
185
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
186
|
-
.optional()
|
|
187
|
-
.describe("Multilingual SEO title"),
|
|
188
|
-
seoDescription: z
|
|
189
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
190
|
-
.optional()
|
|
191
|
-
.describe("Multilingual SEO description"),
|
|
192
|
-
customFields: z
|
|
193
|
-
.preprocess(jsonPreprocess, z.array(z.object({ fieldKey: z.string(), value: z.unknown() })))
|
|
194
|
-
.optional()
|
|
195
|
-
.describe("Custom field values for the page type's schema: [{ fieldKey, value }]. value is JSON."),
|
|
196
|
-
response: responseModeSchema,
|
|
197
|
-
}, async ({ name, slug, parentId, pageType, displayName, seoTitle, seoDescription, customFields, response, }) => {
|
|
198
|
-
const input = { name, slug };
|
|
199
|
-
if (parentId)
|
|
200
|
-
input.parentId = parentId;
|
|
201
|
-
if (pageType)
|
|
202
|
-
input.pageType = pageType;
|
|
203
|
-
if (displayName)
|
|
204
|
-
input.displayName = displayName;
|
|
205
|
-
if (seoTitle)
|
|
206
|
-
input.seoTitle = seoTitle;
|
|
207
|
-
if (seoDescription)
|
|
208
|
-
input.seoDescription = seoDescription;
|
|
209
|
-
if (customFields !== undefined)
|
|
210
|
-
input.customFields = customFields;
|
|
211
|
-
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
212
|
-
input,
|
|
213
|
-
});
|
|
214
|
-
return jsonText(response, data.savePage, pageMinimal);
|
|
215
|
-
});
|
|
171
|
+
bindSharedTool(server, createPageTool, sharedOps);
|
|
216
172
|
server.tool("update_page_blocks", "Set the full content blocks array on a page. Replaces all existing content blocks. Block types are validated server-side on save. Blocks with matching IDs preserve their existing content/settings when not explicitly provided. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
217
173
|
pageId: z.string().describe("Page ID"),
|
|
218
174
|
blocks: z.preprocess(jsonPreprocess, z
|
|
@@ -341,7 +297,9 @@ export function createServer(client) {
|
|
|
341
297
|
allowChildren: z.boolean().optional(),
|
|
342
298
|
fields: z
|
|
343
299
|
.preprocess(jsonPreprocess, z.array(z.object({
|
|
344
|
-
key: z
|
|
300
|
+
key: z
|
|
301
|
+
.string()
|
|
302
|
+
.describe("Field key (identifier in customFields)"),
|
|
345
303
|
label: z.string().describe("Human-readable label"),
|
|
346
304
|
type: z
|
|
347
305
|
.enum([
|
|
@@ -1047,101 +1005,7 @@ export function createServer(client) {
|
|
|
1047
1005
|
],
|
|
1048
1006
|
};
|
|
1049
1007
|
});
|
|
1050
|
-
server
|
|
1051
|
-
name: z.string().describe("Form name"),
|
|
1052
|
-
slug: z.string().describe("URL-friendly slug (must be unique)"),
|
|
1053
|
-
description: z.string().optional().describe("Form description"),
|
|
1054
|
-
fields: z
|
|
1055
|
-
.preprocess(jsonPreprocess, z.array(z.object({
|
|
1056
|
-
id: z.string().describe("Unique field ID"),
|
|
1057
|
-
name: z.string().describe("Field name (used as form data key)"),
|
|
1058
|
-
fieldType: z
|
|
1059
|
-
.enum([
|
|
1060
|
-
"text",
|
|
1061
|
-
"email",
|
|
1062
|
-
"password",
|
|
1063
|
-
"textarea",
|
|
1064
|
-
"number",
|
|
1065
|
-
"phone",
|
|
1066
|
-
"url",
|
|
1067
|
-
"date",
|
|
1068
|
-
"datetime",
|
|
1069
|
-
"select",
|
|
1070
|
-
"multiselect",
|
|
1071
|
-
"checkbox",
|
|
1072
|
-
"radio",
|
|
1073
|
-
"file",
|
|
1074
|
-
"hidden",
|
|
1075
|
-
])
|
|
1076
|
-
.optional()
|
|
1077
|
-
.default("text"),
|
|
1078
|
-
label: z
|
|
1079
|
-
.record(z.string(), z.unknown())
|
|
1080
|
-
.optional()
|
|
1081
|
-
.describe("i18n labels: { en: 'Name', pl: 'Imię' }"),
|
|
1082
|
-
placeholder: z.record(z.string(), z.unknown()).optional(),
|
|
1083
|
-
helpText: z.record(z.string(), z.unknown()).optional(),
|
|
1084
|
-
defaultValue: z.string().optional(),
|
|
1085
|
-
validation: z
|
|
1086
|
-
.object({
|
|
1087
|
-
required: z.boolean().optional(),
|
|
1088
|
-
minLength: z.number().optional(),
|
|
1089
|
-
maxLength: z.number().optional(),
|
|
1090
|
-
minValue: z.number().optional(),
|
|
1091
|
-
maxValue: z.number().optional(),
|
|
1092
|
-
pattern: z.string().optional(),
|
|
1093
|
-
customMessage: z.string().optional(),
|
|
1094
|
-
})
|
|
1095
|
-
.optional(),
|
|
1096
|
-
options: z
|
|
1097
|
-
.array(z.object({
|
|
1098
|
-
value: z.string(),
|
|
1099
|
-
label: z.record(z.string(), z.unknown()).optional(),
|
|
1100
|
-
disabled: z.boolean().optional(),
|
|
1101
|
-
}))
|
|
1102
|
-
.optional(),
|
|
1103
|
-
width: z
|
|
1104
|
-
.enum(["full", "half", "third"])
|
|
1105
|
-
.optional()
|
|
1106
|
-
.default("full"),
|
|
1107
|
-
order: z.number().optional().default(0),
|
|
1108
|
-
showIf: z.record(z.string(), z.unknown()).optional(),
|
|
1109
|
-
})))
|
|
1110
|
-
.optional()
|
|
1111
|
-
.describe("Form field definitions"),
|
|
1112
|
-
settings: z
|
|
1113
|
-
.preprocess(jsonPreprocess, z.object({
|
|
1114
|
-
actionType: z
|
|
1115
|
-
.enum(["login", "register", "newsletter", "contact", "custom"])
|
|
1116
|
-
.optional()
|
|
1117
|
-
.default("contact"),
|
|
1118
|
-
webhookUrl: z.string().optional(),
|
|
1119
|
-
emailRecipients: z.array(z.string()).optional(),
|
|
1120
|
-
newsletterListId: z.string().optional(),
|
|
1121
|
-
submitButtonLabel: z.record(z.string(), z.unknown()).optional(),
|
|
1122
|
-
successMessage: z.record(z.string(), z.unknown()).optional(),
|
|
1123
|
-
errorMessage: z.record(z.string(), z.unknown()).optional(),
|
|
1124
|
-
redirectUrl: z.string().optional(),
|
|
1125
|
-
enableCaptcha: z.boolean().optional(),
|
|
1126
|
-
requireLogin: z.boolean().optional(),
|
|
1127
|
-
saveSubmissions: z.boolean().optional(),
|
|
1128
|
-
sendEmailNotification: z.boolean().optional(),
|
|
1129
|
-
emailConfigurationId: z.string().optional(),
|
|
1130
|
-
}))
|
|
1131
|
-
.optional()
|
|
1132
|
-
.describe("Form settings (action type, notifications, etc.)"),
|
|
1133
|
-
response: responseModeSchema,
|
|
1134
|
-
}, async ({ name, slug, description, fields, settings, response }) => {
|
|
1135
|
-
const input = { name, slug };
|
|
1136
|
-
if (description)
|
|
1137
|
-
input.description = description;
|
|
1138
|
-
if (fields)
|
|
1139
|
-
input.fields = fields;
|
|
1140
|
-
if (settings)
|
|
1141
|
-
input.settings = settings;
|
|
1142
|
-
const data = await client.query(CREATE_FORM_MUTATION, { input });
|
|
1143
|
-
return jsonText(response, data.createForm, formMinimal);
|
|
1144
|
-
});
|
|
1008
|
+
bindSharedTool(server, createFormTool, sharedOps);
|
|
1145
1009
|
server.tool("update_form", "Update an existing form's name, slug, status, fields, or settings. Returns a minimal ack by default; pass response='full' for the full form.", {
|
|
1146
1010
|
formId: z.string().describe("Form ID to update"),
|
|
1147
1011
|
name: z.string().optional().describe("New form name"),
|
|
@@ -1340,91 +1204,7 @@ export function createServer(client) {
|
|
|
1340
1204
|
};
|
|
1341
1205
|
});
|
|
1342
1206
|
// ─── Model Tools (Custom Data Models) ────────────────────────
|
|
1343
|
-
const propertyFieldTypes = [
|
|
1344
|
-
"string",
|
|
1345
|
-
"richtext",
|
|
1346
|
-
"number",
|
|
1347
|
-
"boolean",
|
|
1348
|
-
"date",
|
|
1349
|
-
"datetime",
|
|
1350
|
-
"email",
|
|
1351
|
-
"url",
|
|
1352
|
-
"media",
|
|
1353
|
-
"relation",
|
|
1354
|
-
"select",
|
|
1355
|
-
"multiselect",
|
|
1356
|
-
"object",
|
|
1357
|
-
"list",
|
|
1358
|
-
];
|
|
1359
|
-
const relationTypes = ["hasOne", "hasMany", "manyToMany"];
|
|
1360
|
-
const propertyFieldSchema = z.lazy(() => z.object({
|
|
1361
|
-
key: z.string().describe("Field key (identifier in record data)"),
|
|
1362
|
-
label: z.string().describe("Human-readable field label"),
|
|
1363
|
-
type: z
|
|
1364
|
-
.enum(propertyFieldTypes)
|
|
1365
|
-
.describe("Field type — controls validation + UI"),
|
|
1366
|
-
required: z.boolean().optional(),
|
|
1367
|
-
description: z.string().optional(),
|
|
1368
|
-
defaultValue: z.string().optional(),
|
|
1369
|
-
options: z
|
|
1370
|
-
.array(z.string())
|
|
1371
|
-
.optional()
|
|
1372
|
-
.describe("For select/multiselect: allowed values"),
|
|
1373
|
-
fields: z
|
|
1374
|
-
.array(propertyFieldSchema)
|
|
1375
|
-
.optional()
|
|
1376
|
-
.describe("For type=object: nested fields"),
|
|
1377
|
-
itemType: z
|
|
1378
|
-
.enum(propertyFieldTypes)
|
|
1379
|
-
.optional()
|
|
1380
|
-
.describe("For type=list: item type"),
|
|
1381
|
-
itemFields: z
|
|
1382
|
-
.array(propertyFieldSchema)
|
|
1383
|
-
.optional()
|
|
1384
|
-
.describe("For type=list of objects: nested field defs"),
|
|
1385
|
-
relationTo: z
|
|
1386
|
-
.string()
|
|
1387
|
-
.optional()
|
|
1388
|
-
.describe("For type=relation: target model slug"),
|
|
1389
|
-
relationType: z.enum(relationTypes).optional(),
|
|
1390
|
-
acceptedTypes: z
|
|
1391
|
-
.array(z.enum(mediaTypeValues))
|
|
1392
|
-
.optional()
|
|
1393
|
-
.describe("For type=media: allowed media kinds (empty = all)"),
|
|
1394
|
-
multiple: z
|
|
1395
|
-
.boolean()
|
|
1396
|
-
.optional()
|
|
1397
|
-
.describe("For type=media: true = gallery, false = single"),
|
|
1398
|
-
schemaProperty: z.string().optional(),
|
|
1399
|
-
minLength: z.number().int().optional(),
|
|
1400
|
-
maxLength: z.number().int().optional(),
|
|
1401
|
-
minValue: z.number().optional(),
|
|
1402
|
-
maxValue: z.number().optional(),
|
|
1403
|
-
pattern: z.string().optional(),
|
|
1404
|
-
}));
|
|
1405
|
-
const statusFieldSchema = z.object({
|
|
1406
|
-
enabled: z.boolean().optional(),
|
|
1407
|
-
values: z
|
|
1408
|
-
.array(z.string())
|
|
1409
|
-
.optional()
|
|
1410
|
-
.describe("Allowed status values (e.g. ['draft','active'])"),
|
|
1411
|
-
defaultValue: z.string().optional(),
|
|
1412
|
-
transitions: z
|
|
1413
|
-
.array(z.object({
|
|
1414
|
-
from: z.string(),
|
|
1415
|
-
to: z.array(z.string()),
|
|
1416
|
-
}))
|
|
1417
|
-
.optional()
|
|
1418
|
-
.describe("Allowed transitions: from one state to a set of states"),
|
|
1419
|
-
});
|
|
1420
|
-
const defaultSortSchema = z.object({
|
|
1421
|
-
field: z.string(),
|
|
1422
|
-
direction: z.enum(["asc", "desc"]),
|
|
1423
|
-
});
|
|
1424
1207
|
const OBJECT_ID_RE = /^[a-f0-9]{24}$/i;
|
|
1425
|
-
// Match backend: apps/backend/src/models/model-definition.ts
|
|
1426
|
-
const SLUG_RE = /^[a-z][a-z0-9-]*$/;
|
|
1427
|
-
const SLUG_MSG = "Slug must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens";
|
|
1428
1208
|
server.tool("list_models", "List all Custom Data Models (ModelDefinition) in the workspace.", {}, async () => {
|
|
1429
1209
|
const data = await client.query(MODEL_DEFINITIONS_QUERY);
|
|
1430
1210
|
return {
|
|
@@ -1483,91 +1263,8 @@ export function createServer(client) {
|
|
|
1483
1263
|
],
|
|
1484
1264
|
};
|
|
1485
1265
|
});
|
|
1486
|
-
server
|
|
1487
|
-
|
|
1488
|
-
slug: z
|
|
1489
|
-
.string()
|
|
1490
|
-
.trim()
|
|
1491
|
-
.regex(SLUG_RE, SLUG_MSG)
|
|
1492
|
-
.describe("URL-safe slug, lowercase (regex: ^[a-z][a-z0-9-]*$)"),
|
|
1493
|
-
description: z.string().optional(),
|
|
1494
|
-
icon: z
|
|
1495
|
-
.string()
|
|
1496
|
-
.optional()
|
|
1497
|
-
.describe("Lucide icon name, defaults to 'database'"),
|
|
1498
|
-
color: z.string().optional(),
|
|
1499
|
-
displayField: z
|
|
1500
|
-
.string()
|
|
1501
|
-
.optional()
|
|
1502
|
-
.describe("Field key used as the record's display label in UI"),
|
|
1503
|
-
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1504
|
-
fields: z
|
|
1505
|
-
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1506
|
-
.optional()
|
|
1507
|
-
.describe("Field definitions. Validates record data on write."),
|
|
1508
|
-
statusField: z
|
|
1509
|
-
.preprocess(jsonPreprocess, statusFieldSchema)
|
|
1510
|
-
.optional()
|
|
1511
|
-
.describe("Enable record lifecycle states with allowed transitions"),
|
|
1512
|
-
response: responseModeSchema,
|
|
1513
|
-
}, async ({ name, slug, description, icon, color, displayField, defaultSort, fields, statusField, response, }) => {
|
|
1514
|
-
const input = { name, slug };
|
|
1515
|
-
if (description !== undefined)
|
|
1516
|
-
input.description = description;
|
|
1517
|
-
if (icon !== undefined)
|
|
1518
|
-
input.icon = icon;
|
|
1519
|
-
if (color !== undefined)
|
|
1520
|
-
input.color = color;
|
|
1521
|
-
if (displayField !== undefined)
|
|
1522
|
-
input.displayField = displayField;
|
|
1523
|
-
if (defaultSort !== undefined)
|
|
1524
|
-
input.defaultSort = defaultSort;
|
|
1525
|
-
if (fields !== undefined)
|
|
1526
|
-
input.fields = fields;
|
|
1527
|
-
if (statusField !== undefined)
|
|
1528
|
-
input.statusField = statusField;
|
|
1529
|
-
const data = await client.query(CREATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1530
|
-
return jsonText(response, data.createModelDefinition, modelMinimal);
|
|
1531
|
-
});
|
|
1532
|
-
server.tool("update_model", "Update any field of an existing ModelDefinition. Changing 'fields' migrates the schema - existing records are re-validated on next write. Returns a minimal ack by default; pass response='full' for the full model.", {
|
|
1533
|
-
id: z.string().describe("Model id (ObjectId) to update"),
|
|
1534
|
-
name: z.string().optional(),
|
|
1535
|
-
slug: z.string().trim().regex(SLUG_RE, SLUG_MSG).optional(),
|
|
1536
|
-
description: z.string().optional(),
|
|
1537
|
-
icon: z.string().optional(),
|
|
1538
|
-
color: z.string().optional(),
|
|
1539
|
-
displayField: z.string().optional(),
|
|
1540
|
-
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1541
|
-
fields: z
|
|
1542
|
-
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1543
|
-
.optional(),
|
|
1544
|
-
statusField: z.preprocess(jsonPreprocess, statusFieldSchema).optional(),
|
|
1545
|
-
response: responseModeSchema,
|
|
1546
|
-
}, async (args) => {
|
|
1547
|
-
const input = { id: args.id };
|
|
1548
|
-
for (const key of [
|
|
1549
|
-
"name",
|
|
1550
|
-
"slug",
|
|
1551
|
-
"description",
|
|
1552
|
-
"icon",
|
|
1553
|
-
"color",
|
|
1554
|
-
"displayField",
|
|
1555
|
-
"defaultSort",
|
|
1556
|
-
"fields",
|
|
1557
|
-
"statusField",
|
|
1558
|
-
]) {
|
|
1559
|
-
if (args[key] !== undefined)
|
|
1560
|
-
input[key] = args[key];
|
|
1561
|
-
}
|
|
1562
|
-
const data = await client.query(UPDATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1563
|
-
if (!data.updateModelDefinition) {
|
|
1564
|
-
return {
|
|
1565
|
-
content: [{ type: "text", text: "Model not found" }],
|
|
1566
|
-
isError: true,
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
return jsonText(args.response, data.updateModelDefinition, modelMinimal);
|
|
1570
|
-
});
|
|
1266
|
+
bindSharedTool(server, createModelTool, sharedOps);
|
|
1267
|
+
bindSharedTool(server, updateModelTool, sharedOps);
|
|
1571
1268
|
server.tool("delete_model", "Delete a ModelDefinition. WARNING: cascades - ALL records of this model are deleted.", {
|
|
1572
1269
|
id: z.string().describe("Model id (ObjectId) to delete"),
|
|
1573
1270
|
}, async ({ id }) => {
|
|
@@ -1637,142 +1334,630 @@ export function createServer(client) {
|
|
|
1637
1334
|
],
|
|
1638
1335
|
};
|
|
1639
1336
|
});
|
|
1640
|
-
server
|
|
1337
|
+
bindSharedTool(server, createRecordTool, sharedOps);
|
|
1338
|
+
bindSharedTool(server, updateRecordTool, sharedOps);
|
|
1339
|
+
server.tool("delete_record", "Delete a record permanently.", {
|
|
1340
|
+
id: z.string().describe("Record id (ObjectId) to delete"),
|
|
1341
|
+
}, async ({ id }) => {
|
|
1342
|
+
const data = await client.query(DELETE_MODEL_RECORD_MUTATION, { id });
|
|
1343
|
+
return {
|
|
1344
|
+
content: [
|
|
1345
|
+
{
|
|
1346
|
+
type: "text",
|
|
1347
|
+
text: data.deleteModelRecord
|
|
1348
|
+
? "Record deleted"
|
|
1349
|
+
: "Failed to delete record",
|
|
1350
|
+
},
|
|
1351
|
+
],
|
|
1352
|
+
isError: !data.deleteModelRecord,
|
|
1353
|
+
};
|
|
1354
|
+
});
|
|
1355
|
+
server.tool("import_records", "Bulk import records into a model. Accepts up to 1000 rows as plain objects. Returns { importedCount, errors[{row, message}] } - same shape as CSV/XLSX import.", {
|
|
1641
1356
|
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1642
|
-
|
|
1643
|
-
.preprocess(jsonPreprocess, z
|
|
1644
|
-
.
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
});
|
|
1650
|
-
return
|
|
1357
|
+
rows: z
|
|
1358
|
+
.preprocess(jsonPreprocess, z
|
|
1359
|
+
.array(z.record(z.string(), z.unknown()))
|
|
1360
|
+
.min(1, "Provide at least 1 row")
|
|
1361
|
+
.max(1000, "Maximum 1000 rows per import"))
|
|
1362
|
+
.describe("Array of plain objects keyed by model field keys"),
|
|
1363
|
+
}, async ({ modelId, rows }) => {
|
|
1364
|
+
const data = await client.query(IMPORT_MODEL_RECORDS_MUTATION, { input: { modelId, rows } });
|
|
1365
|
+
return {
|
|
1366
|
+
content: [
|
|
1367
|
+
{
|
|
1368
|
+
type: "text",
|
|
1369
|
+
text: JSON.stringify(data.importModelRecords, null, 2),
|
|
1370
|
+
},
|
|
1371
|
+
],
|
|
1372
|
+
};
|
|
1651
1373
|
});
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1374
|
+
// ─── Commerce: Orders ────────────────────────────────────────
|
|
1375
|
+
//
|
|
1376
|
+
// workspaceId is required by every commerce resolver and cross-checked
|
|
1377
|
+
// against the token's workspace context, so it always comes from the
|
|
1378
|
+
// client (the same value sent in the x-workspace-id header) - it is not
|
|
1379
|
+
// a caller-supplied arg. All money fields are integer minor units (cents).
|
|
1380
|
+
const PAYMENT_STATUS = [
|
|
1381
|
+
"unpaid",
|
|
1382
|
+
"partially_paid",
|
|
1383
|
+
"authorized",
|
|
1384
|
+
"paid",
|
|
1385
|
+
"partially_refunded",
|
|
1386
|
+
"refunded",
|
|
1387
|
+
];
|
|
1388
|
+
const FULFILLMENT_STATUS = [
|
|
1389
|
+
"unfulfilled",
|
|
1390
|
+
"partially_fulfilled",
|
|
1391
|
+
"fulfilled",
|
|
1392
|
+
"returned",
|
|
1393
|
+
];
|
|
1394
|
+
const CART_STATUS = [
|
|
1395
|
+
"active",
|
|
1396
|
+
"saved",
|
|
1397
|
+
"quote_requested",
|
|
1398
|
+
"merged",
|
|
1399
|
+
"expired",
|
|
1400
|
+
"abandoned",
|
|
1401
|
+
"ordered",
|
|
1402
|
+
];
|
|
1403
|
+
const DISCOUNT_TYPE = ["percentage", "fixed", "free_shipping"];
|
|
1404
|
+
const orderItemSchema = z
|
|
1405
|
+
.object({
|
|
1406
|
+
recordId: z
|
|
1407
|
+
.string()
|
|
1408
|
+
.optional()
|
|
1409
|
+
.describe("Product record id (ObjectId) the line refers to"),
|
|
1410
|
+
quantity: z.number().int().positive(),
|
|
1411
|
+
variantSelections: z
|
|
1655
1412
|
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1656
1413
|
.optional()
|
|
1657
|
-
.describe("
|
|
1658
|
-
|
|
1414
|
+
.describe("Selected variant options, e.g. {size: 'M', color: 'red'}"),
|
|
1415
|
+
name: z.string().optional().describe("Custom line name (ad-hoc item)"),
|
|
1416
|
+
price: z
|
|
1417
|
+
.number()
|
|
1418
|
+
.int()
|
|
1419
|
+
.optional()
|
|
1420
|
+
.describe("Custom unit price in minor units (ad-hoc item)"),
|
|
1421
|
+
})
|
|
1422
|
+
.refine((item) => item.recordId != null || (item.name != null && item.price != null), {
|
|
1423
|
+
message: "Each item needs a recordId, or both name and price for an ad-hoc line",
|
|
1424
|
+
});
|
|
1425
|
+
server.tool("list_orders", "List orders with optional filters and pagination. Money fields are integer minor units (cents). Pass pipelineStageId='__unassigned__' to list orders with no pipeline stage.", {
|
|
1426
|
+
paymentStatus: z.enum(PAYMENT_STATUS).optional(),
|
|
1427
|
+
fulfillmentStatus: z.enum(FULFILLMENT_STATUS).optional(),
|
|
1428
|
+
customerId: z.string().optional(),
|
|
1429
|
+
search: z.string().optional(),
|
|
1430
|
+
pipelineStageId: z.string().optional(),
|
|
1431
|
+
dateFrom: z
|
|
1659
1432
|
.string()
|
|
1660
1433
|
.optional()
|
|
1661
|
-
.describe("
|
|
1434
|
+
.describe("ISO date-time lower bound (inclusive)"),
|
|
1435
|
+
dateTo: z
|
|
1436
|
+
.string()
|
|
1437
|
+
.optional()
|
|
1438
|
+
.describe("ISO date-time upper bound (inclusive)"),
|
|
1439
|
+
skip: z.number().int().optional().default(0),
|
|
1440
|
+
limit: z.number().int().optional().default(20),
|
|
1441
|
+
}, async (args) => {
|
|
1442
|
+
const data = await client.query(ORDERS_QUERY, { workspaceId: client.workspaceId, ...args });
|
|
1443
|
+
return {
|
|
1444
|
+
content: [
|
|
1445
|
+
{ type: "text", text: JSON.stringify(data.orders, null, 2) },
|
|
1446
|
+
],
|
|
1447
|
+
};
|
|
1448
|
+
});
|
|
1449
|
+
server.tool("get_order", "Get a single order by id, including items, payments, and tax summary.", {
|
|
1450
|
+
id: z.string().describe("Order id"),
|
|
1451
|
+
}, async ({ id }) => {
|
|
1452
|
+
const data = await client.query(ORDER_BY_ID_QUERY, { workspaceId: client.workspaceId, id });
|
|
1453
|
+
if (!data.order) {
|
|
1454
|
+
return {
|
|
1455
|
+
content: [{ type: "text", text: "Order not found" }],
|
|
1456
|
+
isError: true,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
content: [
|
|
1461
|
+
{ type: "text", text: JSON.stringify(data.order, null, 2) },
|
|
1462
|
+
],
|
|
1463
|
+
};
|
|
1464
|
+
});
|
|
1465
|
+
server.tool("get_order_pipeline", "Get the workspace order pipeline (the configurable stages orders move through).", {}, async () => {
|
|
1466
|
+
const data = await client.query(ORDER_PIPELINE_QUERY, { workspaceId: client.workspaceId });
|
|
1467
|
+
return {
|
|
1468
|
+
content: [
|
|
1469
|
+
{
|
|
1470
|
+
type: "text",
|
|
1471
|
+
text: JSON.stringify(data.orderPipeline, null, 2),
|
|
1472
|
+
},
|
|
1473
|
+
],
|
|
1474
|
+
};
|
|
1475
|
+
});
|
|
1476
|
+
server.tool("create_manual_order", "Create a manual (admin-entered) order. Each item references a product recordId or is an ad-hoc line with name+price (minor units). Returns a minimal ack by default; pass response='full' for the full order.", {
|
|
1477
|
+
customerEmail: z.string().describe("Customer email for the order"),
|
|
1478
|
+
customerId: z.string().optional(),
|
|
1479
|
+
items: z.array(orderItemSchema).min(1, "Provide at least 1 item"),
|
|
1480
|
+
response: responseModeSchema,
|
|
1481
|
+
}, async ({ customerEmail, customerId, items, response }) => {
|
|
1482
|
+
const result = await client.query(CREATE_MANUAL_ORDER_MUTATION, {
|
|
1483
|
+
input: {
|
|
1484
|
+
workspaceId: client.workspaceId,
|
|
1485
|
+
customerEmail,
|
|
1486
|
+
customerId,
|
|
1487
|
+
items,
|
|
1488
|
+
},
|
|
1489
|
+
});
|
|
1490
|
+
return jsonText(response, result.createManualOrder, orderMinimal);
|
|
1491
|
+
});
|
|
1492
|
+
server.tool("edit_order", "Replace an order's line items (full replace of the items array). Recomputes totals. Returns a minimal ack by default; pass response='full' for the full order.", {
|
|
1493
|
+
orderId: z.string(),
|
|
1494
|
+
items: z.array(orderItemSchema).min(1, "Provide at least 1 item"),
|
|
1495
|
+
response: responseModeSchema,
|
|
1496
|
+
}, async ({ orderId, items, response }) => {
|
|
1497
|
+
const result = await client.query(EDIT_ORDER_MUTATION, { input: { workspaceId: client.workspaceId, orderId, items } });
|
|
1498
|
+
return jsonText(response, result.editOrder, orderMinimal);
|
|
1499
|
+
});
|
|
1500
|
+
server.tool("update_order_details", "Update order metadata: customer email, internal notes, and shipment tracking. Only provided fields change.", {
|
|
1501
|
+
orderId: z.string(),
|
|
1502
|
+
customerEmail: z.string().optional(),
|
|
1503
|
+
notes: z.string().optional(),
|
|
1504
|
+
trackingNumber: z.string().optional(),
|
|
1505
|
+
trackingCarrier: z.string().optional(),
|
|
1506
|
+
response: responseModeSchema,
|
|
1507
|
+
}, async ({ orderId, response, ...fields }) => {
|
|
1508
|
+
if (Object.keys(fields).length === 0) {
|
|
1509
|
+
return {
|
|
1510
|
+
content: [
|
|
1511
|
+
{
|
|
1512
|
+
type: "text",
|
|
1513
|
+
text: "Provide at least one field to update",
|
|
1514
|
+
},
|
|
1515
|
+
],
|
|
1516
|
+
isError: true,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
const result = await client.query(UPDATE_ORDER_DETAILS_MUTATION, { input: { workspaceId: client.workspaceId, orderId, ...fields } });
|
|
1520
|
+
return jsonText(response, result.updateOrderDetails, orderMinimal);
|
|
1521
|
+
});
|
|
1522
|
+
server.tool("mark_order_paid", "Record a full payment for an order (does NOT verify with a payment provider - admin/manual reconciliation). amount is in minor units.", {
|
|
1523
|
+
orderId: z.string(),
|
|
1524
|
+
amount: z.number().int().describe("Amount in minor units (cents)"),
|
|
1525
|
+
reference: z.string().describe("Payment reference / receipt id"),
|
|
1526
|
+
provider: z.string().describe("Payment provider label, e.g. 'manual'"),
|
|
1527
|
+
response: responseModeSchema,
|
|
1528
|
+
}, async ({ orderId, amount, reference, provider, response }) => {
|
|
1529
|
+
const result = await client.query(MARK_ORDER_PAID_MUTATION, {
|
|
1530
|
+
input: {
|
|
1531
|
+
workspaceId: client.workspaceId,
|
|
1532
|
+
orderId,
|
|
1533
|
+
amount,
|
|
1534
|
+
reference,
|
|
1535
|
+
provider,
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
return jsonText(response, result.markOrderPaid, orderMinimal);
|
|
1539
|
+
});
|
|
1540
|
+
server.tool("record_order_payment", "Record a (possibly partial) payment against an order's outstanding balance. amount is in minor units and capped at the balance due.", {
|
|
1541
|
+
orderId: z.string(),
|
|
1542
|
+
amount: z.number().int().describe("Amount in minor units (cents)"),
|
|
1543
|
+
reference: z.string().describe("Payment reference / receipt id"),
|
|
1544
|
+
provider: z.string().optional(),
|
|
1545
|
+
response: responseModeSchema,
|
|
1546
|
+
}, async ({ orderId, amount, reference, provider, response }) => {
|
|
1547
|
+
const result = await client.query(RECORD_ORDER_PAYMENT_MUTATION, {
|
|
1548
|
+
input: {
|
|
1549
|
+
workspaceId: client.workspaceId,
|
|
1550
|
+
orderId,
|
|
1551
|
+
amount,
|
|
1552
|
+
reference,
|
|
1553
|
+
provider,
|
|
1554
|
+
},
|
|
1555
|
+
});
|
|
1556
|
+
return jsonText(response, result.recordOrderPayment, orderMinimal);
|
|
1557
|
+
});
|
|
1558
|
+
server.tool("refund_order", "Refund an order. Omit amount for a full refund; pass amount (minor units) for a partial refund.", {
|
|
1559
|
+
orderId: z.string(),
|
|
1560
|
+
reference: z.string().describe("Refund reference / receipt id"),
|
|
1561
|
+
amount: z
|
|
1562
|
+
.number()
|
|
1563
|
+
.int()
|
|
1564
|
+
.optional()
|
|
1565
|
+
.describe("Partial refund amount in minor units; omit for full refund"),
|
|
1566
|
+
response: responseModeSchema,
|
|
1567
|
+
}, async ({ orderId, reference, amount, response }) => {
|
|
1568
|
+
const result = await client.query(REFUND_ORDER_MUTATION, {
|
|
1569
|
+
input: {
|
|
1570
|
+
workspaceId: client.workspaceId,
|
|
1571
|
+
orderId,
|
|
1572
|
+
reference,
|
|
1573
|
+
amount,
|
|
1574
|
+
},
|
|
1575
|
+
});
|
|
1576
|
+
return jsonText(response, result.refundOrder, orderMinimal);
|
|
1577
|
+
});
|
|
1578
|
+
server.tool("cancel_order", "Cancel an order.", {
|
|
1579
|
+
orderId: z.string(),
|
|
1580
|
+
response: responseModeSchema,
|
|
1581
|
+
}, async ({ orderId, response }) => {
|
|
1582
|
+
const result = await client.query(CANCEL_ORDER_MUTATION, { input: { workspaceId: client.workspaceId, orderId } });
|
|
1583
|
+
return jsonText(response, result.cancelOrder, orderMinimal);
|
|
1584
|
+
});
|
|
1585
|
+
server.tool("transition_order_fulfillment", "Move an order to a new fulfillment status. Optionally attach tracking when marking fulfilled.", {
|
|
1586
|
+
orderId: z.string(),
|
|
1587
|
+
status: z.enum(FULFILLMENT_STATUS),
|
|
1588
|
+
trackingNumber: z.string().optional(),
|
|
1589
|
+
trackingCarrier: z.string().optional(),
|
|
1590
|
+
response: responseModeSchema,
|
|
1591
|
+
}, async ({ orderId, status, trackingNumber, trackingCarrier, response }) => {
|
|
1592
|
+
const result = await client.query(TRANSITION_ORDER_FULFILLMENT_MUTATION, {
|
|
1593
|
+
input: {
|
|
1594
|
+
workspaceId: client.workspaceId,
|
|
1595
|
+
orderId,
|
|
1596
|
+
status,
|
|
1597
|
+
trackingNumber,
|
|
1598
|
+
trackingCarrier,
|
|
1599
|
+
},
|
|
1600
|
+
});
|
|
1601
|
+
return jsonText(response, result.transitionOrderFulfillment, orderMinimal);
|
|
1602
|
+
});
|
|
1603
|
+
server.tool("set_order_pipeline_stage", "Move an order to a pipeline stage (use get_order_pipeline for valid stage ids).", {
|
|
1604
|
+
orderId: z.string(),
|
|
1605
|
+
stageId: z.string(),
|
|
1606
|
+
response: responseModeSchema,
|
|
1607
|
+
}, async ({ orderId, stageId, response }) => {
|
|
1608
|
+
const result = await client.query(SET_ORDER_PIPELINE_STAGE_MUTATION, { input: { workspaceId: client.workspaceId, orderId, stageId } });
|
|
1609
|
+
return jsonText(response, result.setOrderPipelineStage, orderMinimal);
|
|
1610
|
+
});
|
|
1611
|
+
server.tool("record_order_invoice", "Attach an invoice (number, optional URL and provider) to an order.", {
|
|
1612
|
+
orderId: z.string(),
|
|
1613
|
+
number: z.string().describe("Invoice number"),
|
|
1614
|
+
url: z.string().optional(),
|
|
1615
|
+
provider: z.string().optional(),
|
|
1616
|
+
response: responseModeSchema,
|
|
1617
|
+
}, async ({ orderId, number, url, provider, response }) => {
|
|
1618
|
+
const result = await client.query(RECORD_ORDER_INVOICE_MUTATION, {
|
|
1619
|
+
input: {
|
|
1620
|
+
workspaceId: client.workspaceId,
|
|
1621
|
+
orderId,
|
|
1622
|
+
number,
|
|
1623
|
+
url,
|
|
1624
|
+
provider,
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
return jsonText(response, result.recordOrderInvoice, orderMinimal);
|
|
1628
|
+
});
|
|
1629
|
+
// ─── Commerce: Carts ─────────────────────────────────────────
|
|
1630
|
+
server.tool("list_carts", "List shopping carts (admin view) with optional status filter and pagination. totalValue is in minor units.", {
|
|
1631
|
+
status: z.enum(CART_STATUS).optional(),
|
|
1632
|
+
skip: z.number().int().optional().default(0),
|
|
1633
|
+
limit: z.number().int().optional().default(20),
|
|
1634
|
+
}, async (args) => {
|
|
1635
|
+
const data = await client.query(ADMIN_CARTS_QUERY, { workspaceId: client.workspaceId, ...args });
|
|
1636
|
+
return {
|
|
1637
|
+
content: [
|
|
1638
|
+
{
|
|
1639
|
+
type: "text",
|
|
1640
|
+
text: JSON.stringify(data.adminCarts, null, 2),
|
|
1641
|
+
},
|
|
1642
|
+
],
|
|
1643
|
+
};
|
|
1644
|
+
});
|
|
1645
|
+
// ─── Commerce: Discounts ─────────────────────────────────────
|
|
1646
|
+
server.tool("list_discounts", "List discount codes with optional filters and pagination.", {
|
|
1647
|
+
enabled: z.boolean().optional(),
|
|
1648
|
+
type: z.enum(DISCOUNT_TYPE).optional(),
|
|
1649
|
+
search: z.string().optional().describe("Substring match on code"),
|
|
1650
|
+
limit: z.number().int().optional().default(25),
|
|
1651
|
+
offset: z.number().int().optional().default(0),
|
|
1652
|
+
}, async (args) => {
|
|
1653
|
+
const data = await client.query(DISCOUNTS_QUERY, { workspaceId: client.workspaceId, ...args });
|
|
1654
|
+
return {
|
|
1655
|
+
content: [
|
|
1656
|
+
{
|
|
1657
|
+
type: "text",
|
|
1658
|
+
text: JSON.stringify(data.discounts, null, 2),
|
|
1659
|
+
},
|
|
1660
|
+
],
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
server.tool("get_discount", "Get a single discount by id.", {
|
|
1664
|
+
id: z.string(),
|
|
1665
|
+
}, async ({ id }) => {
|
|
1666
|
+
const data = await client.query(DISCOUNT_BY_ID_QUERY, { workspaceId: client.workspaceId, id });
|
|
1667
|
+
if (!data.discount) {
|
|
1668
|
+
return {
|
|
1669
|
+
content: [{ type: "text", text: "Discount not found" }],
|
|
1670
|
+
isError: true,
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
return {
|
|
1674
|
+
content: [
|
|
1675
|
+
{
|
|
1676
|
+
type: "text",
|
|
1677
|
+
text: JSON.stringify(data.discount, null, 2),
|
|
1678
|
+
},
|
|
1679
|
+
],
|
|
1680
|
+
};
|
|
1681
|
+
});
|
|
1682
|
+
bindSharedTool(server, createDiscountTool, sharedOps);
|
|
1683
|
+
server.tool("update_discount", "Update a discount (partial - only provided fields change). code/type/currency become immutable once the discount has been used at least once.", {
|
|
1684
|
+
id: z.string(),
|
|
1685
|
+
code: z.string().optional(),
|
|
1686
|
+
type: z.enum(DISCOUNT_TYPE).optional(),
|
|
1687
|
+
value: z.number().optional(),
|
|
1688
|
+
currency: z.string().optional(),
|
|
1689
|
+
minSubtotal: z.number().optional(),
|
|
1690
|
+
maxUses: z.number().int().optional(),
|
|
1691
|
+
maxUsesPerUser: z.number().int().optional(),
|
|
1692
|
+
startsAt: z.string().optional().describe("ISO date-time"),
|
|
1693
|
+
endsAt: z.string().optional().describe("ISO date-time"),
|
|
1694
|
+
enabled: z.boolean().optional(),
|
|
1662
1695
|
response: responseModeSchema,
|
|
1663
|
-
}, async ({ id,
|
|
1664
|
-
if (
|
|
1696
|
+
}, async ({ id, response, ...input }) => {
|
|
1697
|
+
if (Object.keys(input).length === 0) {
|
|
1665
1698
|
return {
|
|
1666
1699
|
content: [
|
|
1667
1700
|
{
|
|
1668
1701
|
type: "text",
|
|
1669
|
-
text: "Provide
|
|
1702
|
+
text: "Provide at least one field to update",
|
|
1670
1703
|
},
|
|
1671
1704
|
],
|
|
1672
1705
|
isError: true,
|
|
1673
1706
|
};
|
|
1674
1707
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1708
|
+
const result = await client.query(UPDATE_DISCOUNT_MUTATION, { workspaceId: client.workspaceId, id, input });
|
|
1709
|
+
return jsonText(response, result.updateDiscount, discountMinimal);
|
|
1710
|
+
});
|
|
1711
|
+
server.tool("set_discount_enabled", "Enable or disable a discount code.", {
|
|
1712
|
+
id: z.string(),
|
|
1713
|
+
enabled: z.boolean(),
|
|
1714
|
+
response: responseModeSchema,
|
|
1715
|
+
}, async ({ id, enabled, response }) => {
|
|
1716
|
+
const result = await client.query(SET_DISCOUNT_ENABLED_MUTATION, { workspaceId: client.workspaceId, id, enabled });
|
|
1717
|
+
return jsonText(response, result.setDiscountEnabled, discountMinimal);
|
|
1718
|
+
});
|
|
1719
|
+
// ─── Commerce: Products (catalog over model records) ─────────
|
|
1720
|
+
//
|
|
1721
|
+
// Products are stored as records of a Custom Data Model. These tools add
|
|
1722
|
+
// product-aware reads/writes (stock + variants) on top of the generic
|
|
1723
|
+
// record tools. Money fields are integer minor units (cents).
|
|
1724
|
+
const productFilterSchema = z.object({
|
|
1725
|
+
search: z.string().optional(),
|
|
1726
|
+
status: z.string().optional(),
|
|
1727
|
+
stockState: z
|
|
1728
|
+
.enum(["in", "low", "out"])
|
|
1729
|
+
.optional()
|
|
1730
|
+
.describe("in stock / low stock / out of stock"),
|
|
1731
|
+
priceMin: z.number().optional(),
|
|
1732
|
+
priceMax: z.number().optional(),
|
|
1733
|
+
hasVariants: z.boolean().optional(),
|
|
1734
|
+
});
|
|
1735
|
+
server.tool("list_products", "List a product model's catalog with stock and variant info. modelId is the Custom Data Model holding the products. Returns CatalogItem rows (onHand/reserved/available + per-variant stock) plus the workspace lowStockThreshold.", {
|
|
1736
|
+
modelId: z.string().describe("Product model id (ObjectId)"),
|
|
1737
|
+
filter: productFilterSchema.optional(),
|
|
1738
|
+
limit: z.number().int().optional().default(50),
|
|
1739
|
+
offset: z.number().int().optional().default(0),
|
|
1740
|
+
sort: z
|
|
1741
|
+
.string()
|
|
1742
|
+
.optional()
|
|
1743
|
+
.describe("Sort expression, e.g. 'createdAt' or '-updatedAt'"),
|
|
1744
|
+
}, async ({ modelId, filter, limit, offset, sort }) => {
|
|
1745
|
+
const data = await client.query(PRODUCT_CATALOG_QUERY, { modelId, filter, limit, offset, sort });
|
|
1746
|
+
return {
|
|
1747
|
+
content: [
|
|
1748
|
+
{
|
|
1749
|
+
type: "text",
|
|
1750
|
+
text: JSON.stringify(data.productCatalog, null, 2),
|
|
1751
|
+
},
|
|
1752
|
+
],
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
// Selection model shared by the bulk product mutations: target an explicit
|
|
1756
|
+
// list of ids, OR everything matching a filter (allMatching=true). One of
|
|
1757
|
+
// the two is required by the backend.
|
|
1758
|
+
const productSelectionSchema = z
|
|
1759
|
+
.object({
|
|
1760
|
+
ids: z
|
|
1761
|
+
.array(z.string())
|
|
1762
|
+
.optional()
|
|
1763
|
+
.describe("Explicit record ids to target"),
|
|
1764
|
+
allMatching: z
|
|
1765
|
+
.boolean()
|
|
1766
|
+
.optional()
|
|
1767
|
+
.describe("Target every record matching filter (ignores ids)"),
|
|
1768
|
+
filter: productFilterSchema.optional(),
|
|
1769
|
+
})
|
|
1770
|
+
.refine((s) => (s.ids?.length ?? 0) > 0 || s.allMatching === true, {
|
|
1771
|
+
message: "Provide a non-empty 'ids' array or set 'allMatching: true'",
|
|
1772
|
+
})
|
|
1773
|
+
.describe("Provide non-empty 'ids' OR 'allMatching: true'");
|
|
1774
|
+
server.tool("bulk_update_products", "Bulk-update selected product records with a single patch applied uniformly (status, set/adjust stock, set/adjust price). Stock fields are units; price fields are minor units. Returns the number of records updated.", {
|
|
1775
|
+
modelId: z.string().describe("Product model id (ObjectId)"),
|
|
1776
|
+
selection: productSelectionSchema,
|
|
1777
|
+
patch: z
|
|
1778
|
+
.object({
|
|
1779
|
+
status: z.string().optional(),
|
|
1780
|
+
setStock: z.number().int().optional().describe("Set absolute stock"),
|
|
1781
|
+
adjustStock: z
|
|
1782
|
+
.number()
|
|
1783
|
+
.int()
|
|
1784
|
+
.optional()
|
|
1785
|
+
.describe("Delta to stock (+/-)"),
|
|
1786
|
+
setPrice: z
|
|
1787
|
+
.number()
|
|
1788
|
+
.int()
|
|
1789
|
+
.optional()
|
|
1790
|
+
.describe("Set absolute price (minor units)"),
|
|
1791
|
+
adjustPrice: z
|
|
1792
|
+
.number()
|
|
1793
|
+
.int()
|
|
1794
|
+
.optional()
|
|
1795
|
+
.describe("Delta to price (minor units, +/-)"),
|
|
1796
|
+
})
|
|
1797
|
+
.describe("Single patch applied to every selected record"),
|
|
1798
|
+
}, async ({ modelId, selection, patch }) => {
|
|
1799
|
+
if (Object.keys(patch).length === 0) {
|
|
1800
|
+
return {
|
|
1801
|
+
content: [
|
|
1802
|
+
{
|
|
1803
|
+
type: "text",
|
|
1804
|
+
text: "Provide at least one patch field (status, set/adjust stock or price)",
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
isError: true,
|
|
1808
|
+
};
|
|
1688
1809
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1810
|
+
const data = await client.query(BULK_UPDATE_PRODUCT_RECORDS_MUTATION, { modelId, selection, patch });
|
|
1811
|
+
return {
|
|
1812
|
+
content: [
|
|
1813
|
+
{
|
|
1814
|
+
type: "text",
|
|
1815
|
+
text: JSON.stringify({ updated: data.bulkUpdateProductRecords }),
|
|
1816
|
+
},
|
|
1817
|
+
],
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
server.tool("bulk_delete_products", "Bulk-delete selected product records permanently. Returns the number of records deleted.", {
|
|
1821
|
+
modelId: z.string().describe("Product model id (ObjectId)"),
|
|
1822
|
+
selection: productSelectionSchema,
|
|
1823
|
+
}, async ({ modelId, selection }) => {
|
|
1824
|
+
const data = await client.query(BULK_DELETE_PRODUCT_RECORDS_MUTATION, { modelId, selection });
|
|
1825
|
+
return {
|
|
1826
|
+
content: [
|
|
1827
|
+
{
|
|
1828
|
+
type: "text",
|
|
1829
|
+
text: JSON.stringify({ deleted: data.bulkDeleteProductRecords }),
|
|
1830
|
+
},
|
|
1831
|
+
],
|
|
1832
|
+
};
|
|
1833
|
+
});
|
|
1834
|
+
// ─── Webhooks ────────────────────────────────────────────────
|
|
1835
|
+
//
|
|
1836
|
+
// Outbound event webhooks. Like commerce, workspaceId is required by every
|
|
1837
|
+
// resolver and sourced from the client. createWebhookEndpoint and
|
|
1838
|
+
// rotateWebhookSecret return the signing secret ONCE - it is surfaced in
|
|
1839
|
+
// full and cannot be retrieved again.
|
|
1840
|
+
server.tool("list_webhooks", "List the workspace's webhook endpoints (signing secrets are never returned here).", {}, async () => {
|
|
1841
|
+
const data = await client.query(WEBHOOK_ENDPOINTS_QUERY, { workspaceId: client.workspaceId });
|
|
1842
|
+
return {
|
|
1843
|
+
content: [
|
|
1844
|
+
{
|
|
1845
|
+
type: "text",
|
|
1846
|
+
text: JSON.stringify(data.webhookEndpoints, null, 2),
|
|
1847
|
+
},
|
|
1848
|
+
],
|
|
1849
|
+
};
|
|
1850
|
+
});
|
|
1851
|
+
server.tool("list_webhook_deliveries", "List recent webhook delivery attempts (status: pending/success/failed) for debugging.", {
|
|
1852
|
+
limit: z.number().int().optional().default(50),
|
|
1853
|
+
}, async ({ limit }) => {
|
|
1854
|
+
const data = await client.query(WEBHOOK_DELIVERIES_QUERY, { workspaceId: client.workspaceId, limit });
|
|
1855
|
+
return {
|
|
1856
|
+
content: [
|
|
1857
|
+
{
|
|
1858
|
+
type: "text",
|
|
1859
|
+
text: JSON.stringify(data.webhookDeliveries, null, 2),
|
|
1860
|
+
},
|
|
1861
|
+
],
|
|
1862
|
+
};
|
|
1863
|
+
});
|
|
1864
|
+
server.tool("list_webhook_event_types", "List the event types a webhook can subscribe to (the authoritative allowlist).", {}, async () => {
|
|
1865
|
+
const data = await client.query(WEBHOOK_EVENT_TYPES_QUERY, { workspaceId: client.workspaceId });
|
|
1866
|
+
return {
|
|
1867
|
+
content: [
|
|
1868
|
+
{
|
|
1869
|
+
type: "text",
|
|
1870
|
+
text: JSON.stringify(data.webhookEventTypes, null, 2),
|
|
1871
|
+
},
|
|
1872
|
+
],
|
|
1873
|
+
};
|
|
1874
|
+
});
|
|
1875
|
+
// The backend validates events against its allowlist; list_webhook_event_types
|
|
1876
|
+
// is authoritative. These are the known values at time of writing.
|
|
1877
|
+
const WEBHOOK_EVENTS_HINT = "Event names, e.g. order.created, order.paid, order.refunded, order.canceled, order.fulfilled, order.returned, order.edited, order.pipeline_changed. Use list_webhook_event_types for the authoritative list.";
|
|
1878
|
+
server.tool("create_webhook", "Create a webhook endpoint subscribed to one or more events. The URL must be a public https endpoint. Returns the endpoint AND its signing secret - the secret is shown only once.", {
|
|
1879
|
+
url: z.string().describe("Public https endpoint URL"),
|
|
1880
|
+
events: z
|
|
1881
|
+
.array(z.string())
|
|
1882
|
+
.min(1, "Subscribe to at least 1 event")
|
|
1883
|
+
.describe(WEBHOOK_EVENTS_HINT),
|
|
1884
|
+
description: z.string().optional(),
|
|
1885
|
+
}, async ({ url, events, description }) => {
|
|
1886
|
+
const data = await client.query(CREATE_WEBHOOK_ENDPOINT_MUTATION, {
|
|
1887
|
+
input: { workspaceId: client.workspaceId, url, events, description },
|
|
1888
|
+
});
|
|
1889
|
+
return {
|
|
1890
|
+
content: [
|
|
1891
|
+
{
|
|
1892
|
+
type: "text",
|
|
1893
|
+
text: JSON.stringify(data.createWebhookEndpoint, null, 2),
|
|
1894
|
+
},
|
|
1895
|
+
],
|
|
1896
|
+
};
|
|
1897
|
+
});
|
|
1898
|
+
server.tool("update_webhook", "Update a webhook endpoint (partial - only provided fields change). Pass enabled to enable/disable. Pass description=null to clear it. The signing secret is not returned.", {
|
|
1899
|
+
id: z.string(),
|
|
1900
|
+
url: z.string().optional(),
|
|
1901
|
+
events: z
|
|
1902
|
+
.array(z.string())
|
|
1903
|
+
.min(1)
|
|
1904
|
+
.optional()
|
|
1905
|
+
.describe(WEBHOOK_EVENTS_HINT),
|
|
1906
|
+
enabled: z.boolean().optional(),
|
|
1907
|
+
description: z
|
|
1908
|
+
.string()
|
|
1909
|
+
.nullable()
|
|
1910
|
+
.optional()
|
|
1911
|
+
.describe("New description; null clears it"),
|
|
1912
|
+
}, async ({ id, ...fields }) => {
|
|
1913
|
+
if (Object.keys(fields).length === 0) {
|
|
1914
|
+
return {
|
|
1915
|
+
content: [
|
|
1916
|
+
{
|
|
1917
|
+
type: "text",
|
|
1918
|
+
text: "Provide at least one field to update",
|
|
1919
|
+
},
|
|
1920
|
+
],
|
|
1921
|
+
isError: true,
|
|
1922
|
+
};
|
|
1739
1923
|
}
|
|
1740
|
-
const
|
|
1741
|
-
return
|
|
1924
|
+
const data = await client.query(UPDATE_WEBHOOK_ENDPOINT_MUTATION, { input: { workspaceId: client.workspaceId, id, ...fields } });
|
|
1925
|
+
return {
|
|
1926
|
+
content: [
|
|
1927
|
+
{
|
|
1928
|
+
type: "text",
|
|
1929
|
+
text: JSON.stringify(data.updateWebhookEndpoint, null, 2),
|
|
1930
|
+
},
|
|
1931
|
+
],
|
|
1932
|
+
};
|
|
1742
1933
|
});
|
|
1743
|
-
server.tool("
|
|
1744
|
-
id: z.string()
|
|
1934
|
+
server.tool("rotate_webhook_secret", "Rotate a webhook endpoint's signing secret. Returns the endpoint and the NEW secret - shown only once.", {
|
|
1935
|
+
id: z.string(),
|
|
1745
1936
|
}, async ({ id }) => {
|
|
1746
|
-
const data = await client.query(
|
|
1937
|
+
const data = await client.query(ROTATE_WEBHOOK_SECRET_MUTATION, { workspaceId: client.workspaceId, id });
|
|
1747
1938
|
return {
|
|
1748
1939
|
content: [
|
|
1749
1940
|
{
|
|
1750
1941
|
type: "text",
|
|
1751
|
-
text: data.
|
|
1752
|
-
? "Record deleted"
|
|
1753
|
-
: "Failed to delete record",
|
|
1942
|
+
text: JSON.stringify(data.rotateWebhookSecret, null, 2),
|
|
1754
1943
|
},
|
|
1755
1944
|
],
|
|
1756
|
-
isError: !data.deleteModelRecord,
|
|
1757
1945
|
};
|
|
1758
1946
|
});
|
|
1759
|
-
server.tool("
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
.array(z.record(z.string(), z.unknown()))
|
|
1764
|
-
.min(1, "Provide at least 1 row")
|
|
1765
|
-
.max(1000, "Maximum 1000 rows per import"))
|
|
1766
|
-
.describe("Array of plain objects keyed by model field keys"),
|
|
1767
|
-
}, async ({ modelId, rows }) => {
|
|
1768
|
-
const data = await client.query(IMPORT_MODEL_RECORDS_MUTATION, { input: { modelId, rows } });
|
|
1947
|
+
server.tool("delete_webhook", "Delete a webhook endpoint permanently.", {
|
|
1948
|
+
id: z.string(),
|
|
1949
|
+
}, async ({ id }) => {
|
|
1950
|
+
const data = await client.query(DELETE_WEBHOOK_ENDPOINT_MUTATION, { workspaceId: client.workspaceId, id });
|
|
1769
1951
|
return {
|
|
1770
1952
|
content: [
|
|
1771
1953
|
{
|
|
1772
1954
|
type: "text",
|
|
1773
|
-
text:
|
|
1955
|
+
text: data.deleteWebhookEndpoint
|
|
1956
|
+
? "Webhook deleted"
|
|
1957
|
+
: "Failed to delete webhook",
|
|
1774
1958
|
},
|
|
1775
1959
|
],
|
|
1960
|
+
isError: !data.deleteWebhookEndpoint,
|
|
1776
1961
|
};
|
|
1777
1962
|
});
|
|
1778
1963
|
// ─── Resources ───────────────────────────────────────────────
|