@cmssy/mcp-server 0.11.0 → 0.16.1

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/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 { mediaTypeValues } from "@cmssy/types";
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, CREATE_FORM_MUTATION, 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, CREATE_MODEL_DEFINITION_MUTATION, UPDATE_MODEL_DEFINITION_MUTATION, DELETE_MODEL_DEFINITION_MUTATION, CREATE_MODEL_RECORD_MUTATION, UPDATE_MODEL_RECORD_MUTATION, UPDATE_MODEL_RECORD_STATUS_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, CREATE_DISCOUNT_MUTATION, 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";
38
- import { responseModeSchema, pageMinimal, pageBlockMinimal, formMinimal, modelMinimal, recordMinimal, orderMinimal, discountMinimal, jsonText, } from "./responses.js";
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.tool("create_page", "Create a new page. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
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.string().describe("Field key (identifier in customFields)"),
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.tool("create_form", "Create a new form with fields and settings. Returns a minimal ack by default; pass response='full' for the full form.", {
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.tool("create_model", "Create a new Custom Data Model (ModelDefinition). Returns a minimal ack by default; pass response='full' for the full model.", {
1487
- name: z.string().describe("Display name"),
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,109 +1334,8 @@ export function createServer(client) {
1637
1334
  ],
1638
1335
  };
1639
1336
  });
1640
- server.tool("create_record", "Create a new record in a model. Data keys must match field keys of the model; backend validates against the ModelDefinition. Returns a minimal ack by default; pass response='full' for the full record.", {
1641
- modelId: z.string().describe("Target model id (ObjectId)"),
1642
- data: z
1643
- .preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
1644
- .describe("Record data keyed by model field keys"),
1645
- response: responseModeSchema,
1646
- }, async ({ modelId, data: recordData, response }) => {
1647
- const result = await client.query(CREATE_MODEL_RECORD_MUTATION, {
1648
- input: { modelId, data: recordData },
1649
- });
1650
- return jsonText(response, result.createModelRecord, recordMinimal);
1651
- });
1652
- server.tool("update_record", "Update a record. Pass 'data' for a full data replace. Pass 'status' to transition the record's status (validated against statusField.transitions). At least one of data/status is required. Returns a minimal ack by default; pass response='full' for the full record.", {
1653
- id: z.string().describe("Record id (ObjectId)"),
1654
- data: z
1655
- .preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
1656
- .optional()
1657
- .describe("New data (full replace)"),
1658
- status: z
1659
- .string()
1660
- .optional()
1661
- .describe("New status value (must be allowed by model.statusField)"),
1662
- response: responseModeSchema,
1663
- }, async ({ id, data: recordData, status, response }) => {
1664
- if (recordData === undefined && status === undefined) {
1665
- return {
1666
- content: [
1667
- {
1668
- type: "text",
1669
- text: "Provide 'data' and/or 'status' to update",
1670
- },
1671
- ],
1672
- isError: true,
1673
- };
1674
- }
1675
- let dataResult = null;
1676
- let statusResult = null;
1677
- if (recordData !== undefined) {
1678
- const res = await client.query(UPDATE_MODEL_RECORD_MUTATION, {
1679
- input: { id, data: recordData },
1680
- });
1681
- dataResult = res.updateModelRecord;
1682
- if (!dataResult) {
1683
- return {
1684
- content: [{ type: "text", text: "Record not found" }],
1685
- isError: true,
1686
- };
1687
- }
1688
- }
1689
- if (status !== undefined) {
1690
- // Data update (if any) already committed. Wrap status call so a
1691
- // failed transition reports partial-success clearly instead of
1692
- // hiding the completed data write behind a bare throw.
1693
- try {
1694
- const res = await client.query(UPDATE_MODEL_RECORD_STATUS_MUTATION, {
1695
- input: { id, status },
1696
- });
1697
- statusResult = res.updateModelRecordStatus;
1698
- if (!statusResult) {
1699
- if (dataResult) {
1700
- return {
1701
- content: [
1702
- {
1703
- type: "text",
1704
- text: JSON.stringify({
1705
- partialSuccess: true,
1706
- message: "Data updated, but record not found when applying status.",
1707
- dataResult,
1708
- }, null, 2),
1709
- },
1710
- ],
1711
- isError: true,
1712
- };
1713
- }
1714
- return {
1715
- content: [{ type: "text", text: "Record not found" }],
1716
- isError: true,
1717
- };
1718
- }
1719
- }
1720
- catch (error) {
1721
- if (dataResult) {
1722
- return {
1723
- content: [
1724
- {
1725
- type: "text",
1726
- text: JSON.stringify({
1727
- partialSuccess: true,
1728
- message: "Data updated, but status transition failed. Retry status only.",
1729
- dataResult,
1730
- statusError: error instanceof Error ? error.message : String(error),
1731
- }, null, 2),
1732
- },
1733
- ],
1734
- isError: true,
1735
- };
1736
- }
1737
- throw error;
1738
- }
1739
- }
1740
- const finalResult = (statusResult ?? dataResult);
1741
- return jsonText(response, finalResult, recordMinimal);
1742
- });
1337
+ bindSharedTool(server, createRecordTool, sharedOps);
1338
+ bindSharedTool(server, updateRecordTool, sharedOps);
1743
1339
  server.tool("delete_record", "Delete a record permanently.", {
1744
1340
  id: z.string().describe("Record id (ObjectId) to delete"),
1745
1341
  }, async ({ id }) => {
@@ -2083,41 +1679,7 @@ export function createServer(client) {
2083
1679
  ],
2084
1680
  };
2085
1681
  });
2086
- server.tool("create_discount", "Create a discount code. For type='fixed', value and currency are required (value in minor units). For 'percentage', value is the percent and currency must be omitted. minSubtotal is in minor units. Dates are ISO date-time strings.", {
2087
- code: z.string(),
2088
- type: z.enum(DISCOUNT_TYPE),
2089
- value: z
2090
- .number()
2091
- .describe("Percent for 'percentage'; minor units for 'fixed'"),
2092
- currency: z
2093
- .string()
2094
- .optional()
2095
- .describe("Required for 'fixed'; omit otherwise"),
2096
- minSubtotal: z
2097
- .number()
2098
- .optional()
2099
- .describe("Minimum order subtotal in minor units"),
2100
- maxUses: z.number().int().optional(),
2101
- maxUsesPerUser: z.number().int().optional(),
2102
- startsAt: z.string().optional().describe("ISO date-time"),
2103
- endsAt: z.string().optional().describe("ISO date-time"),
2104
- enabled: z.boolean(),
2105
- response: responseModeSchema,
2106
- }, async ({ response, ...input }) => {
2107
- const currencyError = input.type === "fixed" && !input.currency
2108
- ? "currency is required for type 'fixed'"
2109
- : input.type !== "fixed" && input.currency
2110
- ? `currency must be omitted for type '${input.type}'`
2111
- : null;
2112
- if (currencyError) {
2113
- return {
2114
- content: [{ type: "text", text: currencyError }],
2115
- isError: true,
2116
- };
2117
- }
2118
- const result = await client.query(CREATE_DISCOUNT_MUTATION, { workspaceId: client.workspaceId, input });
2119
- return jsonText(response, result.createDiscount, discountMinimal);
2120
- });
1682
+ bindSharedTool(server, createDiscountTool, sharedOps);
2121
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.", {
2122
1684
  id: z.string(),
2123
1685
  code: z.string().optional(),