@centrali-io/centrali-mcp 4.2.0 → 4.2.2
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 +25 -1
- package/dist/index.js +4 -0
- package/dist/tools/compute.js +323 -0
- package/dist/tools/describe.d.ts +10 -0
- package/dist/tools/describe.js +1485 -0
- package/dist/tools/orchestrations.js +146 -0
- package/dist/tools/pages.d.ts +3 -0
- package/dist/tools/pages.js +477 -0
- package/dist/tools/smart-queries.js +152 -0
- package/dist/tools/structures.js +117 -0
- package/package.json +2 -2
- package/src/index.ts +4 -0
- package/src/tools/compute.ts +371 -0
- package/src/tools/describe.ts +1798 -0
- package/src/tools/orchestrations.ts +167 -0
- package/src/tools/pages.ts +573 -0
- package/src/tools/smart-queries.ts +175 -0
- package/src/tools/structures.ts +123 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import axios, { AxiosInstance } from "axios";
|
|
5
|
+
|
|
6
|
+
function formatError(error: unknown, context: string): string {
|
|
7
|
+
if (axios.isAxiosError(error) && error.response?.data) {
|
|
8
|
+
const body = error.response.data;
|
|
9
|
+
const status = error.response.status;
|
|
10
|
+
const message = body.message || body.error || error.message;
|
|
11
|
+
return `Error ${context}: [${status}] ${message}`;
|
|
12
|
+
}
|
|
13
|
+
if (error && typeof error === "object") {
|
|
14
|
+
const e = error as Record<string, any>;
|
|
15
|
+
if ("message" in e) {
|
|
16
|
+
let msg = `Error ${context}`;
|
|
17
|
+
if ("code" in e || "status" in e) {
|
|
18
|
+
msg += `: [${e.code ?? e.status ?? "ERROR"}] ${e.message}`;
|
|
19
|
+
} else {
|
|
20
|
+
msg += `: ${e.message}`;
|
|
21
|
+
}
|
|
22
|
+
return msg;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derives the pages API base URL from the CENTRALI_URL.
|
|
30
|
+
* Pattern: https://centrali.io -> https://api.centrali.io/pages
|
|
31
|
+
* Or uses CENTRALI_PAGES_URL env var if explicitly set.
|
|
32
|
+
*/
|
|
33
|
+
function getPagesBaseUrl(centraliUrl: string): string {
|
|
34
|
+
const override = process.env.CENTRALI_PAGES_URL;
|
|
35
|
+
if (override) return override.replace(/\/$/, "");
|
|
36
|
+
|
|
37
|
+
const url = new URL(centraliUrl);
|
|
38
|
+
const hostname = url.hostname.startsWith("api.")
|
|
39
|
+
? url.hostname
|
|
40
|
+
: `api.${url.hostname}`;
|
|
41
|
+
return `${url.protocol}//${hostname}/pages`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates an axios instance for the pages API that uses the SDK's token.
|
|
46
|
+
*/
|
|
47
|
+
function createPagesClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: string): {
|
|
48
|
+
client: AxiosInstance;
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
} {
|
|
51
|
+
const baseURL = getPagesBaseUrl(centraliUrl);
|
|
52
|
+
const client = axios.create({ baseURL });
|
|
53
|
+
|
|
54
|
+
// Attach the SDK's bearer token to every request
|
|
55
|
+
client.interceptors.request.use(async (config) => {
|
|
56
|
+
const token = (sdk as any).getToken?.() ?? (sdk as any).token;
|
|
57
|
+
if (!token) {
|
|
58
|
+
// Force the SDK to fetch a token by calling getTokenOrFetch
|
|
59
|
+
const freshToken = await (sdk as any).getTokenOrFetch?.();
|
|
60
|
+
if (freshToken) {
|
|
61
|
+
config.headers.Authorization = `Bearer ${freshToken}`;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
65
|
+
}
|
|
66
|
+
return config;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return { client, workspaceId };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerPageTools(
|
|
73
|
+
server: McpServer,
|
|
74
|
+
sdk: CentraliSDK,
|
|
75
|
+
centraliUrl: string,
|
|
76
|
+
workspaceId: string
|
|
77
|
+
) {
|
|
78
|
+
const { client } = createPagesClient(sdk, centraliUrl, workspaceId);
|
|
79
|
+
const basePath = `/ws/${workspaceId}/api/v1/pages`;
|
|
80
|
+
|
|
81
|
+
// ── Page CRUD ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
server.tool(
|
|
84
|
+
"list_pages",
|
|
85
|
+
"List all pages in the workspace. Pages are pre-built UI views (lists, detail views, forms, dashboards) that surface data from structures.",
|
|
86
|
+
{
|
|
87
|
+
page: z.number().optional().describe("Page number (1-indexed, default: 1)"),
|
|
88
|
+
pageSize: z.number().optional().describe("Items per page (default: 20)"),
|
|
89
|
+
pageType: z
|
|
90
|
+
.enum(["list", "detail", "form", "dashboard"])
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Filter by page type"),
|
|
93
|
+
},
|
|
94
|
+
async ({ page, pageSize, pageType }) => {
|
|
95
|
+
try {
|
|
96
|
+
const params: Record<string, any> = {};
|
|
97
|
+
if (page !== undefined) params.page = page;
|
|
98
|
+
if (pageSize !== undefined) params.pageSize = pageSize;
|
|
99
|
+
if (pageType) params.pageType = pageType;
|
|
100
|
+
|
|
101
|
+
const resp = await client.get(basePath, { params });
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: formatError(error, "listing pages") }],
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
server.tool(
|
|
115
|
+
"get_page",
|
|
116
|
+
"Get a page by its ID. Returns the page metadata including name, slug, type, and description.",
|
|
117
|
+
{
|
|
118
|
+
pageId: z.string().describe("The page ID (UUID)"),
|
|
119
|
+
},
|
|
120
|
+
async ({ pageId }) => {
|
|
121
|
+
try {
|
|
122
|
+
const resp = await client.get(`${basePath}/${pageId}`);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: formatError(error, `getting page '${pageId}'`) }],
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
server.tool(
|
|
136
|
+
"create_page",
|
|
137
|
+
"Create a new page in the workspace. A page is a UI view backed by data from structures. Specify the page type: 'list' for data tables, 'detail' for single-record views, 'form' for data entry, 'dashboard' for metrics and charts.",
|
|
138
|
+
{
|
|
139
|
+
name: z.string().describe("Display name for the page (e.g., 'Customer List')"),
|
|
140
|
+
slug: z.string().describe("URL-safe slug (e.g., 'customer-list')"),
|
|
141
|
+
pageType: z
|
|
142
|
+
.enum(["list", "detail", "form", "dashboard"])
|
|
143
|
+
.describe("The type of page to create"),
|
|
144
|
+
description: z.string().optional().describe("Optional description of the page's purpose"),
|
|
145
|
+
},
|
|
146
|
+
async ({ name, slug, pageType, description }) => {
|
|
147
|
+
try {
|
|
148
|
+
const resp = await client.post(basePath, { name, slug, pageType, description });
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: formatError(error, "creating page") }],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
server.tool(
|
|
162
|
+
"update_page",
|
|
163
|
+
"Update a page's metadata (name, slug, description). Does not modify the page definition — use save_page_draft for that.",
|
|
164
|
+
{
|
|
165
|
+
pageId: z.string().describe("The page ID (UUID) to update"),
|
|
166
|
+
name: z.string().optional().describe("New display name"),
|
|
167
|
+
slug: z.string().optional().describe("New URL slug"),
|
|
168
|
+
description: z.string().optional().describe("New description"),
|
|
169
|
+
showInNavShell: z.boolean().optional().describe("Whether to show this page in the navigation shell"),
|
|
170
|
+
},
|
|
171
|
+
async ({ pageId, name, slug, description, showInNavShell }) => {
|
|
172
|
+
try {
|
|
173
|
+
const body: Record<string, any> = {};
|
|
174
|
+
if (name !== undefined) body.name = name;
|
|
175
|
+
if (slug !== undefined) body.slug = slug;
|
|
176
|
+
if (description !== undefined) body.description = description;
|
|
177
|
+
if (showInNavShell !== undefined) body.showInNavShell = showInNavShell;
|
|
178
|
+
|
|
179
|
+
const resp = await client.patch(`${basePath}/${pageId}`, body);
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: formatError(error, `updating page '${pageId}'`) }],
|
|
186
|
+
isError: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
server.tool(
|
|
193
|
+
"delete_page",
|
|
194
|
+
"Soft-delete a page by its ID. The page will no longer be accessible but can potentially be restored.",
|
|
195
|
+
{
|
|
196
|
+
pageId: z.string().describe("The page ID (UUID) to delete"),
|
|
197
|
+
},
|
|
198
|
+
async ({ pageId }) => {
|
|
199
|
+
try {
|
|
200
|
+
await client.delete(`${basePath}/${pageId}`);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: `Page '${pageId}' deleted successfully.` }],
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: formatError(error, `deleting page '${pageId}'`) }],
|
|
207
|
+
isError: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ── Drafts & Versions ─────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
server.tool(
|
|
216
|
+
"save_page_draft",
|
|
217
|
+
"Save or update the draft definition for a page. The definition describes the page's layout: sections, blocks, data sources, actions, and presentation. This does NOT publish the page — use publish_page after saving.",
|
|
218
|
+
{
|
|
219
|
+
pageId: z.string().describe("The page ID (UUID)"),
|
|
220
|
+
definition: z
|
|
221
|
+
.record(z.string(), z.any())
|
|
222
|
+
.describe(
|
|
223
|
+
"The page definition object containing sections with blocks. Each section has: id, kind (content|metrics|exceptions|actions|hero|supporting), title, layout (single-column|two-column|metric-strip|stack), and blocks array."
|
|
224
|
+
),
|
|
225
|
+
changeSummary: z.string().optional().describe("Summary of changes in this draft"),
|
|
226
|
+
},
|
|
227
|
+
async ({ pageId, definition, changeSummary }) => {
|
|
228
|
+
try {
|
|
229
|
+
const body: Record<string, any> = { definition };
|
|
230
|
+
if (changeSummary) body.changeSummary = changeSummary;
|
|
231
|
+
|
|
232
|
+
const resp = await client.put(`${basePath}/${pageId}/draft`, body);
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: formatError(error, `saving draft for page '${pageId}'`) }],
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
server.tool(
|
|
246
|
+
"get_page_draft",
|
|
247
|
+
"Get the current draft version of a page, including its full definition (sections, blocks, data sources).",
|
|
248
|
+
{
|
|
249
|
+
pageId: z.string().describe("The page ID (UUID)"),
|
|
250
|
+
},
|
|
251
|
+
async ({ pageId }) => {
|
|
252
|
+
try {
|
|
253
|
+
const resp = await client.get(`${basePath}/${pageId}/draft`);
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: formatError(error, `getting draft for page '${pageId}'`) }],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
server.tool(
|
|
267
|
+
"list_page_versions",
|
|
268
|
+
"List all versions of a page. Each version captures a snapshot of the page definition at a point in time.",
|
|
269
|
+
{
|
|
270
|
+
pageId: z.string().describe("The page ID (UUID)"),
|
|
271
|
+
page: z.number().optional().describe("Page number (1-indexed, default: 1)"),
|
|
272
|
+
pageSize: z.number().optional().describe("Items per page (default: 20)"),
|
|
273
|
+
},
|
|
274
|
+
async ({ pageId, page, pageSize }) => {
|
|
275
|
+
try {
|
|
276
|
+
const params: Record<string, any> = {};
|
|
277
|
+
if (page !== undefined) params.page = page;
|
|
278
|
+
if (pageSize !== undefined) params.pageSize = pageSize;
|
|
279
|
+
|
|
280
|
+
const resp = await client.get(`${basePath}/${pageId}/versions`, { params });
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
283
|
+
};
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: "text", text: formatError(error, `listing versions for page '${pageId}'`) }],
|
|
287
|
+
isError: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// ── Publishing ────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
server.tool(
|
|
296
|
+
"validate_page",
|
|
297
|
+
"Run validation checks on a page's current draft to determine if it is ready to publish. Returns a list of issues (errors and warnings) that must be resolved before publishing.",
|
|
298
|
+
{
|
|
299
|
+
pageId: z.string().describe("The page ID (UUID) to validate"),
|
|
300
|
+
},
|
|
301
|
+
async ({ pageId }) => {
|
|
302
|
+
try {
|
|
303
|
+
const resp = await client.post(`${basePath}/${pageId}/validate`);
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
306
|
+
};
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: formatError(error, `validating page '${pageId}'`) }],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
server.tool(
|
|
317
|
+
"publish_page",
|
|
318
|
+
"Publish the current draft of a page, making it accessible at its runtime URL. Validates the draft first — if there are errors, publishing is rejected with details. Returns the publication record and the public runtime URL.",
|
|
319
|
+
{
|
|
320
|
+
pageId: z.string().describe("The page ID (UUID) to publish"),
|
|
321
|
+
},
|
|
322
|
+
async ({ pageId }) => {
|
|
323
|
+
try {
|
|
324
|
+
const resp = await client.post(`${basePath}/${pageId}/publish`);
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: formatError(error, `publishing page '${pageId}'`) }],
|
|
331
|
+
isError: true,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
server.tool(
|
|
338
|
+
"unpublish_page",
|
|
339
|
+
"Unpublish a page, making it no longer accessible at its runtime URL. The page definition is preserved and can be re-published later.",
|
|
340
|
+
{
|
|
341
|
+
pageId: z.string().describe("The page ID (UUID) to unpublish"),
|
|
342
|
+
},
|
|
343
|
+
async ({ pageId }) => {
|
|
344
|
+
try {
|
|
345
|
+
const resp = await client.post(`${basePath}/${pageId}/unpublish`);
|
|
346
|
+
return {
|
|
347
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: formatError(error, `unpublishing page '${pageId}'`) }],
|
|
352
|
+
isError: true,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// ── Access Policy ─────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
server.tool(
|
|
361
|
+
"set_page_access_policy",
|
|
362
|
+
"Set the access policy for a page. Controls who can view the published page: 'public' (anyone), 'authenticated' (logged-in users), or 'role-gated' (users with specific roles).",
|
|
363
|
+
{
|
|
364
|
+
pageId: z.string().describe("The page ID (UUID)"),
|
|
365
|
+
accessMode: z
|
|
366
|
+
.enum(["public", "authenticated", "role-gated"])
|
|
367
|
+
.describe("Access control mode"),
|
|
368
|
+
requiredRoles: z
|
|
369
|
+
.array(z.string())
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("Required roles (only used when accessMode is 'role-gated')"),
|
|
372
|
+
},
|
|
373
|
+
async ({ pageId, accessMode, requiredRoles }) => {
|
|
374
|
+
try {
|
|
375
|
+
const body: Record<string, any> = { accessMode };
|
|
376
|
+
if (requiredRoles) body.requiredRoles = requiredRoles;
|
|
377
|
+
|
|
378
|
+
const resp = await client.put(`${basePath}/${pageId}/access-policy`, body);
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
381
|
+
};
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: formatError(error, `setting access policy for page '${pageId}'`) }],
|
|
385
|
+
isError: true,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// ── Theme ─────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
server.tool(
|
|
394
|
+
"get_page_theme",
|
|
395
|
+
"Get the workspace theme configuration used by published pages. Includes primary color, accent color, optional logo URL, and font family.",
|
|
396
|
+
{},
|
|
397
|
+
async () => {
|
|
398
|
+
try {
|
|
399
|
+
const resp = await client.get(`${basePath}/theme`);
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return {
|
|
405
|
+
content: [{ type: "text", text: formatError(error, "getting page theme") }],
|
|
406
|
+
isError: true,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
server.tool(
|
|
413
|
+
"set_page_theme",
|
|
414
|
+
"Set the workspace theme for published pages. Controls the visual branding of all published pages.",
|
|
415
|
+
{
|
|
416
|
+
primaryColor: z.string().describe("Primary brand color (hex, e.g., '#1a73e8')"),
|
|
417
|
+
accentColor: z.string().describe("Accent color (hex, e.g., '#ff6d00')"),
|
|
418
|
+
logoUrl: z.string().optional().describe("URL to the workspace logo"),
|
|
419
|
+
fontFamily: z.string().optional().describe("Font family name (e.g., 'Inter', 'Roboto')"),
|
|
420
|
+
},
|
|
421
|
+
async ({ primaryColor, accentColor, logoUrl, fontFamily }) => {
|
|
422
|
+
try {
|
|
423
|
+
const config: Record<string, any> = { primaryColor, accentColor };
|
|
424
|
+
if (logoUrl !== undefined) config.logoUrl = logoUrl;
|
|
425
|
+
if (fontFamily !== undefined) config.fontFamily = fontFamily;
|
|
426
|
+
|
|
427
|
+
const resp = await client.put(`${basePath}/theme`, { config });
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
430
|
+
};
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
content: [{ type: "text", text: formatError(error, "setting page theme") }],
|
|
434
|
+
isError: true,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// ── Navigation ────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
server.tool(
|
|
443
|
+
"get_navigation",
|
|
444
|
+
"Get the workspace navigation configuration. Controls the sidebar/nav shell that wraps published pages.",
|
|
445
|
+
{},
|
|
446
|
+
async () => {
|
|
447
|
+
try {
|
|
448
|
+
const resp = await client.get(`${basePath}/navigation`);
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text", text: formatError(error, "getting navigation config") }],
|
|
455
|
+
isError: true,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
server.tool(
|
|
462
|
+
"set_navigation",
|
|
463
|
+
"Set the workspace navigation configuration. Defines the sidebar items, branding, and grouping for the page navigation shell.",
|
|
464
|
+
{
|
|
465
|
+
config: z
|
|
466
|
+
.record(z.string(), z.any())
|
|
467
|
+
.describe(
|
|
468
|
+
"Navigation config object with: enabled (boolean), branding ({ logoUrl, displayName }), items (array of nav items with pageId, label, icon, group)"
|
|
469
|
+
),
|
|
470
|
+
},
|
|
471
|
+
async ({ config }) => {
|
|
472
|
+
try {
|
|
473
|
+
const resp = await client.put(`${basePath}/navigation`, { config });
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
476
|
+
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return {
|
|
479
|
+
content: [{ type: "text", text: formatError(error, "setting navigation config") }],
|
|
480
|
+
isError: true,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
server.tool(
|
|
487
|
+
"delete_navigation",
|
|
488
|
+
"Delete the workspace navigation configuration. Pages revert to standalone rendering without a sidebar/nav shell.",
|
|
489
|
+
{},
|
|
490
|
+
async () => {
|
|
491
|
+
try {
|
|
492
|
+
await client.delete(`${basePath}/navigation`);
|
|
493
|
+
return {
|
|
494
|
+
content: [{ type: "text", text: "Navigation config deleted. Pages will render standalone." }],
|
|
495
|
+
};
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return {
|
|
498
|
+
content: [{ type: "text", text: formatError(error, "deleting navigation config") }],
|
|
499
|
+
isError: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// ── Assembly (AI-assisted page generation) ────────────────────────
|
|
506
|
+
|
|
507
|
+
server.tool(
|
|
508
|
+
"generate_starter_pages",
|
|
509
|
+
"Generate page proposals from structure IDs. Uses guided assembly to create page definitions (list, detail, form, dashboard) tailored to the data schema. Returns proposals that can be reviewed and accepted.",
|
|
510
|
+
{
|
|
511
|
+
structureIds: z
|
|
512
|
+
.array(z.string())
|
|
513
|
+
.describe("Array of structure IDs (UUIDs) to generate pages for"),
|
|
514
|
+
goals: z
|
|
515
|
+
.array(z.string())
|
|
516
|
+
.optional()
|
|
517
|
+
.describe("Optional goals to guide page generation (e.g., 'customer management dashboard', 'order tracking')"),
|
|
518
|
+
},
|
|
519
|
+
async ({ structureIds, goals }) => {
|
|
520
|
+
try {
|
|
521
|
+
const body: Record<string, any> = { structureIds };
|
|
522
|
+
if (goals) body.goals = goals;
|
|
523
|
+
|
|
524
|
+
const resp = await client.post(`${basePath}/generate-starter-pages`, body);
|
|
525
|
+
return {
|
|
526
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
527
|
+
};
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return {
|
|
530
|
+
content: [{ type: "text", text: formatError(error, "generating starter pages") }],
|
|
531
|
+
isError: true,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
server.tool(
|
|
538
|
+
"accept_page_proposal",
|
|
539
|
+
"Accept a generated page proposal and create the page with its initial draft definition. Use after reviewing proposals from generate_starter_pages.",
|
|
540
|
+
{
|
|
541
|
+
name: z.string().describe("Display name for the page"),
|
|
542
|
+
slug: z.string().describe("URL-safe slug for the page"),
|
|
543
|
+
pageType: z
|
|
544
|
+
.enum(["list", "detail", "form", "dashboard"])
|
|
545
|
+
.describe("Page type"),
|
|
546
|
+
definition: z
|
|
547
|
+
.record(z.string(), z.any())
|
|
548
|
+
.describe("The page definition from the proposal"),
|
|
549
|
+
generationSource: z
|
|
550
|
+
.string()
|
|
551
|
+
.describe("The source that generated this proposal (from the proposal object)"),
|
|
552
|
+
},
|
|
553
|
+
async ({ name, slug, pageType, definition, generationSource }) => {
|
|
554
|
+
try {
|
|
555
|
+
const resp = await client.post(`${basePath}/accept-proposal`, {
|
|
556
|
+
name,
|
|
557
|
+
slug,
|
|
558
|
+
pageType,
|
|
559
|
+
definition,
|
|
560
|
+
generationSource,
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }],
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: "text", text: formatError(error, "accepting page proposal") }],
|
|
568
|
+
isError: true,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
}
|