@cmssy/mcp-server 0.16.1 → 0.17.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/dist/ai-tools-ops.d.ts.map +1 -1
- package/dist/ai-tools-ops.js +925 -15
- package/dist/ai-tools-ops.js.map +1 -1
- package/dist/queries.d.ts +1 -1
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +0 -4
- package/dist/queries.js.map +1 -1
- package/dist/responses.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +63 -1919
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -2,10 +2,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
import { createRecordTool, createDiscountTool, createPageTool, createFormTool, createModelTool, updateModelTool, updateRecordTool, } from "@cmssy/ai-tools";
|
|
5
|
+
import { listPagesTool, getPageTool, getSiteConfigTool, getWorkspaceInfoTool, listMediaTool, createPageTool, updatePageBlocksTool, updatePageSettingsTool, listPageTypesTool, createPageTypeTool, publishPageTool, unpublishPageTool, revertToPublishedTool, deletePageTool, updatePageLayoutTool, addBlockToPageTool, updateBlockContentTool, patchBlockContentTool, removeBlockFromPageTool, listFormsTool, getFormTool, createFormTool, updateFormTool, deleteFormTool, listFormSubmissionsTool, getFormSubmissionTool, updateFormSubmissionStatusTool, deleteFormSubmissionTool, listModelsTool, getModelTool, createModelTool, updateModelTool, deleteModelTool, listRecordsTool, getRecordTool, createRecordTool, updateRecordTool, deleteRecordTool, importRecordsTool, listOrdersTool, getOrderTool, getOrderPipelineTool, createManualOrderTool, editOrderTool, updateOrderDetailsTool, markOrderPaidTool, recordOrderPaymentTool, refundOrderTool, cancelOrderTool, transitionOrderFulfillmentTool, setOrderPipelineStageTool, recordOrderInvoiceTool, listCartsTool, listDiscountsTool, getDiscountTool, createDiscountTool, updateDiscountTool, setDiscountEnabledTool, listProductsTool, bulkUpdateProductsTool, bulkDeleteProductsTool, listWebhooksTool, listWebhookDeliveriesTool, listWebhookEventTypesTool, createWebhookTool, updateWebhookTool, rotateWebhookSecretTool, deleteWebhookTool, } from "@cmssy/ai-tools";
|
|
7
6
|
import { createMcpWorkspaceOps } from "./ai-tools-ops.js";
|
|
8
7
|
import { bindSharedTool } from "./ai-tools-binder.js";
|
|
8
|
+
import { PAGES_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, } from "./queries.js";
|
|
9
9
|
// Read our own version from package.json so the MCP handshake
|
|
10
10
|
// advertises what the user actually installed, instead of drifting
|
|
11
11
|
// whenever we bump the package.
|
|
@@ -19,1947 +19,91 @@ const PACKAGE_VERSION = (() => {
|
|
|
19
19
|
return "0.0.0";
|
|
20
20
|
}
|
|
21
21
|
})();
|
|
22
|
-
/**
|
|
23
|
-
* Preprocess helper: parse JSON string to object if needed.
|
|
24
|
-
* MCP clients may serialize complex nested objects as JSON strings
|
|
25
|
-
* instead of passing them as objects. This ensures they're parsed.
|
|
26
|
-
* Malformed JSON is passed through so Zod produces a validation error
|
|
27
|
-
* instead of crashing the handler.
|
|
28
|
-
*/
|
|
29
|
-
const jsonPreprocess = (val) => {
|
|
30
|
-
if (typeof val !== "string")
|
|
31
|
-
return val;
|
|
32
|
-
try {
|
|
33
|
-
return JSON.parse(val);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return val;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
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";
|
|
41
22
|
export function createServer(client) {
|
|
42
23
|
const server = new McpServer({
|
|
43
24
|
name: "cmssy",
|
|
44
25
|
version: PACKAGE_VERSION,
|
|
45
26
|
});
|
|
46
27
|
const sharedOps = createMcpWorkspaceOps(client);
|
|
47
|
-
/** Check if value is null, undefined, or empty object */
|
|
48
|
-
const isEmpty = (obj) => obj == null ||
|
|
49
|
-
(typeof obj === "object" && Object.keys(obj).length === 0);
|
|
50
|
-
/** Extract last slug segment - savePage expects relative slug, not fullSlug */
|
|
51
|
-
function toRelativeSlug(slug) {
|
|
52
|
-
if (slug === "/")
|
|
53
|
-
return "/";
|
|
54
|
-
return "/" + slug.split("/").filter(Boolean).pop();
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Optimistic-concurrency guard for a read-modify-write save. Passes the
|
|
58
|
-
* version we just read as `expectedVersion`, so if another writer (e.g. the
|
|
59
|
-
* web editor) changed the page in the read→save window the backend returns
|
|
60
|
-
* VERSION_CONFLICT instead of silently clobbering it. Re-running the tool
|
|
61
|
-
* re-reads the fresh page and succeeds.
|
|
62
|
-
*/
|
|
63
|
-
function expectedVersionOf(page) {
|
|
64
|
-
return page?.version != null ? Number(page.version) : undefined;
|
|
65
|
-
}
|
|
66
28
|
// ─── Read Tools ──────────────────────────────────────────────
|
|
67
|
-
server
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}, async ({ search }) => {
|
|
73
|
-
const data = await client.query(PAGES_QUERY, {
|
|
74
|
-
search: search || undefined,
|
|
75
|
-
});
|
|
76
|
-
return {
|
|
77
|
-
content: [
|
|
78
|
-
{ type: "text", text: JSON.stringify(data.pages, null, 2) },
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
server.tool("get_page", "Get full page details with all blocks and i18n content. Provide either slug or id.", {
|
|
83
|
-
slug: z.string().optional().describe("Page slug (e.g. '/' or 'about')"),
|
|
84
|
-
id: z.string().optional().describe("Page ID"),
|
|
85
|
-
}, async ({ slug, id }) => {
|
|
86
|
-
if (!slug && !id) {
|
|
87
|
-
return {
|
|
88
|
-
content: [
|
|
89
|
-
{
|
|
90
|
-
type: "text",
|
|
91
|
-
text: "Error: provide either 'slug' or 'id'",
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
isError: true,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
const data = await client.query(PAGE_BY_ID_QUERY, {
|
|
98
|
-
pageId: id || slug,
|
|
99
|
-
});
|
|
100
|
-
const page = data.page;
|
|
101
|
-
if (!page) {
|
|
102
|
-
return {
|
|
103
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
104
|
-
isError: true,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
content: [
|
|
109
|
-
{ type: "text", text: JSON.stringify(page, null, 2) },
|
|
110
|
-
],
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
server.tool("get_site_config", "Get site configuration: languages, navigation, header, footer, site name, enabled features", {}, async () => {
|
|
114
|
-
// Dynamically introspect header/footer types from the backend schema
|
|
115
|
-
const [headerSel, footerSel] = await Promise.all([
|
|
116
|
-
client.buildSelectionSet("SiteHeader"),
|
|
117
|
-
client.buildSelectionSet("SiteFooter"),
|
|
118
|
-
]);
|
|
119
|
-
const query = `
|
|
120
|
-
query SiteConfig {
|
|
121
|
-
siteConfig {
|
|
122
|
-
id
|
|
123
|
-
defaultLanguage
|
|
124
|
-
enabledLanguages
|
|
125
|
-
siteName
|
|
126
|
-
enabledFeatures
|
|
127
|
-
${headerSel ? `header { ${headerSel} }` : ""}
|
|
128
|
-
${footerSel ? `footer { ${footerSel} }` : ""}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
`;
|
|
132
|
-
const data = await client.query(query);
|
|
133
|
-
return {
|
|
134
|
-
content: [
|
|
135
|
-
{
|
|
136
|
-
type: "text",
|
|
137
|
-
text: JSON.stringify(data.siteConfig, null, 2),
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
server.tool("get_workspace_info", "Get workspace name, plan, limits, and usage", {}, async () => {
|
|
143
|
-
const data = await client.query(CURRENT_WORKSPACE_QUERY);
|
|
144
|
-
return {
|
|
145
|
-
content: [
|
|
146
|
-
{
|
|
147
|
-
type: "text",
|
|
148
|
-
text: JSON.stringify(data.currentWorkspace, null, 2),
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
server.tool("list_media", "List media assets in the workspace (url, filename, alt, type)", {
|
|
154
|
-
limit: z
|
|
155
|
-
.number()
|
|
156
|
-
.optional()
|
|
157
|
-
.default(50)
|
|
158
|
-
.describe("Max items to return (default 50, max 100)"),
|
|
159
|
-
}, async ({ limit }) => {
|
|
160
|
-
const data = await client.query(MEDIA_ASSETS_QUERY, { limit });
|
|
161
|
-
return {
|
|
162
|
-
content: [
|
|
163
|
-
{
|
|
164
|
-
type: "text",
|
|
165
|
-
text: JSON.stringify(data.mediaAssets, null, 2),
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
};
|
|
169
|
-
});
|
|
29
|
+
bindSharedTool(server, listPagesTool, sharedOps);
|
|
30
|
+
bindSharedTool(server, getPageTool, sharedOps);
|
|
31
|
+
bindSharedTool(server, getSiteConfigTool, sharedOps);
|
|
32
|
+
bindSharedTool(server, getWorkspaceInfoTool, sharedOps);
|
|
33
|
+
bindSharedTool(server, listMediaTool, sharedOps);
|
|
170
34
|
// ─── Write Tools ─────────────────────────────────────────────
|
|
171
35
|
bindSharedTool(server, createPageTool, sharedOps);
|
|
172
|
-
server
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
style: z.record(z.string(), z.unknown()).optional(),
|
|
181
|
-
advanced: z.record(z.string(), z.unknown()).optional(),
|
|
182
|
-
translations: z
|
|
183
|
-
.record(z.string(), z.object({ status: z.string() }))
|
|
184
|
-
.optional(),
|
|
185
|
-
defaultLanguage: z.string().optional(),
|
|
186
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
187
|
-
blockVersion: z.string().optional(),
|
|
188
|
-
}))
|
|
189
|
-
.describe("Full array of content blocks to set on the page")),
|
|
190
|
-
response: responseModeSchema,
|
|
191
|
-
}, async ({ pageId, blocks, response }) => {
|
|
192
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
193
|
-
if (!pageData.page) {
|
|
194
|
-
return {
|
|
195
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
196
|
-
isError: true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
// Merge: preserve existing block data when not provided in input
|
|
200
|
-
const existingBlocks = pageData.page.blocks || [];
|
|
201
|
-
const mergedBlocks = blocks.map((block) => {
|
|
202
|
-
const existing = existingBlocks.find((b) => b.id === block.id);
|
|
203
|
-
if (!existing)
|
|
204
|
-
return block;
|
|
205
|
-
return {
|
|
206
|
-
...block,
|
|
207
|
-
content: isEmpty(block.content) ? existing.content : block.content,
|
|
208
|
-
settings: isEmpty(block.settings)
|
|
209
|
-
? existing.settings
|
|
210
|
-
: block.settings,
|
|
211
|
-
style: block.style ?? existing.style,
|
|
212
|
-
advanced: block.advanced ?? existing.advanced,
|
|
213
|
-
translations: isEmpty(block.translations)
|
|
214
|
-
? existing.translations
|
|
215
|
-
: block.translations,
|
|
216
|
-
defaultLanguage: block.defaultLanguage ?? existing.defaultLanguage,
|
|
217
|
-
metadata: block.metadata ?? existing.metadata,
|
|
218
|
-
blockVersion: block.blockVersion ?? existing.blockVersion,
|
|
219
|
-
};
|
|
220
|
-
});
|
|
221
|
-
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
222
|
-
input: {
|
|
223
|
-
id: pageId,
|
|
224
|
-
name: pageData.page.name,
|
|
225
|
-
slug: toRelativeSlug(pageData.page.slug),
|
|
226
|
-
parentId: pageData.page.parentId ?? undefined,
|
|
227
|
-
blocks: mergedBlocks,
|
|
228
|
-
expectedVersion: expectedVersionOf(pageData.page),
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
return jsonText(response, data.savePage, pageMinimal);
|
|
232
|
-
});
|
|
233
|
-
server.tool("update_page_settings", "Update page metadata: name, slug, display name, SEO fields, and custom fields. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
234
|
-
id: z.string().describe("Page ID"),
|
|
235
|
-
name: z.string().optional().describe("Internal page name"),
|
|
236
|
-
slug: z.string().optional().describe("URL slug"),
|
|
237
|
-
displayName: z
|
|
238
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
239
|
-
.optional()
|
|
240
|
-
.describe("Multilingual display name"),
|
|
241
|
-
seoTitle: z
|
|
242
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
243
|
-
.optional()
|
|
244
|
-
.describe("Multilingual SEO title"),
|
|
245
|
-
seoDescription: z
|
|
246
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
247
|
-
.optional()
|
|
248
|
-
.describe("Multilingual SEO description"),
|
|
249
|
-
seoKeywords: z.array(z.string()).optional().describe("SEO keywords"),
|
|
250
|
-
customFields: z
|
|
251
|
-
.preprocess(jsonPreprocess, z.array(z.object({ fieldKey: z.string(), value: z.unknown() })))
|
|
252
|
-
.optional()
|
|
253
|
-
.describe("Custom field values: [{ fieldKey, value }]. value is JSON."),
|
|
254
|
-
response: responseModeSchema,
|
|
255
|
-
}, async ({ id, name, slug, displayName, seoTitle, seoDescription, seoKeywords, customFields, response, }) => {
|
|
256
|
-
const input = { id };
|
|
257
|
-
if (name !== undefined)
|
|
258
|
-
input.name = name;
|
|
259
|
-
if (slug !== undefined)
|
|
260
|
-
input.slug = slug;
|
|
261
|
-
if (displayName !== undefined)
|
|
262
|
-
input.displayName = displayName;
|
|
263
|
-
if (seoTitle !== undefined)
|
|
264
|
-
input.seoTitle = seoTitle;
|
|
265
|
-
if (seoDescription !== undefined)
|
|
266
|
-
input.seoDescription = seoDescription;
|
|
267
|
-
if (seoKeywords !== undefined)
|
|
268
|
-
input.seoKeywords = seoKeywords;
|
|
269
|
-
if (customFields !== undefined)
|
|
270
|
-
input.customFields = customFields;
|
|
271
|
-
const data = await client.query(UPDATE_PAGE_SETTINGS_MUTATION, { input });
|
|
272
|
-
return jsonText(response, data.updatePageSettings, pageMinimal);
|
|
273
|
-
});
|
|
274
|
-
server.tool("list_page_types", "List page types in the workspace with their custom-field schema. Page types define which custom fields a page of that type carries.", {}, async () => {
|
|
275
|
-
const data = await client.query(PAGE_TYPES_QUERY, {});
|
|
276
|
-
return {
|
|
277
|
-
content: [
|
|
278
|
-
{
|
|
279
|
-
type: "text",
|
|
280
|
-
text: JSON.stringify(data.pageTypes, null, 2),
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
};
|
|
284
|
-
});
|
|
285
|
-
server.tool("create_page_type", "Create a page type with a custom-field schema. Pages created with this pageType carry these custom fields (settable via create_page/update_page_settings customFields).", {
|
|
286
|
-
name: z.string().describe("Display name"),
|
|
287
|
-
slug: z
|
|
288
|
-
.string()
|
|
289
|
-
.regex(/^[a-z][a-z0-9-]*$/, "lowercase, URL-safe: start with a letter, then letters/digits/hyphens")
|
|
290
|
-
.describe("URL-safe slug, lowercase (e.g. 'post', 'case-study')"),
|
|
291
|
-
description: z.string().optional().describe("Description"),
|
|
292
|
-
icon: z.string().optional().describe("Lucide icon name"),
|
|
293
|
-
urlPrefix: z
|
|
294
|
-
.string()
|
|
295
|
-
.optional()
|
|
296
|
-
.describe("URL prefix for pages of this type (e.g. 'blog')"),
|
|
297
|
-
allowChildren: z.boolean().optional(),
|
|
298
|
-
fields: z
|
|
299
|
-
.preprocess(jsonPreprocess, z.array(z.object({
|
|
300
|
-
key: z
|
|
301
|
-
.string()
|
|
302
|
-
.describe("Field key (identifier in customFields)"),
|
|
303
|
-
label: z.string().describe("Human-readable label"),
|
|
304
|
-
type: z
|
|
305
|
-
.enum([
|
|
306
|
-
"string",
|
|
307
|
-
"richtext",
|
|
308
|
-
"number",
|
|
309
|
-
"boolean",
|
|
310
|
-
"date",
|
|
311
|
-
"datetime",
|
|
312
|
-
"email",
|
|
313
|
-
"url",
|
|
314
|
-
"media",
|
|
315
|
-
"relation",
|
|
316
|
-
"select",
|
|
317
|
-
"multiselect",
|
|
318
|
-
])
|
|
319
|
-
.describe("Field type"),
|
|
320
|
-
required: z.boolean().optional(),
|
|
321
|
-
description: z.string().optional(),
|
|
322
|
-
options: z
|
|
323
|
-
.array(z.string())
|
|
324
|
-
.optional()
|
|
325
|
-
.describe("For select/multiselect"),
|
|
326
|
-
defaultValue: z.string().optional(),
|
|
327
|
-
})))
|
|
328
|
-
.optional()
|
|
329
|
-
.describe("Custom-field schema for this page type"),
|
|
330
|
-
response: responseModeSchema,
|
|
331
|
-
}, async ({ name, slug, description, icon, urlPrefix, allowChildren, fields, response, }) => {
|
|
332
|
-
const input = { name, slug };
|
|
333
|
-
if (description !== undefined)
|
|
334
|
-
input.description = description;
|
|
335
|
-
if (icon !== undefined)
|
|
336
|
-
input.icon = icon;
|
|
337
|
-
if (urlPrefix !== undefined)
|
|
338
|
-
input.urlPrefix = urlPrefix;
|
|
339
|
-
if (allowChildren !== undefined)
|
|
340
|
-
input.allowChildren = allowChildren;
|
|
341
|
-
if (fields !== undefined)
|
|
342
|
-
input.fields = fields;
|
|
343
|
-
const data = await client.query(CREATE_PAGE_TYPE_MUTATION, { input });
|
|
344
|
-
return jsonText(response, data.createPageType, (pt) => {
|
|
345
|
-
const p = pt;
|
|
346
|
-
return { id: p.id, name: p.name, slug: p.slug };
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
server.tool("publish_page", "Publish a page or re-publish with latest draft changes. Publishes both content and layout axes. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
350
|
-
pageId: z.string().describe("Page ID to publish"),
|
|
351
|
-
response: responseModeSchema,
|
|
352
|
-
}, async ({ pageId, response }) => {
|
|
353
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
354
|
-
if (!pageData.page) {
|
|
355
|
-
return {
|
|
356
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
357
|
-
isError: true,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
const page = pageData.page;
|
|
361
|
-
if (page.published &&
|
|
362
|
-
!page.hasUnpublishedContentChanges &&
|
|
363
|
-
!page.hasUnpublishedLayoutChanges) {
|
|
364
|
-
return {
|
|
365
|
-
content: [
|
|
366
|
-
{
|
|
367
|
-
type: "text",
|
|
368
|
-
text: "Page is already published with latest content. No changes to publish.",
|
|
369
|
-
},
|
|
370
|
-
],
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
// Publishing content flips published=true and snapshots blocks.
|
|
374
|
-
const contentData = await client.query(PUBLISH_PAGE_CONTENT_MUTATION, { id: pageId });
|
|
375
|
-
// Publish layout too so no draft axis is left behind. The two axes are
|
|
376
|
-
// separate backend mutations - if layout fails, content is already
|
|
377
|
-
// published, so report the partial state instead of a bare throw.
|
|
378
|
-
let layoutData;
|
|
379
|
-
try {
|
|
380
|
-
layoutData = await client.query(PUBLISH_PAGE_LAYOUT_MUTATION, { id: pageId });
|
|
381
|
-
}
|
|
382
|
-
catch (err) {
|
|
383
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
384
|
-
return {
|
|
385
|
-
content: [
|
|
386
|
-
{
|
|
387
|
-
type: "text",
|
|
388
|
-
text: `Content published, but the layout axis failed and is still unpublished: ${message}. Note: re-running publish_page also re-publishes the current content draft, so publish any pending content edits first.`,
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
isError: true,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
if (!layoutData.publishPageLayout) {
|
|
395
|
-
return {
|
|
396
|
-
content: [
|
|
397
|
-
{
|
|
398
|
-
type: "text",
|
|
399
|
-
text: "Content published, but the layout axis returned no result and is still unpublished. Note: re-running publish_page also re-publishes the current content draft, so publish any pending content edits first.",
|
|
400
|
-
},
|
|
401
|
-
],
|
|
402
|
-
isError: true,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
// Merge: content payload carries blocks, layout payload carries the
|
|
406
|
-
// up-to-date layout flags (it ran last).
|
|
407
|
-
const published = {
|
|
408
|
-
...contentData.publishPageContent,
|
|
409
|
-
...layoutData.publishPageLayout,
|
|
410
|
-
};
|
|
411
|
-
return jsonText(response, published, (p) => pageMinimal(p, { published: true }));
|
|
412
|
-
});
|
|
413
|
-
server.tool("unpublish_page", "Unpublish a published page (toggles published state off). Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
414
|
-
pageId: z.string().describe("Page ID to unpublish"),
|
|
415
|
-
response: responseModeSchema,
|
|
416
|
-
}, async ({ pageId, response }) => {
|
|
417
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
418
|
-
if (!pageData.page) {
|
|
419
|
-
return {
|
|
420
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
421
|
-
isError: true,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
if (!pageData.page.published) {
|
|
425
|
-
return {
|
|
426
|
-
content: [
|
|
427
|
-
{ type: "text", text: "Page is already unpublished" },
|
|
428
|
-
],
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
const data = await client.query(TOGGLE_PUBLISH_MUTATION, { id: pageId });
|
|
432
|
-
return jsonText(response, data.togglePublish, (p) => pageMinimal(p, { published: false }));
|
|
433
|
-
});
|
|
434
|
-
server.tool("revert_to_published", "Discard all draft changes and revert a page to its last published version. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
435
|
-
pageId: z.string().describe("Page ID to revert"),
|
|
436
|
-
response: responseModeSchema,
|
|
437
|
-
}, async ({ pageId, response }) => {
|
|
438
|
-
// Both revert mutations throw if the page has no published version, so
|
|
439
|
-
// guard up front for a clean message (mirrors publish_page/unpublish_page).
|
|
440
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
441
|
-
if (!pageData.page) {
|
|
442
|
-
return {
|
|
443
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
444
|
-
isError: true,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
if (!pageData.page.published) {
|
|
448
|
-
return {
|
|
449
|
-
content: [
|
|
450
|
-
{
|
|
451
|
-
type: "text",
|
|
452
|
-
text: "Page has no published version to revert to",
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
isError: true,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
const contentData = await client.query(REVERT_CONTENT_TO_PUBLISHED_MUTATION, { id: pageId });
|
|
459
|
-
const revertedContent = contentData.revertContentToPublished;
|
|
460
|
-
if (!revertedContent) {
|
|
461
|
-
return {
|
|
462
|
-
content: [
|
|
463
|
-
{ type: "text", text: "Failed to revert page content" },
|
|
464
|
-
],
|
|
465
|
-
isError: true,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
// Layout revert is a separate mutation - if it fails, content drafts are
|
|
469
|
-
// already discarded, so report the partial state rather than a bare throw.
|
|
470
|
-
let layoutData;
|
|
471
|
-
try {
|
|
472
|
-
layoutData = await client.query(REVERT_LAYOUT_TO_PUBLISHED_MUTATION, { id: pageId });
|
|
473
|
-
}
|
|
474
|
-
catch (err) {
|
|
475
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
476
|
-
return {
|
|
477
|
-
content: [
|
|
478
|
-
{
|
|
479
|
-
type: "text",
|
|
480
|
-
text: `Content reverted, but the layout axis failed and still holds draft changes: ${message}. Note: re-running revert_to_published reverts the content axis again first, discarding any content edits made since this failure.`,
|
|
481
|
-
},
|
|
482
|
-
],
|
|
483
|
-
isError: true,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
if (!layoutData.revertLayoutToPublished) {
|
|
487
|
-
return {
|
|
488
|
-
content: [
|
|
489
|
-
{
|
|
490
|
-
type: "text",
|
|
491
|
-
text: "Content reverted, but the layout axis returned no result and still holds draft changes. Note: re-running revert_to_published reverts the content axis again first, discarding any content edits made since this failure.",
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
isError: true,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
// Merge: content payload carries blocks, layout payload carries the
|
|
498
|
-
// up-to-date layout flags (it ran last).
|
|
499
|
-
const reverted = {
|
|
500
|
-
...revertedContent,
|
|
501
|
-
...layoutData.revertLayoutToPublished,
|
|
502
|
-
};
|
|
503
|
-
return jsonText(response, reverted, pageMinimal);
|
|
504
|
-
});
|
|
505
|
-
server.tool("delete_page", "Permanently delete a page and all its descendants. Cannot delete the homepage.", { pageId: z.string().describe("Page ID to delete") }, async ({ pageId }) => {
|
|
506
|
-
const data = await client.query(REMOVE_PAGE_MUTATION, { id: pageId });
|
|
507
|
-
return {
|
|
508
|
-
content: [
|
|
509
|
-
{
|
|
510
|
-
type: "text",
|
|
511
|
-
text: data.removePage
|
|
512
|
-
? "Page deleted successfully"
|
|
513
|
-
: "Failed to delete page",
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
};
|
|
517
|
-
});
|
|
36
|
+
bindSharedTool(server, updatePageBlocksTool, sharedOps);
|
|
37
|
+
bindSharedTool(server, updatePageSettingsTool, sharedOps);
|
|
38
|
+
bindSharedTool(server, listPageTypesTool, sharedOps);
|
|
39
|
+
bindSharedTool(server, createPageTypeTool, sharedOps);
|
|
40
|
+
bindSharedTool(server, publishPageTool, sharedOps);
|
|
41
|
+
bindSharedTool(server, unpublishPageTool, sharedOps);
|
|
42
|
+
bindSharedTool(server, revertToPublishedTool, sharedOps);
|
|
43
|
+
bindSharedTool(server, deletePageTool, sharedOps);
|
|
518
44
|
// ─── Layout Tools ──────────────────────────────────────────
|
|
519
|
-
server
|
|
520
|
-
pageId: z.string().describe("Page ID"),
|
|
521
|
-
layoutBlocks: z
|
|
522
|
-
.array(z.record(z.string(), z.unknown()))
|
|
523
|
-
.optional()
|
|
524
|
-
.describe("Full replacement array of layout blocks. Each must have 'type' (validated server-side on save). Position, order, content etc. are passed through to the backend."),
|
|
525
|
-
layoutOverrides: z
|
|
526
|
-
.array(z.object({
|
|
527
|
-
position: z.string().describe("Layout position to override"),
|
|
528
|
-
action: z.string().describe("Override action"),
|
|
529
|
-
blockId: z.string().optional().describe("Replacement block ID"),
|
|
530
|
-
}))
|
|
531
|
-
.optional()
|
|
532
|
-
.describe("Layout overrides for inherited positions"),
|
|
533
|
-
inheritsLayout: z
|
|
534
|
-
.boolean()
|
|
535
|
-
.optional()
|
|
536
|
-
.describe("Whether this page inherits layout from parent"),
|
|
537
|
-
response: responseModeSchema,
|
|
538
|
-
}, async ({ pageId, layoutBlocks, layoutOverrides, inheritsLayout, response, }) => {
|
|
539
|
-
// Merge layout blocks: preserve existing content when not provided
|
|
540
|
-
let mergedLayoutBlocks = layoutBlocks;
|
|
541
|
-
if (layoutBlocks) {
|
|
542
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
543
|
-
const existingLayoutBlocks = pageData.page?.layoutBlocks ?? [];
|
|
544
|
-
mergedLayoutBlocks = layoutBlocks.map((block) => {
|
|
545
|
-
const existing = existingLayoutBlocks.find((b) => b.id === block.id);
|
|
546
|
-
if (!existing)
|
|
547
|
-
return block;
|
|
548
|
-
return {
|
|
549
|
-
...block,
|
|
550
|
-
content: isEmpty(block.content) ? existing.content : block.content,
|
|
551
|
-
settings: isEmpty(block.settings)
|
|
552
|
-
? existing.settings
|
|
553
|
-
: block.settings,
|
|
554
|
-
translations: isEmpty(block.translations)
|
|
555
|
-
? existing.translations
|
|
556
|
-
: block.translations,
|
|
557
|
-
};
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
const input = { pageId };
|
|
561
|
-
if (mergedLayoutBlocks !== undefined)
|
|
562
|
-
input.layoutBlocks = mergedLayoutBlocks;
|
|
563
|
-
if (layoutOverrides !== undefined)
|
|
564
|
-
input.layoutOverrides = layoutOverrides;
|
|
565
|
-
if (inheritsLayout !== undefined)
|
|
566
|
-
input.inheritsLayout = inheritsLayout;
|
|
567
|
-
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input });
|
|
568
|
-
if (!data.updatePageLayout) {
|
|
569
|
-
return {
|
|
570
|
-
content: [
|
|
571
|
-
{ type: "text", text: "Page not found or update failed" },
|
|
572
|
-
],
|
|
573
|
-
isError: true,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
return jsonText(response, data.updatePageLayout, pageMinimal);
|
|
577
|
-
});
|
|
45
|
+
bindSharedTool(server, updatePageLayoutTool, sharedOps);
|
|
578
46
|
// ─── Block Helper Tools (read-modify-write) ─────────────────
|
|
579
|
-
server
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
content: z
|
|
584
|
-
.record(z.string(), z.unknown())
|
|
585
|
-
.describe("Language-keyed content: { en: { title: '...' }, pl: { title: '...' } }"),
|
|
586
|
-
settings: z.record(z.string(), z.unknown()).optional(),
|
|
587
|
-
style: z.record(z.string(), z.unknown()).optional(),
|
|
588
|
-
})),
|
|
589
|
-
layoutPosition: z
|
|
590
|
-
.string()
|
|
591
|
-
.trim()
|
|
592
|
-
.min(1)
|
|
593
|
-
.optional()
|
|
594
|
-
.describe("Layout position (e.g. 'header', 'footer') to add a layout block; omit for a content block"),
|
|
595
|
-
position: z
|
|
596
|
-
.number()
|
|
597
|
-
.optional()
|
|
598
|
-
.describe("0-based position to insert at (default: end). Content blocks only - layout blocks are always appended within their position"),
|
|
599
|
-
response: responseModeSchema,
|
|
600
|
-
}, async ({ pageId, block, layoutPosition, position, response }) => {
|
|
601
|
-
// Fetch current page
|
|
602
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
603
|
-
if (!pageData.page) {
|
|
604
|
-
return {
|
|
605
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
606
|
-
isError: true,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
// Get site config for languages
|
|
610
|
-
const configData = await client.query(SITE_CONFIG_QUERY);
|
|
611
|
-
const defaultLanguage = configData.siteConfig?.defaultLanguage ?? "en";
|
|
612
|
-
const enabledLanguages = configData.siteConfig?.enabledLanguages ?? [
|
|
613
|
-
defaultLanguage,
|
|
614
|
-
];
|
|
615
|
-
// Build translations object
|
|
616
|
-
const translations = {};
|
|
617
|
-
for (const lang of enabledLanguages) {
|
|
618
|
-
translations[lang] = {
|
|
619
|
-
status: block.content[lang] ? "completed" : "pending",
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
const newBlockId = crypto.randomUUID();
|
|
623
|
-
if (layoutPosition) {
|
|
624
|
-
// Layout block — add to layoutBlocks via updatePageLayout
|
|
625
|
-
const existingLayoutBlocks = pageData.page.layoutBlocks || [];
|
|
626
|
-
// Append after existing blocks in same position
|
|
627
|
-
const maxOrder = existingLayoutBlocks
|
|
628
|
-
.filter((b) => b.position === layoutPosition)
|
|
629
|
-
.reduce((max, b) => Math.max(max, b.order), -1);
|
|
630
|
-
const newLayoutBlock = {
|
|
631
|
-
id: newBlockId,
|
|
632
|
-
type: block.type,
|
|
633
|
-
position: layoutPosition,
|
|
634
|
-
order: maxOrder + 1,
|
|
635
|
-
isActive: true,
|
|
636
|
-
content: block.content,
|
|
637
|
-
settings: block.settings,
|
|
638
|
-
style: block.style,
|
|
639
|
-
translations,
|
|
640
|
-
defaultLanguage,
|
|
641
|
-
};
|
|
642
|
-
const layoutBlocks = [...existingLayoutBlocks, newLayoutBlock];
|
|
643
|
-
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
644
|
-
if (!data.updatePageLayout) {
|
|
645
|
-
return {
|
|
646
|
-
content: [
|
|
647
|
-
{
|
|
648
|
-
type: "text",
|
|
649
|
-
text: `Failed to update layout while adding block to page '${pageId}'`,
|
|
650
|
-
},
|
|
651
|
-
],
|
|
652
|
-
isError: true,
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
return jsonText(response, { blockId: newBlockId, page: data.updatePageLayout }, () => pageBlockMinimal(data.updatePageLayout, newBlockId));
|
|
656
|
-
}
|
|
657
|
-
else {
|
|
658
|
-
// Content block — add to blocks via savePage
|
|
659
|
-
const newBlock = {
|
|
660
|
-
id: newBlockId,
|
|
661
|
-
type: block.type,
|
|
662
|
-
content: block.content,
|
|
663
|
-
settings: block.settings,
|
|
664
|
-
style: block.style,
|
|
665
|
-
translations,
|
|
666
|
-
defaultLanguage,
|
|
667
|
-
};
|
|
668
|
-
const blocks = [...pageData.page.blocks];
|
|
669
|
-
if (position !== undefined &&
|
|
670
|
-
position >= 0 &&
|
|
671
|
-
position < blocks.length) {
|
|
672
|
-
blocks.splice(position, 0, newBlock);
|
|
673
|
-
}
|
|
674
|
-
else {
|
|
675
|
-
blocks.push(newBlock);
|
|
676
|
-
}
|
|
677
|
-
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
678
|
-
input: {
|
|
679
|
-
id: pageId,
|
|
680
|
-
name: pageData.page.name,
|
|
681
|
-
slug: toRelativeSlug(pageData.page.slug),
|
|
682
|
-
parentId: pageData.page.parentId ?? undefined,
|
|
683
|
-
blocks,
|
|
684
|
-
expectedVersion: expectedVersionOf(pageData.page),
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
return jsonText(response, { blockId: newBlockId, page: data.savePage }, () => pageBlockMinimal(data.savePage, newBlockId));
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
server.tool("update_block_content", "Update a specific block's content on a page. Merges with existing content. Works for both content and layout blocks. Returns a minimal ack by default ({pageId, blockId, hasUnpublishedChanges, updatedAt}); pass response='full' for the full mutation response.", {
|
|
691
|
-
pageId: z.string().describe("Page ID"),
|
|
692
|
-
blockId: z.string().describe("Block instance ID (UUID) to update"),
|
|
693
|
-
content: z
|
|
694
|
-
.record(z.string(), z.unknown())
|
|
695
|
-
.describe("Content to merge: { en: { title: 'New Title' } }"),
|
|
696
|
-
settings: z.record(z.string(), z.unknown()).optional(),
|
|
697
|
-
response: responseModeSchema,
|
|
698
|
-
}, async ({ pageId, blockId, content, settings, response }) => {
|
|
699
|
-
// Fetch current page
|
|
700
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
701
|
-
if (!pageData.page) {
|
|
702
|
-
return {
|
|
703
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
704
|
-
isError: true,
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
const page = pageData.page;
|
|
708
|
-
// Search in content blocks first, then layout blocks
|
|
709
|
-
const contentIdx = page.blocks.findIndex((b) => b.id === blockId);
|
|
710
|
-
const layoutIdx = contentIdx === -1
|
|
711
|
-
? (page.layoutBlocks || []).findIndex((b) => b.id === blockId)
|
|
712
|
-
: -1;
|
|
713
|
-
if (contentIdx === -1 && layoutIdx === -1) {
|
|
714
|
-
return {
|
|
715
|
-
content: [
|
|
716
|
-
{
|
|
717
|
-
type: "text",
|
|
718
|
-
text: `Block '${blockId}' not found on page (checked both content and layout blocks)`,
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
isError: true,
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
const isLayout = layoutIdx !== -1;
|
|
725
|
-
const targetArray = isLayout
|
|
726
|
-
? [...(page.layoutBlocks || [])]
|
|
727
|
-
: [...page.blocks];
|
|
728
|
-
const targetIndex = isLayout ? layoutIdx : contentIdx;
|
|
729
|
-
// Merge content
|
|
730
|
-
const existingBlock = { ...targetArray[targetIndex] };
|
|
731
|
-
const mergedContent = { ...(existingBlock.content ?? {}) };
|
|
732
|
-
for (const [lang, langContent] of Object.entries(content)) {
|
|
733
|
-
if (typeof langContent === "object" &&
|
|
734
|
-
langContent !== null &&
|
|
735
|
-
typeof mergedContent[lang] === "object" &&
|
|
736
|
-
mergedContent[lang] !== null) {
|
|
737
|
-
mergedContent[lang] = {
|
|
738
|
-
...mergedContent[lang],
|
|
739
|
-
...langContent,
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
mergedContent[lang] = langContent;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
existingBlock.content = mergedContent;
|
|
747
|
-
if (settings) {
|
|
748
|
-
existingBlock.settings = {
|
|
749
|
-
...(existingBlock.settings ?? {}),
|
|
750
|
-
...settings,
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
// Update translation status
|
|
754
|
-
if (existingBlock.translations) {
|
|
755
|
-
for (const lang of Object.keys(content)) {
|
|
756
|
-
if (existingBlock.translations[lang]) {
|
|
757
|
-
existingBlock.translations[lang] = { status: "completed" };
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
targetArray[targetIndex] = existingBlock;
|
|
762
|
-
if (isLayout) {
|
|
763
|
-
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks: targetArray } });
|
|
764
|
-
if (!data.updatePageLayout) {
|
|
765
|
-
return {
|
|
766
|
-
content: [
|
|
767
|
-
{
|
|
768
|
-
type: "text",
|
|
769
|
-
text: `Failed to update layout while editing block '${blockId}' on page '${pageId}'`,
|
|
770
|
-
},
|
|
771
|
-
],
|
|
772
|
-
isError: true,
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
779
|
-
input: {
|
|
780
|
-
id: pageId,
|
|
781
|
-
name: page.name,
|
|
782
|
-
slug: toRelativeSlug(page.slug),
|
|
783
|
-
parentId: page.parentId ?? undefined,
|
|
784
|
-
blocks: targetArray,
|
|
785
|
-
expectedVersion: expectedVersionOf(page),
|
|
786
|
-
},
|
|
787
|
-
});
|
|
788
|
-
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
server.tool("patch_block_content", "Apply surgical HTML edits (insert_before/insert_after/replace_section) to a block's localized content string (e.g. a docs-article HTML body) without re-sending the full content. Use this for small edits where you have a unique anchor substring in the existing HTML - it's ~10x cheaper in tokens than update_block_content and rejects any op whose marker is missing or ambiguous. For replace_section: startMarker is inclusive, endMarker is exclusive.", {
|
|
792
|
-
pageId: z.string().describe("Page ID"),
|
|
793
|
-
blockId: z
|
|
794
|
-
.string()
|
|
795
|
-
.describe("Block instance ID (UUID) on the page. Layout blocks (header/footer) aren't supported - use update_block_content for those."),
|
|
796
|
-
locale: z
|
|
797
|
-
.string()
|
|
798
|
-
.describe("Language code matching the workspace's enabledLanguages (e.g. 'en', 'pl', 'zh'). Check get_site_config first if unsure."),
|
|
799
|
-
fieldPath: z
|
|
800
|
-
.string()
|
|
801
|
-
.optional()
|
|
802
|
-
.describe("Field inside content[locale] to patch (default: 'content', the HTML body of docs-article blocks). Must resolve to a string."),
|
|
803
|
-
expectedVersion: z
|
|
804
|
-
.number()
|
|
805
|
-
.int()
|
|
806
|
-
.optional()
|
|
807
|
-
.describe("Optimistic-concurrency guard: the page `version` you last read (e.g. from get_page). If set and the page changed since, the patch is rejected with VERSION_CONFLICT instead of overwriting concurrent edits - refetch the page and retry."),
|
|
808
|
-
// `jsonPreprocess` matches the other tools in this file that accept
|
|
809
|
-
// complex inputs - MCP clients sometimes JSON-serialize nested
|
|
810
|
-
// arrays/objects instead of passing raw JSON. Without the preprocess
|
|
811
|
-
// those clients fail validation before the handler ever runs.
|
|
812
|
-
operations: z.preprocess(jsonPreprocess, z
|
|
813
|
-
.array(
|
|
814
|
-
// Discriminated on `op` so clients can't pass e.g. `insert_before`
|
|
815
|
-
// with a `startMarker` - the backend would reject, but it's
|
|
816
|
-
// cleaner (and cheaper) to fail at MCP boundary.
|
|
817
|
-
z
|
|
818
|
-
.discriminatedUnion("op", [
|
|
819
|
-
z
|
|
820
|
-
.object({
|
|
821
|
-
op: z
|
|
822
|
-
.literal("insert_before")
|
|
823
|
-
.describe("Insert html immediately before `marker`"),
|
|
824
|
-
marker: z
|
|
825
|
-
.string()
|
|
826
|
-
.describe("Unique anchor substring. Must match exactly one location in the field (zero or multiple → error)."),
|
|
827
|
-
html: z.string().describe("HTML fragment to insert"),
|
|
828
|
-
})
|
|
829
|
-
.strict(),
|
|
830
|
-
z
|
|
831
|
-
.object({
|
|
832
|
-
op: z
|
|
833
|
-
.literal("insert_after")
|
|
834
|
-
.describe("Insert html immediately after `marker`"),
|
|
835
|
-
marker: z
|
|
836
|
-
.string()
|
|
837
|
-
.describe("Unique anchor substring. Must match exactly one location in the field (zero or multiple → error)."),
|
|
838
|
-
html: z.string().describe("HTML fragment to insert"),
|
|
839
|
-
})
|
|
840
|
-
.strict(),
|
|
841
|
-
z
|
|
842
|
-
.object({
|
|
843
|
-
op: z
|
|
844
|
-
.literal("replace_section")
|
|
845
|
-
.describe("Replace everything from startMarker (inclusive) up to endMarker (exclusive)"),
|
|
846
|
-
startMarker: z
|
|
847
|
-
.string()
|
|
848
|
-
.describe("Inclusive lower bound. Must match exactly one location."),
|
|
849
|
-
endMarker: z
|
|
850
|
-
.string()
|
|
851
|
-
.describe("Exclusive upper bound. Must appear exactly once after startMarker."),
|
|
852
|
-
html: z
|
|
853
|
-
.string()
|
|
854
|
-
.describe("HTML fragment to replace the section with"),
|
|
855
|
-
})
|
|
856
|
-
.strict(),
|
|
857
|
-
])
|
|
858
|
-
.describe("A single patch op. Ops apply in order; any failure aborts the whole patch (no half-applied state)."))
|
|
859
|
-
.min(1)
|
|
860
|
-
.describe("Ordered list of patch operations. Apply smallest first - each op must still find its markers in the string after previous ops have applied.")),
|
|
861
|
-
}, async ({ pageId, blockId, locale, fieldPath, operations, expectedVersion, }) => {
|
|
862
|
-
// Zod's discriminatedUnion + .strict() already shapes each op
|
|
863
|
-
// exactly like the backend's PatchOperationInput (insert_*: op,
|
|
864
|
-
// marker, html; replace_section: op, startMarker, endMarker, html).
|
|
865
|
-
// Pass the array straight through - a manual map here is both
|
|
866
|
-
// redundant AND unsafe (a switch without an exhaustiveness check
|
|
867
|
-
// could return undefined on an unexpected op, which would then
|
|
868
|
-
// serialize as null in the mutation variables and blow up server-side
|
|
869
|
-
// with a hard-to-trace error).
|
|
870
|
-
const data = await client.query(PATCH_BLOCK_CONTENT_MUTATION, {
|
|
871
|
-
input: {
|
|
872
|
-
pageId,
|
|
873
|
-
blockId,
|
|
874
|
-
locale,
|
|
875
|
-
...(fieldPath ? { fieldPath } : {}),
|
|
876
|
-
...(expectedVersion !== undefined ? { expectedVersion } : {}),
|
|
877
|
-
operations,
|
|
878
|
-
},
|
|
879
|
-
});
|
|
880
|
-
if (!data.patchBlockContent) {
|
|
881
|
-
return {
|
|
882
|
-
content: [
|
|
883
|
-
{
|
|
884
|
-
type: "text",
|
|
885
|
-
text: "Page not found or patch failed",
|
|
886
|
-
},
|
|
887
|
-
],
|
|
888
|
-
isError: true,
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
// Compact JSON: patch_block_content is ack-only (no pretty-print
|
|
892
|
-
// needed), matching the compact shape used by minimal-mode returns.
|
|
893
|
-
const patched = data.patchBlockContent;
|
|
894
|
-
return {
|
|
895
|
-
content: [
|
|
896
|
-
{
|
|
897
|
-
type: "text",
|
|
898
|
-
text: JSON.stringify({
|
|
899
|
-
id: patched.id,
|
|
900
|
-
slug: patched.slug,
|
|
901
|
-
hasUnpublishedChanges: Boolean(patched.hasUnpublishedContentChanges) ||
|
|
902
|
-
Boolean(patched.hasUnpublishedLayoutChanges),
|
|
903
|
-
updatedAt: patched.updatedAt,
|
|
904
|
-
}),
|
|
905
|
-
},
|
|
906
|
-
],
|
|
907
|
-
};
|
|
908
|
-
});
|
|
909
|
-
server.tool("remove_block_from_page", "Remove a specific block from a page by its instance ID. Works for both content and layout blocks. Returns a minimal ack by default ({pageId, blockId, hasUnpublishedChanges, updatedAt}); pass response='full' for the full mutation response.", {
|
|
910
|
-
pageId: z.string().describe("Page ID"),
|
|
911
|
-
blockId: z.string().describe("Block instance ID (UUID) to remove"),
|
|
912
|
-
response: responseModeSchema,
|
|
913
|
-
}, async ({ pageId, blockId, response }) => {
|
|
914
|
-
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
915
|
-
if (!pageData.page) {
|
|
916
|
-
return {
|
|
917
|
-
content: [{ type: "text", text: "Page not found" }],
|
|
918
|
-
isError: true,
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
const page = pageData.page;
|
|
922
|
-
// Try content blocks first
|
|
923
|
-
const contentBlocks = page.blocks.filter((b) => b.id !== blockId);
|
|
924
|
-
if (contentBlocks.length < page.blocks.length) {
|
|
925
|
-
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
926
|
-
input: {
|
|
927
|
-
id: pageId,
|
|
928
|
-
name: page.name,
|
|
929
|
-
slug: toRelativeSlug(page.slug),
|
|
930
|
-
parentId: page.parentId ?? undefined,
|
|
931
|
-
blocks: contentBlocks,
|
|
932
|
-
expectedVersion: expectedVersionOf(page),
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
936
|
-
}
|
|
937
|
-
// Try layout blocks
|
|
938
|
-
const layoutBlocks = (page.layoutBlocks || []).filter((b) => b.id !== blockId);
|
|
939
|
-
if (layoutBlocks.length < (page.layoutBlocks || []).length) {
|
|
940
|
-
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
941
|
-
if (!data.updatePageLayout) {
|
|
942
|
-
return {
|
|
943
|
-
content: [
|
|
944
|
-
{
|
|
945
|
-
type: "text",
|
|
946
|
-
text: `Failed to update layout while removing block '${blockId}' from page '${pageId}'`,
|
|
947
|
-
},
|
|
948
|
-
],
|
|
949
|
-
isError: true,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
953
|
-
}
|
|
954
|
-
return {
|
|
955
|
-
content: [
|
|
956
|
-
{
|
|
957
|
-
type: "text",
|
|
958
|
-
text: `Block '${blockId}' not found on page (checked both content and layout blocks)`,
|
|
959
|
-
},
|
|
960
|
-
],
|
|
961
|
-
isError: true,
|
|
962
|
-
};
|
|
963
|
-
});
|
|
47
|
+
bindSharedTool(server, addBlockToPageTool, sharedOps);
|
|
48
|
+
bindSharedTool(server, updateBlockContentTool, sharedOps);
|
|
49
|
+
bindSharedTool(server, patchBlockContentTool, sharedOps);
|
|
50
|
+
bindSharedTool(server, removeBlockFromPageTool, sharedOps);
|
|
964
51
|
// ─── Form Tools ──────────────────────────────────────────────
|
|
965
|
-
server
|
|
966
|
-
|
|
967
|
-
.enum(["draft", "published", "archived"])
|
|
968
|
-
.optional()
|
|
969
|
-
.describe("Filter by form status"),
|
|
970
|
-
skip: z
|
|
971
|
-
.number()
|
|
972
|
-
.optional()
|
|
973
|
-
.default(0)
|
|
974
|
-
.describe("Number of items to skip"),
|
|
975
|
-
limit: z
|
|
976
|
-
.number()
|
|
977
|
-
.optional()
|
|
978
|
-
.default(50)
|
|
979
|
-
.describe("Max items to return (default 50)"),
|
|
980
|
-
}, async ({ status, skip, limit }) => {
|
|
981
|
-
const data = await client.query(FORMS_QUERY, {
|
|
982
|
-
status: status || undefined,
|
|
983
|
-
skip,
|
|
984
|
-
limit,
|
|
985
|
-
});
|
|
986
|
-
return {
|
|
987
|
-
content: [
|
|
988
|
-
{ type: "text", text: JSON.stringify(data.forms, null, 2) },
|
|
989
|
-
],
|
|
990
|
-
};
|
|
991
|
-
});
|
|
992
|
-
server.tool("get_form", "Get full form details including fields, settings, and i18n content.", {
|
|
993
|
-
formId: z.string().describe("Form ID"),
|
|
994
|
-
}, async ({ formId }) => {
|
|
995
|
-
const data = await client.query(FORM_BY_ID_QUERY, { formId });
|
|
996
|
-
if (!data.form) {
|
|
997
|
-
return {
|
|
998
|
-
content: [{ type: "text", text: "Form not found" }],
|
|
999
|
-
isError: true,
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
return {
|
|
1003
|
-
content: [
|
|
1004
|
-
{ type: "text", text: JSON.stringify(data.form, null, 2) },
|
|
1005
|
-
],
|
|
1006
|
-
};
|
|
1007
|
-
});
|
|
52
|
+
bindSharedTool(server, listFormsTool, sharedOps);
|
|
53
|
+
bindSharedTool(server, getFormTool, sharedOps);
|
|
1008
54
|
bindSharedTool(server, createFormTool, sharedOps);
|
|
1009
|
-
server
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
.enum(["draft", "published", "archived"])
|
|
1016
|
-
.optional()
|
|
1017
|
-
.describe("New status"),
|
|
1018
|
-
fields: z
|
|
1019
|
-
.preprocess(jsonPreprocess, z.array(z.object({
|
|
1020
|
-
id: z.string(),
|
|
1021
|
-
name: z.string(),
|
|
1022
|
-
fieldType: z
|
|
1023
|
-
.enum([
|
|
1024
|
-
"text",
|
|
1025
|
-
"email",
|
|
1026
|
-
"password",
|
|
1027
|
-
"textarea",
|
|
1028
|
-
"number",
|
|
1029
|
-
"phone",
|
|
1030
|
-
"url",
|
|
1031
|
-
"date",
|
|
1032
|
-
"datetime",
|
|
1033
|
-
"select",
|
|
1034
|
-
"multiselect",
|
|
1035
|
-
"checkbox",
|
|
1036
|
-
"radio",
|
|
1037
|
-
"file",
|
|
1038
|
-
"hidden",
|
|
1039
|
-
])
|
|
1040
|
-
.optional(),
|
|
1041
|
-
label: z.record(z.string(), z.unknown()).optional(),
|
|
1042
|
-
placeholder: z.record(z.string(), z.unknown()).optional(),
|
|
1043
|
-
helpText: z.record(z.string(), z.unknown()).optional(),
|
|
1044
|
-
defaultValue: z.string().optional(),
|
|
1045
|
-
validation: z
|
|
1046
|
-
.object({
|
|
1047
|
-
required: z.boolean().optional(),
|
|
1048
|
-
minLength: z.number().optional(),
|
|
1049
|
-
maxLength: z.number().optional(),
|
|
1050
|
-
minValue: z.number().optional(),
|
|
1051
|
-
maxValue: z.number().optional(),
|
|
1052
|
-
pattern: z.string().optional(),
|
|
1053
|
-
customMessage: z.string().optional(),
|
|
1054
|
-
})
|
|
1055
|
-
.optional(),
|
|
1056
|
-
options: z
|
|
1057
|
-
.array(z.object({
|
|
1058
|
-
value: z.string(),
|
|
1059
|
-
label: z.record(z.string(), z.unknown()).optional(),
|
|
1060
|
-
disabled: z.boolean().optional(),
|
|
1061
|
-
}))
|
|
1062
|
-
.optional(),
|
|
1063
|
-
width: z.enum(["full", "half", "third"]).optional(),
|
|
1064
|
-
order: z.number().optional(),
|
|
1065
|
-
showIf: z.record(z.string(), z.unknown()).optional(),
|
|
1066
|
-
})))
|
|
1067
|
-
.optional()
|
|
1068
|
-
.describe("Replacement fields array"),
|
|
1069
|
-
settings: z
|
|
1070
|
-
.preprocess(jsonPreprocess, z.object({
|
|
1071
|
-
actionType: z
|
|
1072
|
-
.enum(["login", "register", "newsletter", "contact", "custom"])
|
|
1073
|
-
.optional(),
|
|
1074
|
-
webhookUrl: z.string().optional(),
|
|
1075
|
-
emailRecipients: z.array(z.string()).optional(),
|
|
1076
|
-
newsletterListId: z.string().optional(),
|
|
1077
|
-
submitButtonLabel: z.record(z.string(), z.unknown()).optional(),
|
|
1078
|
-
successMessage: z.record(z.string(), z.unknown()).optional(),
|
|
1079
|
-
errorMessage: z.record(z.string(), z.unknown()).optional(),
|
|
1080
|
-
redirectUrl: z.string().optional(),
|
|
1081
|
-
enableCaptcha: z.boolean().optional(),
|
|
1082
|
-
requireLogin: z.boolean().optional(),
|
|
1083
|
-
saveSubmissions: z.boolean().optional(),
|
|
1084
|
-
sendEmailNotification: z.boolean().optional(),
|
|
1085
|
-
emailConfigurationId: z.string().optional(),
|
|
1086
|
-
}))
|
|
1087
|
-
.optional()
|
|
1088
|
-
.describe("Updated settings"),
|
|
1089
|
-
response: responseModeSchema,
|
|
1090
|
-
}, async ({ formId, name, slug, description, status, fields, settings, response, }) => {
|
|
1091
|
-
const input = {};
|
|
1092
|
-
if (name !== undefined)
|
|
1093
|
-
input.name = name;
|
|
1094
|
-
if (slug !== undefined)
|
|
1095
|
-
input.slug = slug;
|
|
1096
|
-
if (description !== undefined)
|
|
1097
|
-
input.description = description;
|
|
1098
|
-
if (status !== undefined)
|
|
1099
|
-
input.status = status;
|
|
1100
|
-
if (fields !== undefined)
|
|
1101
|
-
input.fields = fields;
|
|
1102
|
-
if (settings !== undefined)
|
|
1103
|
-
input.settings = settings;
|
|
1104
|
-
const data = await client.query(UPDATE_FORM_MUTATION, { formId, input });
|
|
1105
|
-
if (!data.updateForm) {
|
|
1106
|
-
return {
|
|
1107
|
-
content: [
|
|
1108
|
-
{ type: "text", text: "Form not found or update failed" },
|
|
1109
|
-
],
|
|
1110
|
-
isError: true,
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
return jsonText(response, data.updateForm, formMinimal);
|
|
1114
|
-
});
|
|
1115
|
-
server.tool("delete_form", "Permanently delete a form and all its submissions.", {
|
|
1116
|
-
formId: z.string().describe("Form ID to delete"),
|
|
1117
|
-
}, async ({ formId }) => {
|
|
1118
|
-
const data = await client.query(DELETE_FORM_MUTATION, { formId });
|
|
1119
|
-
return {
|
|
1120
|
-
content: [
|
|
1121
|
-
{
|
|
1122
|
-
type: "text",
|
|
1123
|
-
text: data.deleteForm
|
|
1124
|
-
? "Form deleted successfully"
|
|
1125
|
-
: "Failed to delete form",
|
|
1126
|
-
},
|
|
1127
|
-
],
|
|
1128
|
-
};
|
|
1129
|
-
});
|
|
1130
|
-
server.tool("list_form_submissions", "List form submissions with optional filters and pagination.", {
|
|
1131
|
-
formId: z.string().optional().describe("Filter by form ID"),
|
|
1132
|
-
status: z
|
|
1133
|
-
.enum(["pending", "processed", "spam", "archived"])
|
|
1134
|
-
.optional()
|
|
1135
|
-
.describe("Filter by submission status"),
|
|
1136
|
-
skip: z.number().optional().default(0),
|
|
1137
|
-
limit: z.number().optional().default(50),
|
|
1138
|
-
}, async ({ formId, status, skip, limit }) => {
|
|
1139
|
-
const data = await client.query(FORM_SUBMISSIONS_QUERY, {
|
|
1140
|
-
formId: formId || undefined,
|
|
1141
|
-
status: status || undefined,
|
|
1142
|
-
skip,
|
|
1143
|
-
limit,
|
|
1144
|
-
});
|
|
1145
|
-
return {
|
|
1146
|
-
content: [
|
|
1147
|
-
{
|
|
1148
|
-
type: "text",
|
|
1149
|
-
text: JSON.stringify(data.formSubmissions, null, 2),
|
|
1150
|
-
},
|
|
1151
|
-
],
|
|
1152
|
-
};
|
|
1153
|
-
});
|
|
1154
|
-
server.tool("get_form_submission", "Get a specific form submission by ID.", {
|
|
1155
|
-
submissionId: z.string().describe("Submission ID"),
|
|
1156
|
-
}, async ({ submissionId }) => {
|
|
1157
|
-
const data = await client.query(FORM_SUBMISSION_BY_ID_QUERY, { submissionId });
|
|
1158
|
-
if (!data.formSubmission) {
|
|
1159
|
-
return {
|
|
1160
|
-
content: [{ type: "text", text: "Submission not found" }],
|
|
1161
|
-
isError: true,
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
return {
|
|
1165
|
-
content: [
|
|
1166
|
-
{
|
|
1167
|
-
type: "text",
|
|
1168
|
-
text: JSON.stringify(data.formSubmission, null, 2),
|
|
1169
|
-
},
|
|
1170
|
-
],
|
|
1171
|
-
};
|
|
1172
|
-
});
|
|
1173
|
-
server.tool("update_form_submission_status", "Update a form submission's status (pending, processed, spam, archived).", {
|
|
1174
|
-
submissionId: z.string().describe("Submission ID"),
|
|
1175
|
-
status: z
|
|
1176
|
-
.enum(["pending", "processed", "spam", "archived"])
|
|
1177
|
-
.describe("New status"),
|
|
1178
|
-
}, async ({ submissionId, status }) => {
|
|
1179
|
-
const data = await client.query(UPDATE_FORM_SUBMISSION_STATUS_MUTATION, { submissionId, status });
|
|
1180
|
-
return {
|
|
1181
|
-
content: [
|
|
1182
|
-
{
|
|
1183
|
-
type: "text",
|
|
1184
|
-
text: data.updateFormSubmissionStatus
|
|
1185
|
-
? "Submission status updated"
|
|
1186
|
-
: "Failed to update submission status",
|
|
1187
|
-
},
|
|
1188
|
-
],
|
|
1189
|
-
};
|
|
1190
|
-
});
|
|
1191
|
-
server.tool("delete_form_submission", "Permanently delete a form submission.", {
|
|
1192
|
-
submissionId: z.string().describe("Submission ID to delete"),
|
|
1193
|
-
}, async ({ submissionId }) => {
|
|
1194
|
-
const data = await client.query(DELETE_FORM_SUBMISSION_MUTATION, { submissionId });
|
|
1195
|
-
return {
|
|
1196
|
-
content: [
|
|
1197
|
-
{
|
|
1198
|
-
type: "text",
|
|
1199
|
-
text: data.deleteFormSubmission
|
|
1200
|
-
? "Submission deleted successfully"
|
|
1201
|
-
: "Failed to delete submission",
|
|
1202
|
-
},
|
|
1203
|
-
],
|
|
1204
|
-
};
|
|
1205
|
-
});
|
|
55
|
+
bindSharedTool(server, updateFormTool, sharedOps);
|
|
56
|
+
bindSharedTool(server, deleteFormTool, sharedOps);
|
|
57
|
+
bindSharedTool(server, listFormSubmissionsTool, sharedOps);
|
|
58
|
+
bindSharedTool(server, getFormSubmissionTool, sharedOps);
|
|
59
|
+
bindSharedTool(server, updateFormSubmissionStatusTool, sharedOps);
|
|
60
|
+
bindSharedTool(server, deleteFormSubmissionTool, sharedOps);
|
|
1206
61
|
// ─── Model Tools (Custom Data Models) ────────────────────────
|
|
1207
|
-
|
|
1208
|
-
server
|
|
1209
|
-
const data = await client.query(MODEL_DEFINITIONS_QUERY);
|
|
1210
|
-
return {
|
|
1211
|
-
content: [
|
|
1212
|
-
{
|
|
1213
|
-
type: "text",
|
|
1214
|
-
text: JSON.stringify(data.modelDefinitions, null, 2),
|
|
1215
|
-
},
|
|
1216
|
-
],
|
|
1217
|
-
};
|
|
1218
|
-
});
|
|
1219
|
-
server.tool("get_model", "Get a ModelDefinition by id (24-hex ObjectId) or by slug. Returns full definition including fields, statusField, and defaultSort.", {
|
|
1220
|
-
idOrSlug: z.string().describe("Model id (ObjectId) or slug"),
|
|
1221
|
-
}, async ({ idOrSlug }) => {
|
|
1222
|
-
if (OBJECT_ID_RE.test(idOrSlug)) {
|
|
1223
|
-
const data = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: idOrSlug });
|
|
1224
|
-
if (data.modelDefinition) {
|
|
1225
|
-
return {
|
|
1226
|
-
content: [
|
|
1227
|
-
{
|
|
1228
|
-
type: "text",
|
|
1229
|
-
text: JSON.stringify(data.modelDefinition, null, 2),
|
|
1230
|
-
},
|
|
1231
|
-
],
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
// Fallback: resolve slug → id via a lightweight list (id + slug only),
|
|
1236
|
-
// then fetch the full definition for that single model. Avoids
|
|
1237
|
-
// pulling every model's fields when only one is needed.
|
|
1238
|
-
const index = await client.query(MODEL_DEFINITIONS_BY_SLUG_INDEX_QUERY);
|
|
1239
|
-
const match = index.modelDefinitions.find((m) => m.slug === idOrSlug);
|
|
1240
|
-
if (!match) {
|
|
1241
|
-
return {
|
|
1242
|
-
content: [
|
|
1243
|
-
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1244
|
-
],
|
|
1245
|
-
isError: true,
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
const full = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: match.id });
|
|
1249
|
-
if (!full.modelDefinition) {
|
|
1250
|
-
return {
|
|
1251
|
-
content: [
|
|
1252
|
-
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1253
|
-
],
|
|
1254
|
-
isError: true,
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
return {
|
|
1258
|
-
content: [
|
|
1259
|
-
{
|
|
1260
|
-
type: "text",
|
|
1261
|
-
text: JSON.stringify(full.modelDefinition, null, 2),
|
|
1262
|
-
},
|
|
1263
|
-
],
|
|
1264
|
-
};
|
|
1265
|
-
});
|
|
62
|
+
bindSharedTool(server, listModelsTool, sharedOps);
|
|
63
|
+
bindSharedTool(server, getModelTool, sharedOps);
|
|
1266
64
|
bindSharedTool(server, createModelTool, sharedOps);
|
|
1267
65
|
bindSharedTool(server, updateModelTool, sharedOps);
|
|
1268
|
-
server
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const data = await client.query(DELETE_MODEL_DEFINITION_MUTATION, { id });
|
|
1272
|
-
return {
|
|
1273
|
-
content: [
|
|
1274
|
-
{
|
|
1275
|
-
type: "text",
|
|
1276
|
-
text: data.deleteModelDefinition
|
|
1277
|
-
? "Model deleted (records cascaded)"
|
|
1278
|
-
: "Failed to delete model",
|
|
1279
|
-
},
|
|
1280
|
-
],
|
|
1281
|
-
isError: !data.deleteModelDefinition,
|
|
1282
|
-
};
|
|
1283
|
-
});
|
|
1284
|
-
server.tool("list_records", "List records of a model with filter, sort, and pagination. Filter is a MongoDB-style plain object. Sort is a string (e.g. '-createdAt' for desc). Populate loads relations by field key.", {
|
|
1285
|
-
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1286
|
-
filter: z
|
|
1287
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1288
|
-
.optional()
|
|
1289
|
-
.describe("Plain object filter, e.g. {status: 'active'}"),
|
|
1290
|
-
limit: z.number().int().optional().default(50),
|
|
1291
|
-
offset: z.number().int().optional().default(0),
|
|
1292
|
-
sort: z
|
|
1293
|
-
.string()
|
|
1294
|
-
.optional()
|
|
1295
|
-
.describe("Sort expression, e.g. 'createdAt' or '-updatedAt'"),
|
|
1296
|
-
populate: z
|
|
1297
|
-
.array(z.string())
|
|
1298
|
-
.optional()
|
|
1299
|
-
.describe("Field keys of type=relation to populate"),
|
|
1300
|
-
}, async ({ modelId, filter, limit, offset, sort, populate }) => {
|
|
1301
|
-
const data = await client.query(MODEL_RECORDS_QUERY, {
|
|
1302
|
-
modelId,
|
|
1303
|
-
filter,
|
|
1304
|
-
limit,
|
|
1305
|
-
offset,
|
|
1306
|
-
sort,
|
|
1307
|
-
populate,
|
|
1308
|
-
});
|
|
1309
|
-
return {
|
|
1310
|
-
content: [
|
|
1311
|
-
{
|
|
1312
|
-
type: "text",
|
|
1313
|
-
text: JSON.stringify(data.modelRecords, null, 2),
|
|
1314
|
-
},
|
|
1315
|
-
],
|
|
1316
|
-
};
|
|
1317
|
-
});
|
|
1318
|
-
server.tool("get_record", "Get a single record by id.", {
|
|
1319
|
-
id: z.string().describe("Record id (ObjectId)"),
|
|
1320
|
-
}, async ({ id }) => {
|
|
1321
|
-
const data = await client.query(MODEL_RECORD_BY_ID_QUERY, { id });
|
|
1322
|
-
if (!data.modelRecord) {
|
|
1323
|
-
return {
|
|
1324
|
-
content: [{ type: "text", text: "Record not found" }],
|
|
1325
|
-
isError: true,
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
content: [
|
|
1330
|
-
{
|
|
1331
|
-
type: "text",
|
|
1332
|
-
text: JSON.stringify(data.modelRecord, null, 2),
|
|
1333
|
-
},
|
|
1334
|
-
],
|
|
1335
|
-
};
|
|
1336
|
-
});
|
|
66
|
+
bindSharedTool(server, deleteModelTool, sharedOps);
|
|
67
|
+
bindSharedTool(server, listRecordsTool, sharedOps);
|
|
68
|
+
bindSharedTool(server, getRecordTool, sharedOps);
|
|
1337
69
|
bindSharedTool(server, createRecordTool, sharedOps);
|
|
1338
70
|
bindSharedTool(server, updateRecordTool, sharedOps);
|
|
1339
|
-
server
|
|
1340
|
-
|
|
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.", {
|
|
1356
|
-
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
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
|
-
};
|
|
1373
|
-
});
|
|
71
|
+
bindSharedTool(server, deleteRecordTool, sharedOps);
|
|
72
|
+
bindSharedTool(server, importRecordsTool, sharedOps);
|
|
1374
73
|
// ─── Commerce: Orders ────────────────────────────────────────
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
|
1412
|
-
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1413
|
-
.optional()
|
|
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
|
|
1432
|
-
.string()
|
|
1433
|
-
.optional()
|
|
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
|
-
});
|
|
74
|
+
bindSharedTool(server, listOrdersTool, sharedOps);
|
|
75
|
+
bindSharedTool(server, getOrderTool, sharedOps);
|
|
76
|
+
bindSharedTool(server, getOrderPipelineTool, sharedOps);
|
|
77
|
+
bindSharedTool(server, createManualOrderTool, sharedOps);
|
|
78
|
+
bindSharedTool(server, editOrderTool, sharedOps);
|
|
79
|
+
bindSharedTool(server, updateOrderDetailsTool, sharedOps);
|
|
80
|
+
bindSharedTool(server, markOrderPaidTool, sharedOps);
|
|
81
|
+
bindSharedTool(server, recordOrderPaymentTool, sharedOps);
|
|
82
|
+
bindSharedTool(server, refundOrderTool, sharedOps);
|
|
83
|
+
bindSharedTool(server, cancelOrderTool, sharedOps);
|
|
84
|
+
bindSharedTool(server, transitionOrderFulfillmentTool, sharedOps);
|
|
85
|
+
bindSharedTool(server, setOrderPipelineStageTool, sharedOps);
|
|
86
|
+
bindSharedTool(server, recordOrderInvoiceTool, sharedOps);
|
|
1629
87
|
// ─── Commerce: Carts ─────────────────────────────────────────
|
|
1630
|
-
server
|
|
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
|
-
});
|
|
88
|
+
bindSharedTool(server, listCartsTool, sharedOps);
|
|
1645
89
|
// ─── Commerce: Discounts ─────────────────────────────────────
|
|
1646
|
-
server
|
|
1647
|
-
|
|
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
|
-
});
|
|
90
|
+
bindSharedTool(server, listDiscountsTool, sharedOps);
|
|
91
|
+
bindSharedTool(server, getDiscountTool, sharedOps);
|
|
1682
92
|
bindSharedTool(server, createDiscountTool, sharedOps);
|
|
1683
|
-
server
|
|
1684
|
-
|
|
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(),
|
|
1695
|
-
response: responseModeSchema,
|
|
1696
|
-
}, async ({ id, response, ...input }) => {
|
|
1697
|
-
if (Object.keys(input).length === 0) {
|
|
1698
|
-
return {
|
|
1699
|
-
content: [
|
|
1700
|
-
{
|
|
1701
|
-
type: "text",
|
|
1702
|
-
text: "Provide at least one field to update",
|
|
1703
|
-
},
|
|
1704
|
-
],
|
|
1705
|
-
isError: true,
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
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
|
-
});
|
|
93
|
+
bindSharedTool(server, updateDiscountTool, sharedOps);
|
|
94
|
+
bindSharedTool(server, setDiscountEnabledTool, sharedOps);
|
|
1719
95
|
// ─── Commerce: Products (catalog over model records) ─────────
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
};
|
|
1809
|
-
}
|
|
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
|
-
});
|
|
96
|
+
bindSharedTool(server, listProductsTool, sharedOps);
|
|
97
|
+
bindSharedTool(server, bulkUpdateProductsTool, sharedOps);
|
|
98
|
+
bindSharedTool(server, bulkDeleteProductsTool, sharedOps);
|
|
1834
99
|
// ─── Webhooks ────────────────────────────────────────────────
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
server
|
|
1841
|
-
|
|
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
|
-
};
|
|
1923
|
-
}
|
|
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
|
-
};
|
|
1933
|
-
});
|
|
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(),
|
|
1936
|
-
}, async ({ id }) => {
|
|
1937
|
-
const data = await client.query(ROTATE_WEBHOOK_SECRET_MUTATION, { workspaceId: client.workspaceId, id });
|
|
1938
|
-
return {
|
|
1939
|
-
content: [
|
|
1940
|
-
{
|
|
1941
|
-
type: "text",
|
|
1942
|
-
text: JSON.stringify(data.rotateWebhookSecret, null, 2),
|
|
1943
|
-
},
|
|
1944
|
-
],
|
|
1945
|
-
};
|
|
1946
|
-
});
|
|
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 });
|
|
1951
|
-
return {
|
|
1952
|
-
content: [
|
|
1953
|
-
{
|
|
1954
|
-
type: "text",
|
|
1955
|
-
text: data.deleteWebhookEndpoint
|
|
1956
|
-
? "Webhook deleted"
|
|
1957
|
-
: "Failed to delete webhook",
|
|
1958
|
-
},
|
|
1959
|
-
],
|
|
1960
|
-
isError: !data.deleteWebhookEndpoint,
|
|
1961
|
-
};
|
|
1962
|
-
});
|
|
100
|
+
bindSharedTool(server, listWebhooksTool, sharedOps);
|
|
101
|
+
bindSharedTool(server, listWebhookDeliveriesTool, sharedOps);
|
|
102
|
+
bindSharedTool(server, listWebhookEventTypesTool, sharedOps);
|
|
103
|
+
bindSharedTool(server, createWebhookTool, sharedOps);
|
|
104
|
+
bindSharedTool(server, updateWebhookTool, sharedOps);
|
|
105
|
+
bindSharedTool(server, rotateWebhookSecretTool, sharedOps);
|
|
106
|
+
bindSharedTool(server, deleteWebhookTool, sharedOps);
|
|
1963
107
|
// ─── Resources ───────────────────────────────────────────────
|
|
1964
108
|
server.resource("sitemap", "cmssy://sitemap", {
|
|
1965
109
|
description: "Full page tree as JSON — all pages with hierarchy",
|