@cmssy/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/graphql-client.d.ts +14 -0
- package/dist/graphql-client.d.ts.map +1 -0
- package/dist/graphql-client.js +88 -0
- package/dist/graphql-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/queries.d.ts +15 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +391 -0
- package/dist/queries.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +787 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PAGES_QUERY, PAGE_BY_ID_QUERY, PAGE_BY_SLUG_QUERY, WORKSPACE_BLOCKS_QUERY, WORKSPACE_BLOCK_BY_TYPE_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, MEDIA_ASSETS_QUERY, SAVE_PAGE_MUTATION, UPDATE_PAGE_SETTINGS_MUTATION, TOGGLE_PUBLISH_MUTATION, PUBLISH_PAGE_MUTATION, REMOVE_PAGE_MUTATION, UPDATE_PAGE_LAYOUT_MUTATION, } from "./queries.js";
|
|
4
|
+
export function createServer(client) {
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "cmssy",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
});
|
|
9
|
+
// ─── Workspace Block Registry (cached) ──────────────────────
|
|
10
|
+
let workspaceBlocksCache = null;
|
|
11
|
+
async function getWorkspaceBlocks() {
|
|
12
|
+
if (!workspaceBlocksCache) {
|
|
13
|
+
const data = await client.query(WORKSPACE_BLOCKS_QUERY);
|
|
14
|
+
workspaceBlocksCache = data.workspaceBlocks;
|
|
15
|
+
}
|
|
16
|
+
return workspaceBlocksCache;
|
|
17
|
+
}
|
|
18
|
+
async function findBlockDef(blockType) {
|
|
19
|
+
const blocks = await getWorkspaceBlocks();
|
|
20
|
+
return blocks.find((b) => b.blockType === blockType) ?? null;
|
|
21
|
+
}
|
|
22
|
+
async function validateBlockTypes(types) {
|
|
23
|
+
const wsBlocks = await getWorkspaceBlocks();
|
|
24
|
+
const available = wsBlocks.map((b) => b.blockType);
|
|
25
|
+
const invalid = types.filter((t) => !available.includes(t));
|
|
26
|
+
if (invalid.length > 0) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
error: `Unknown block type(s): ${invalid.join(", ")}. Available: ${available.join(", ")}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { valid: true };
|
|
33
|
+
}
|
|
34
|
+
// ─── Read Tools ──────────────────────────────────────────────
|
|
35
|
+
server.tool("list_pages", "List all pages in the workspace with hierarchy info (id, name, slug, published, parentId, pageType)", {}, async () => {
|
|
36
|
+
const data = await client.query(PAGES_QUERY);
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{ type: "text", text: JSON.stringify(data.pages, null, 2) },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
server.tool("get_page", "Get full page details with all blocks and i18n content. Provide either slug or id.", {
|
|
44
|
+
slug: z.string().optional().describe("Page slug (e.g. '/' or 'about')"),
|
|
45
|
+
id: z.string().optional().describe("Page ID"),
|
|
46
|
+
}, async ({ slug, id }) => {
|
|
47
|
+
if (!slug && !id) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: "Error: provide either 'slug' or 'id'",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let page;
|
|
59
|
+
if (id) {
|
|
60
|
+
const data = await client.query(PAGE_BY_ID_QUERY, { id });
|
|
61
|
+
page = data.pageById;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const data = await client.query(PAGE_BY_SLUG_QUERY, { slug });
|
|
65
|
+
page = data.page;
|
|
66
|
+
}
|
|
67
|
+
if (!page) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text: JSON.stringify(page, null, 2) },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
server.tool("list_block_types", "List all available block types with schemas, categories, and defaults", {}, async () => {
|
|
80
|
+
const data = await client.query(WORKSPACE_BLOCKS_QUERY);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify(data.workspaceBlocks, null, 2),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
server.tool("get_block_schema", "Get detailed schema for a specific block type (fields, types, validation, defaults)", {
|
|
91
|
+
blockType: z
|
|
92
|
+
.string()
|
|
93
|
+
.describe("Block type identifier (e.g. 'hero', 'text-block')"),
|
|
94
|
+
}, async ({ blockType }) => {
|
|
95
|
+
const data = await client.query(WORKSPACE_BLOCK_BY_TYPE_QUERY, { blockType });
|
|
96
|
+
if (!data.workspaceBlockByType) {
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Block type '${blockType}' not found`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: JSON.stringify(data.workspaceBlockByType, null, 2),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
server.tool("get_site_config", "Get site configuration: languages, navigation, header, footer, site name, enabled features", {}, async () => {
|
|
117
|
+
// Dynamically introspect header/footer types from the backend schema
|
|
118
|
+
const [headerSel, footerSel] = await Promise.all([
|
|
119
|
+
client.buildSelectionSet("SiteHeader"),
|
|
120
|
+
client.buildSelectionSet("SiteFooter"),
|
|
121
|
+
]);
|
|
122
|
+
const query = `
|
|
123
|
+
query SiteConfig {
|
|
124
|
+
siteConfig {
|
|
125
|
+
id
|
|
126
|
+
defaultLanguage
|
|
127
|
+
enabledLanguages
|
|
128
|
+
siteName
|
|
129
|
+
enabledFeatures
|
|
130
|
+
${headerSel ? `header { ${headerSel} }` : ""}
|
|
131
|
+
${footerSel ? `footer { ${footerSel} }` : ""}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
const data = await client.query(query);
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify(data.siteConfig, null, 2),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
server.tool("get_workspace_info", "Get workspace name, plan, limits, and usage", {}, async () => {
|
|
146
|
+
const data = await client.query(CURRENT_WORKSPACE_QUERY);
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: JSON.stringify(data.currentWorkspace, null, 2),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
server.tool("list_media", "List media assets in the workspace (url, filename, alt, type)", {
|
|
157
|
+
limit: z
|
|
158
|
+
.number()
|
|
159
|
+
.optional()
|
|
160
|
+
.default(50)
|
|
161
|
+
.describe("Max items to return (default 50, max 100)"),
|
|
162
|
+
}, async ({ limit }) => {
|
|
163
|
+
const data = await client.query(MEDIA_ASSETS_QUERY, { limit });
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: JSON.stringify(data.mediaAssets, null, 2),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
// ─── Write Tools ─────────────────────────────────────────────
|
|
174
|
+
server.tool("create_page", "Create a new page. Returns the created page with its ID.", {
|
|
175
|
+
name: z.string().describe("Internal page name"),
|
|
176
|
+
slug: z.string().describe("URL slug (e.g. 'about', 'features')"),
|
|
177
|
+
parentId: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Parent page ID for nested pages"),
|
|
181
|
+
pageType: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.default("page")
|
|
185
|
+
.describe("Page type (default: 'page')"),
|
|
186
|
+
displayName: z
|
|
187
|
+
.record(z.string(), z.string())
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Multilingual display name, e.g. { en: 'About', pl: 'O nas' }"),
|
|
190
|
+
seoTitle: z
|
|
191
|
+
.record(z.string(), z.string())
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Multilingual SEO title"),
|
|
194
|
+
seoDescription: z
|
|
195
|
+
.record(z.string(), z.string())
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Multilingual SEO description"),
|
|
198
|
+
}, async ({ name, slug, parentId, pageType, displayName, seoTitle, seoDescription, }) => {
|
|
199
|
+
const input = { name, slug };
|
|
200
|
+
if (parentId)
|
|
201
|
+
input.parentId = parentId;
|
|
202
|
+
if (pageType)
|
|
203
|
+
input.pageType = pageType;
|
|
204
|
+
if (displayName)
|
|
205
|
+
input.displayName = displayName;
|
|
206
|
+
if (seoTitle)
|
|
207
|
+
input.seoTitle = seoTitle;
|
|
208
|
+
if (seoDescription)
|
|
209
|
+
input.seoDescription = seoDescription;
|
|
210
|
+
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
211
|
+
input,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: JSON.stringify(data.savePage, null, 2),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
server.tool("update_page_blocks", "Set the full content blocks array on a page. Replaces all existing content blocks. Block types are validated against workspace config.", {
|
|
223
|
+
pageId: z.string().describe("Page ID"),
|
|
224
|
+
blocks: z
|
|
225
|
+
.array(z.object({
|
|
226
|
+
id: z.string().describe("Unique block instance ID (UUID)"),
|
|
227
|
+
type: z
|
|
228
|
+
.string()
|
|
229
|
+
.describe("Block type (must exist in workspace blocks)"),
|
|
230
|
+
content: z.record(z.string(), z.unknown()).optional(),
|
|
231
|
+
settings: z.record(z.string(), z.unknown()).optional(),
|
|
232
|
+
style: z.record(z.string(), z.unknown()).optional(),
|
|
233
|
+
advanced: z.record(z.string(), z.unknown()).optional(),
|
|
234
|
+
translations: z
|
|
235
|
+
.record(z.string(), z.object({ status: z.string() }))
|
|
236
|
+
.optional(),
|
|
237
|
+
defaultLanguage: z.string().optional(),
|
|
238
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
239
|
+
blockVersion: z.string().optional(),
|
|
240
|
+
}))
|
|
241
|
+
.describe("Full array of content blocks to set on the page"),
|
|
242
|
+
}, async ({ pageId, blocks }) => {
|
|
243
|
+
// Validate all block types against workspace registry
|
|
244
|
+
const validation = await validateBlockTypes(blocks.map((b) => b.type));
|
|
245
|
+
if (!validation.valid) {
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: validation.error }],
|
|
248
|
+
isError: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
252
|
+
if (!pageData.pageById) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
259
|
+
input: {
|
|
260
|
+
id: pageId,
|
|
261
|
+
name: pageData.pageById.name,
|
|
262
|
+
slug: pageData.pageById.slug,
|
|
263
|
+
blocks,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: JSON.stringify(data.savePage, null, 2),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
server.tool("update_page_settings", "Update page metadata: name, slug, display name, SEO fields", {
|
|
276
|
+
id: z.string().describe("Page ID"),
|
|
277
|
+
name: z.string().optional().describe("Internal page name"),
|
|
278
|
+
slug: z.string().optional().describe("URL slug"),
|
|
279
|
+
displayName: z
|
|
280
|
+
.record(z.string(), z.string())
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("Multilingual display name"),
|
|
283
|
+
seoTitle: z
|
|
284
|
+
.record(z.string(), z.string())
|
|
285
|
+
.optional()
|
|
286
|
+
.describe("Multilingual SEO title"),
|
|
287
|
+
seoDescription: z
|
|
288
|
+
.record(z.string(), z.string())
|
|
289
|
+
.optional()
|
|
290
|
+
.describe("Multilingual SEO description"),
|
|
291
|
+
seoKeywords: z.array(z.string()).optional().describe("SEO keywords"),
|
|
292
|
+
}, async ({ id, name, slug, displayName, seoTitle, seoDescription, seoKeywords, }) => {
|
|
293
|
+
const input = { id };
|
|
294
|
+
if (name !== undefined)
|
|
295
|
+
input.name = name;
|
|
296
|
+
if (slug !== undefined)
|
|
297
|
+
input.slug = slug;
|
|
298
|
+
if (displayName !== undefined)
|
|
299
|
+
input.displayName = displayName;
|
|
300
|
+
if (seoTitle !== undefined)
|
|
301
|
+
input.seoTitle = seoTitle;
|
|
302
|
+
if (seoDescription !== undefined)
|
|
303
|
+
input.seoDescription = seoDescription;
|
|
304
|
+
if (seoKeywords !== undefined)
|
|
305
|
+
input.seoKeywords = seoKeywords;
|
|
306
|
+
const data = await client.query(UPDATE_PAGE_SETTINGS_MUTATION, { input });
|
|
307
|
+
return {
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: JSON.stringify(data.updatePageSettings, null, 2),
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
server.tool("publish_page", "Publish a page or re-publish with latest draft changes. Uses atomic publishPage mutation.", { pageId: z.string().describe("Page ID to publish") }, async ({ pageId }) => {
|
|
317
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
318
|
+
if (!pageData.pageById) {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
321
|
+
isError: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const page = pageData.pageById;
|
|
325
|
+
if (page.published && !page.hasUnpublishedChanges) {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: "Page is already published with latest content. No changes to publish.",
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const blocks = (page.blocks || []).map((b) => ({
|
|
336
|
+
id: b.id,
|
|
337
|
+
type: b.type,
|
|
338
|
+
content: b.content,
|
|
339
|
+
settings: b.settings,
|
|
340
|
+
style: b.style,
|
|
341
|
+
advanced: b.advanced,
|
|
342
|
+
translations: b.translations,
|
|
343
|
+
defaultLanguage: b.defaultLanguage,
|
|
344
|
+
metadata: b.metadata,
|
|
345
|
+
blockVersion: b.blockVersion,
|
|
346
|
+
}));
|
|
347
|
+
const data = await client.query(PUBLISH_PAGE_MUTATION, { id: pageId, blocks });
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: JSON.stringify(data.publishPage, null, 2),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
server.tool("unpublish_page", "Unpublish a published page (toggles published state off).", { pageId: z.string().describe("Page ID to unpublish") }, async ({ pageId }) => {
|
|
358
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
359
|
+
if (!pageData.pageById) {
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
362
|
+
isError: true,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!pageData.pageById.published) {
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{ type: "text", text: "Page is already unpublished" },
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const data = await client.query(TOGGLE_PUBLISH_MUTATION, { id: pageId });
|
|
373
|
+
return {
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: JSON.stringify(data.togglePublish, null, 2),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
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 }) => {
|
|
383
|
+
const data = await client.query(REMOVE_PAGE_MUTATION, { id: pageId });
|
|
384
|
+
return {
|
|
385
|
+
content: [
|
|
386
|
+
{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: data.removePage
|
|
389
|
+
? "Page deleted successfully"
|
|
390
|
+
: "Failed to delete page",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
// ─── Layout Tools ──────────────────────────────────────────
|
|
396
|
+
server.tool("update_page_layout", "Update page-level layout settings: inheritance, overrides, or replace all layout blocks. Block types are validated against workspace config.", {
|
|
397
|
+
pageId: z.string().describe("Page ID"),
|
|
398
|
+
layoutBlocks: z
|
|
399
|
+
.array(z.record(z.string(), z.unknown()))
|
|
400
|
+
.optional()
|
|
401
|
+
.describe("Full replacement array of layout blocks. Each must have 'type' (validated against workspace). Position, order, content etc. are passed through to the backend."),
|
|
402
|
+
layoutOverrides: z
|
|
403
|
+
.array(z.object({
|
|
404
|
+
position: z.string().describe("Layout position to override"),
|
|
405
|
+
action: z.string().describe("Override action"),
|
|
406
|
+
blockId: z.string().optional().describe("Replacement block ID"),
|
|
407
|
+
}))
|
|
408
|
+
.optional()
|
|
409
|
+
.describe("Layout overrides for inherited positions"),
|
|
410
|
+
inheritsLayout: z
|
|
411
|
+
.boolean()
|
|
412
|
+
.optional()
|
|
413
|
+
.describe("Whether this page inherits layout from parent"),
|
|
414
|
+
}, async ({ pageId, layoutBlocks, layoutOverrides, inheritsLayout }) => {
|
|
415
|
+
// Validate block types against workspace registry
|
|
416
|
+
if (layoutBlocks) {
|
|
417
|
+
const types = layoutBlocks
|
|
418
|
+
.map((b) => b.type)
|
|
419
|
+
.filter(Boolean);
|
|
420
|
+
const validation = await validateBlockTypes(types);
|
|
421
|
+
if (!validation.valid) {
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: "text", text: validation.error }],
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const input = { pageId };
|
|
429
|
+
if (layoutBlocks !== undefined)
|
|
430
|
+
input.layoutBlocks = layoutBlocks;
|
|
431
|
+
if (layoutOverrides !== undefined)
|
|
432
|
+
input.layoutOverrides = layoutOverrides;
|
|
433
|
+
if (inheritsLayout !== undefined)
|
|
434
|
+
input.inheritsLayout = inheritsLayout;
|
|
435
|
+
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input });
|
|
436
|
+
if (!data.updatePageLayout) {
|
|
437
|
+
return {
|
|
438
|
+
content: [
|
|
439
|
+
{ type: "text", text: "Page not found or update failed" },
|
|
440
|
+
],
|
|
441
|
+
isError: true,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
content: [
|
|
446
|
+
{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: JSON.stringify(data.updatePageLayout, null, 2),
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
// ─── Block Helper Tools (read-modify-write) ─────────────────
|
|
454
|
+
server.tool("add_block_to_page", "Add a block to a page. Automatically detects layout vs content block from workspace config. Auto-generates UUID and translation status.", {
|
|
455
|
+
pageId: z.string().describe("Page ID"),
|
|
456
|
+
block: z.object({
|
|
457
|
+
type: z
|
|
458
|
+
.string()
|
|
459
|
+
.describe("Block type (must exist in workspace blocks)"),
|
|
460
|
+
content: z
|
|
461
|
+
.record(z.string(), z.unknown())
|
|
462
|
+
.describe("Language-keyed content: { en: { title: '...' }, pl: { title: '...' } }"),
|
|
463
|
+
settings: z.record(z.string(), z.unknown()).optional(),
|
|
464
|
+
style: z.record(z.string(), z.unknown()).optional(),
|
|
465
|
+
}),
|
|
466
|
+
position: z
|
|
467
|
+
.number()
|
|
468
|
+
.optional()
|
|
469
|
+
.describe("0-based position to insert at (default: end)"),
|
|
470
|
+
}, async ({ pageId, block, position }) => {
|
|
471
|
+
// Validate block type against workspace registry
|
|
472
|
+
const blockDef = await findBlockDef(block.type);
|
|
473
|
+
if (!blockDef) {
|
|
474
|
+
const available = await getWorkspaceBlocks();
|
|
475
|
+
return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: `Block type '${block.type}' not found. Available: ${available.map((b) => b.blockType).join(", ")}`,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// Fetch current page
|
|
486
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
487
|
+
if (!pageData.pageById) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
490
|
+
isError: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
// Get site config for languages
|
|
494
|
+
const configData = await client.query(SITE_CONFIG_QUERY);
|
|
495
|
+
const defaultLanguage = configData.siteConfig?.defaultLanguage ?? "en";
|
|
496
|
+
const enabledLanguages = configData.siteConfig?.enabledLanguages ?? [
|
|
497
|
+
defaultLanguage,
|
|
498
|
+
];
|
|
499
|
+
// Build translations object
|
|
500
|
+
const translations = {};
|
|
501
|
+
for (const lang of enabledLanguages) {
|
|
502
|
+
translations[lang] = {
|
|
503
|
+
status: block.content[lang] ? "completed" : "pending",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const newBlockId = crypto.randomUUID();
|
|
507
|
+
const isLayout = blockDef.layoutPosition !== null;
|
|
508
|
+
if (isLayout) {
|
|
509
|
+
// Layout block — add to layoutBlocks via updatePageLayout
|
|
510
|
+
const existingLayoutBlocks = pageData.pageById.layoutBlocks || [];
|
|
511
|
+
const layoutPosition = blockDef.layoutPosition;
|
|
512
|
+
// Append after existing blocks in same position
|
|
513
|
+
const maxOrder = existingLayoutBlocks
|
|
514
|
+
.filter((b) => b.position === layoutPosition)
|
|
515
|
+
.reduce((max, b) => Math.max(max, b.order), -1);
|
|
516
|
+
const newLayoutBlock = {
|
|
517
|
+
id: newBlockId,
|
|
518
|
+
type: block.type,
|
|
519
|
+
position: layoutPosition,
|
|
520
|
+
order: maxOrder + 1,
|
|
521
|
+
isActive: true,
|
|
522
|
+
content: block.content,
|
|
523
|
+
settings: block.settings,
|
|
524
|
+
style: block.style,
|
|
525
|
+
translations,
|
|
526
|
+
defaultLanguage,
|
|
527
|
+
};
|
|
528
|
+
const layoutBlocks = [...existingLayoutBlocks, newLayoutBlock];
|
|
529
|
+
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: JSON.stringify({ blockId: newBlockId, page: data.updatePageLayout }, null, 2),
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Content block — add to blocks via savePage
|
|
541
|
+
const newBlock = {
|
|
542
|
+
id: newBlockId,
|
|
543
|
+
type: block.type,
|
|
544
|
+
content: block.content,
|
|
545
|
+
settings: block.settings,
|
|
546
|
+
style: block.style,
|
|
547
|
+
translations,
|
|
548
|
+
defaultLanguage,
|
|
549
|
+
};
|
|
550
|
+
const blocks = [...pageData.pageById.blocks];
|
|
551
|
+
if (position !== undefined &&
|
|
552
|
+
position >= 0 &&
|
|
553
|
+
position < blocks.length) {
|
|
554
|
+
blocks.splice(position, 0, newBlock);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
blocks.push(newBlock);
|
|
558
|
+
}
|
|
559
|
+
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
560
|
+
input: {
|
|
561
|
+
id: pageId,
|
|
562
|
+
name: pageData.pageById.name,
|
|
563
|
+
slug: pageData.pageById.slug,
|
|
564
|
+
blocks,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: "text",
|
|
571
|
+
text: JSON.stringify({ blockId: newBlockId, page: data.savePage }, null, 2),
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
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.", {
|
|
578
|
+
pageId: z.string().describe("Page ID"),
|
|
579
|
+
blockId: z.string().describe("Block instance ID (UUID) to update"),
|
|
580
|
+
content: z
|
|
581
|
+
.record(z.string(), z.unknown())
|
|
582
|
+
.describe("Content to merge: { en: { title: 'New Title' } }"),
|
|
583
|
+
settings: z.record(z.string(), z.unknown()).optional(),
|
|
584
|
+
}, async ({ pageId, blockId, content, settings }) => {
|
|
585
|
+
// Fetch current page
|
|
586
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
587
|
+
if (!pageData.pageById) {
|
|
588
|
+
return {
|
|
589
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
590
|
+
isError: true,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const page = pageData.pageById;
|
|
594
|
+
// Search in content blocks first, then layout blocks
|
|
595
|
+
const contentIdx = page.blocks.findIndex((b) => b.id === blockId);
|
|
596
|
+
const layoutIdx = contentIdx === -1
|
|
597
|
+
? (page.layoutBlocks || []).findIndex((b) => b.id === blockId)
|
|
598
|
+
: -1;
|
|
599
|
+
if (contentIdx === -1 && layoutIdx === -1) {
|
|
600
|
+
return {
|
|
601
|
+
content: [
|
|
602
|
+
{
|
|
603
|
+
type: "text",
|
|
604
|
+
text: `Block '${blockId}' not found on page (checked both content and layout blocks)`,
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
isError: true,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const isLayout = layoutIdx !== -1;
|
|
611
|
+
const targetArray = isLayout
|
|
612
|
+
? [...(page.layoutBlocks || [])]
|
|
613
|
+
: [...page.blocks];
|
|
614
|
+
const targetIndex = isLayout ? layoutIdx : contentIdx;
|
|
615
|
+
// Merge content
|
|
616
|
+
const existingBlock = { ...targetArray[targetIndex] };
|
|
617
|
+
const mergedContent = { ...(existingBlock.content ?? {}) };
|
|
618
|
+
for (const [lang, langContent] of Object.entries(content)) {
|
|
619
|
+
if (typeof langContent === "object" &&
|
|
620
|
+
langContent !== null &&
|
|
621
|
+
typeof mergedContent[lang] === "object" &&
|
|
622
|
+
mergedContent[lang] !== null) {
|
|
623
|
+
mergedContent[lang] = {
|
|
624
|
+
...mergedContent[lang],
|
|
625
|
+
...langContent,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
mergedContent[lang] = langContent;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
existingBlock.content = mergedContent;
|
|
633
|
+
if (settings) {
|
|
634
|
+
existingBlock.settings = {
|
|
635
|
+
...(existingBlock.settings ?? {}),
|
|
636
|
+
...settings,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
// Update translation status
|
|
640
|
+
if (existingBlock.translations) {
|
|
641
|
+
for (const lang of Object.keys(content)) {
|
|
642
|
+
if (existingBlock.translations[lang]) {
|
|
643
|
+
existingBlock.translations[lang] = { status: "completed" };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
targetArray[targetIndex] = existingBlock;
|
|
648
|
+
if (isLayout) {
|
|
649
|
+
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks: targetArray } });
|
|
650
|
+
return {
|
|
651
|
+
content: [
|
|
652
|
+
{
|
|
653
|
+
type: "text",
|
|
654
|
+
text: JSON.stringify(data.updatePageLayout, null, 2),
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
661
|
+
input: {
|
|
662
|
+
id: pageId,
|
|
663
|
+
name: page.name,
|
|
664
|
+
slug: page.slug,
|
|
665
|
+
blocks: targetArray,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
{
|
|
671
|
+
type: "text",
|
|
672
|
+
text: JSON.stringify(data.savePage, null, 2),
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
server.tool("remove_block_from_page", "Remove a specific block from a page by its instance ID. Works for both content and layout blocks.", {
|
|
679
|
+
pageId: z.string().describe("Page ID"),
|
|
680
|
+
blockId: z.string().describe("Block instance ID (UUID) to remove"),
|
|
681
|
+
}, async ({ pageId, blockId }) => {
|
|
682
|
+
const pageData = await client.query(PAGE_BY_ID_QUERY, { id: pageId });
|
|
683
|
+
if (!pageData.pageById) {
|
|
684
|
+
return {
|
|
685
|
+
content: [{ type: "text", text: "Page not found" }],
|
|
686
|
+
isError: true,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const page = pageData.pageById;
|
|
690
|
+
// Try content blocks first
|
|
691
|
+
const contentBlocks = page.blocks.filter((b) => b.id !== blockId);
|
|
692
|
+
if (contentBlocks.length < page.blocks.length) {
|
|
693
|
+
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
694
|
+
input: {
|
|
695
|
+
id: pageId,
|
|
696
|
+
name: page.name,
|
|
697
|
+
slug: page.slug,
|
|
698
|
+
blocks: contentBlocks,
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
return {
|
|
702
|
+
content: [
|
|
703
|
+
{
|
|
704
|
+
type: "text",
|
|
705
|
+
text: JSON.stringify(data.savePage, null, 2),
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
// Try layout blocks
|
|
711
|
+
const layoutBlocks = (page.layoutBlocks || []).filter((b) => b.id !== blockId);
|
|
712
|
+
if (layoutBlocks.length < (page.layoutBlocks || []).length) {
|
|
713
|
+
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
714
|
+
return {
|
|
715
|
+
content: [
|
|
716
|
+
{
|
|
717
|
+
type: "text",
|
|
718
|
+
text: JSON.stringify(data.updatePageLayout, null, 2),
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
content: [
|
|
725
|
+
{
|
|
726
|
+
type: "text",
|
|
727
|
+
text: `Block '${blockId}' not found on page (checked both content and layout blocks)`,
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
isError: true,
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
// ─── Resources ───────────────────────────────────────────────
|
|
734
|
+
server.resource("sitemap", "cmssy://sitemap", {
|
|
735
|
+
description: "Full page tree as JSON — all pages with hierarchy",
|
|
736
|
+
mimeType: "application/json",
|
|
737
|
+
}, async (uri) => {
|
|
738
|
+
const data = await client.query(PAGES_QUERY);
|
|
739
|
+
return {
|
|
740
|
+
contents: [
|
|
741
|
+
{
|
|
742
|
+
uri: uri.href,
|
|
743
|
+
mimeType: "application/json",
|
|
744
|
+
text: JSON.stringify(data.pages, null, 2),
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
};
|
|
748
|
+
});
|
|
749
|
+
server.resource("blocks", "cmssy://blocks", {
|
|
750
|
+
description: "All available block types with schemas and defaults",
|
|
751
|
+
mimeType: "application/json",
|
|
752
|
+
}, async (uri) => {
|
|
753
|
+
const data = await client.query(WORKSPACE_BLOCKS_QUERY);
|
|
754
|
+
return {
|
|
755
|
+
contents: [
|
|
756
|
+
{
|
|
757
|
+
uri: uri.href,
|
|
758
|
+
mimeType: "application/json",
|
|
759
|
+
text: JSON.stringify(data.workspaceBlocks, null, 2),
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
server.resource("workspace", "cmssy://workspace", {
|
|
765
|
+
description: "Workspace info and site configuration merged",
|
|
766
|
+
mimeType: "application/json",
|
|
767
|
+
}, async (uri) => {
|
|
768
|
+
const [workspaceData, configData] = await Promise.all([
|
|
769
|
+
client.query(CURRENT_WORKSPACE_QUERY),
|
|
770
|
+
client.query(SITE_CONFIG_QUERY),
|
|
771
|
+
]);
|
|
772
|
+
return {
|
|
773
|
+
contents: [
|
|
774
|
+
{
|
|
775
|
+
uri: uri.href,
|
|
776
|
+
mimeType: "application/json",
|
|
777
|
+
text: JSON.stringify({
|
|
778
|
+
workspace: workspaceData.currentWorkspace,
|
|
779
|
+
siteConfig: configData.siteConfig,
|
|
780
|
+
}, null, 2),
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
return server;
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=server.js.map
|