@centrali-io/centrali-mcp 4.2.14 → 4.2.16
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/tools/describe.js +33 -5
- package/dist/tools/pages.js +47 -11
- package/package.json +1 -1
- package/src/tools/describe.ts +33 -5
- package/src/tools/pages.ts +52 -9
package/dist/tools/describe.js
CHANGED
|
@@ -941,7 +941,19 @@ function registerDescribeTools(server) {
|
|
|
941
941
|
"Use generate_starter_pages to auto-generate page proposals from your collections",
|
|
942
942
|
"Always validate_page before publish_page — publishing will reject if there are errors",
|
|
943
943
|
"Set access policy with set_page_access_policy before publishing if you need auth",
|
|
944
|
+
"Use variable bindings on data sources to scope data dynamically — bind to URL params, auth context, record context, or static defaults",
|
|
945
|
+
"For list→detail navigation: set useQueryParams:true on navigate-to-page actions, then bind the detail page's data source variables to { source: 'url', param: 'id' }",
|
|
946
|
+
"For user-scoped views (e.g., My Approvals): bind a variable to { source: 'auth', field: 'userId' } and use a smart query with {{assigneeId}}",
|
|
944
947
|
],
|
|
948
|
+
multi_page_pattern: {
|
|
949
|
+
description: "How to build a list→detail app with scoped related data",
|
|
950
|
+
steps: [
|
|
951
|
+
"1. Create a smart query with {{variables}} for the detail page's related data (e.g., filter by {{requestId}})",
|
|
952
|
+
"2. Create a list page with a data-table block and a navigate-to-page action: set config.useQueryParams:true, paramConfig: { source:'row', mode:'selected', selectedFields:['id'] }",
|
|
953
|
+
"3. Create a detail page with: (a) a record-card block with mode:'single' and variables: { id: { source:'url', param:'id' } }, (b) a related-list block with dataSource type:'query', ref:smartQueryId, variables: { requestId: { source:'url', param:'id' } }",
|
|
954
|
+
"4. Publish both pages. Clicking a row on the list navigates to /ws/detail-slug?id=rowId, and the detail page scopes all blocks to that ID.",
|
|
955
|
+
],
|
|
956
|
+
},
|
|
945
957
|
}, null, 2),
|
|
946
958
|
},
|
|
947
959
|
],
|
|
@@ -1008,9 +1020,10 @@ function registerDescribeTools(server) {
|
|
|
1008
1020
|
filterableColumns: "string[] | null — which columns users can filter on (for data-table blocks)",
|
|
1009
1021
|
},
|
|
1010
1022
|
data_source_shape: {
|
|
1011
|
-
type: "'structure' —
|
|
1012
|
-
ref: "string — the collection ID (UUID
|
|
1013
|
-
|
|
1023
|
+
type: "'structure' | 'query' — 'structure' fetches collection records directly, 'query' executes a smart query with {{variable}} substitution",
|
|
1024
|
+
ref: "string — the collection ID (for structure) or smart query ID (for query) — UUID",
|
|
1025
|
+
mode: "'list' | 'single' | 'aggregate' — optional. 'single' fetches one record (detail pages), 'list' fetches paginated records, 'aggregate' computes metrics",
|
|
1026
|
+
recordSlug: "string | undefined — the collection's record slug for direct resolution (avoids an extra lookup). Recommended for structure type.",
|
|
1014
1027
|
config: {
|
|
1015
1028
|
description: "Optional query configuration",
|
|
1016
1029
|
filter: "object | null — same filter syntax as query_records",
|
|
@@ -1018,12 +1031,27 @@ function registerDescribeTools(server) {
|
|
|
1018
1031
|
page: "number | null — for pagination",
|
|
1019
1032
|
pageSize: "number | null — records per page",
|
|
1020
1033
|
},
|
|
1034
|
+
variables: {
|
|
1035
|
+
description: "Optional variable bindings map. Each key is a variable name, each value declares the runtime source.",
|
|
1036
|
+
example: "{ requestId: { source: 'url', param: 'id' }, status: { source: 'static', value: 'active' } }",
|
|
1037
|
+
sources: {
|
|
1038
|
+
url: "{ source: 'url', param: 'paramName' } — reads from URL query params (e.g., ?id=abc)",
|
|
1039
|
+
auth: "{ source: 'auth', field: 'userId' | 'email' | 'name' } — from the logged-in user",
|
|
1040
|
+
record: "{ source: 'record', field: 'fieldName' } — from the page's primary record (detail pages only)",
|
|
1041
|
+
static: "{ source: 'static', value: 'literalValue' } — a hardcoded default",
|
|
1042
|
+
},
|
|
1043
|
+
behavior: {
|
|
1044
|
+
query_type: "For type:'query' — resolved variables substitute into smart query {{placeholders}} before execution",
|
|
1045
|
+
structure_type: "For type:'structure' — resolved variables become equality filters on the collection",
|
|
1046
|
+
unresolved: "If any variable cannot be resolved, the block returns an empty result with a variableError message — NEVER unfiltered data",
|
|
1047
|
+
},
|
|
1048
|
+
precedence: "url > record > auth > static (highest to lowest priority)",
|
|
1049
|
+
},
|
|
1021
1050
|
aggregation: {
|
|
1022
1051
|
description: "For metric/chart blocks. Defines computations over the data.",
|
|
1023
1052
|
operations: "Record<string, { count?: '*', sum?: 'fieldName', avg?: 'fieldName', min?: 'fieldName', max?: 'fieldName' }>",
|
|
1024
1053
|
groupBy: "string[] | null — fields to group by",
|
|
1025
1054
|
},
|
|
1026
|
-
mode: "'list' | 'single' | 'aggregate' — optional. Set to 'aggregate' for metric-card and chart blocks that use aggregation. For list and detail pages, mode is inferred from context.",
|
|
1027
1055
|
},
|
|
1028
1056
|
narrative_shape: {
|
|
1029
1057
|
primaryQuestion: "string — the business question this block answers (e.g., 'How are sales trending?')",
|
|
@@ -1212,7 +1240,7 @@ function registerDescribeTools(server) {
|
|
|
1212
1240
|
type: "'text' | 'number' | 'email' | 'date' | 'boolean' | 'select' | 'textarea' | 'hidden'",
|
|
1213
1241
|
required: "boolean (default: false) — ignored for hidden fields",
|
|
1214
1242
|
placeholder: "string | null",
|
|
1215
|
-
defaultValue: "string | number | boolean | { source: 'auth' | 'system', field: string } — static value or derived default. For hidden fields, this is the value injected at submit time. For visible fields, this pre-populates the input. Derived sources: auth.userId, auth.email, auth.name (from logged-in user), system.now (server timestamp). Derived defaults are resolved server-side.",
|
|
1243
|
+
defaultValue: "string | number | boolean | { source: 'auth' | 'system', field: string } — static value or derived default. For hidden fields, this is the value injected at submit time. For visible fields, this pre-populates the input. Derived sources: auth.userId, auth.email, auth.name (from logged-in user's JWT), system.now (server timestamp), system.uuid (random UUID). Derived defaults are resolved server-side.",
|
|
1216
1244
|
resolveErrorMessage: "string | null — custom error message shown when a derived defaultValue cannot be resolved (e.g. user not signed in). Defaults to 'This form requires you to be signed in' for auth source.",
|
|
1217
1245
|
options: "For 'select' type: { label: string, value: string }[] — inline static options",
|
|
1218
1246
|
optionSource: "For 'select' type (alternative): { type: 'static' | 'dynamic', staticOptions?: [{label, value}], dynamicRef?: 'collection-uuid', labelField?, valueField? }",
|
package/dist/tools/pages.js
CHANGED
|
@@ -61,20 +61,37 @@ function createPagesClient(sdk, centraliUrl, workspaceId) {
|
|
|
61
61
|
const client = axios_1.default.create({ baseURL });
|
|
62
62
|
// Attach the SDK's bearer token to every request
|
|
63
63
|
client.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
var _a, _b, _c
|
|
64
|
+
var _a, _b, _c;
|
|
65
65
|
const token = (_c = (_b = (_a = sdk).getToken) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : sdk.token;
|
|
66
|
-
if (
|
|
67
|
-
// Force the SDK to fetch a token by calling getTokenOrFetch
|
|
68
|
-
const freshToken = yield ((_e = (_d = sdk).getTokenOrFetch) === null || _e === void 0 ? void 0 : _e.call(_d));
|
|
69
|
-
if (freshToken) {
|
|
70
|
-
config.headers.Authorization = `Bearer ${freshToken}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
66
|
+
if (token) {
|
|
74
67
|
config.headers.Authorization = `Bearer ${token}`;
|
|
75
68
|
}
|
|
76
69
|
return config;
|
|
77
70
|
}));
|
|
71
|
+
// Retry on 401/403 after refreshing the token via the SDK
|
|
72
|
+
client.interceptors.response.use((response) => response, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
74
|
+
const originalRequest = error.config;
|
|
75
|
+
const isAuthError = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403;
|
|
76
|
+
if (isAuthError && !originalRequest._hasRetried) {
|
|
77
|
+
originalRequest._hasRetried = true;
|
|
78
|
+
// Force SDK to fetch a fresh token
|
|
79
|
+
const freshToken = yield ((_d = (_c = sdk).getTokenOrFetch) === null || _d === void 0 ? void 0 : _d.call(_c));
|
|
80
|
+
if (!freshToken) {
|
|
81
|
+
// SDK can't get a token either — trigger a re-auth by making any SDK call
|
|
82
|
+
try {
|
|
83
|
+
yield ((_f = (_e = sdk.axios) === null || _e === void 0 ? void 0 : _e.get) === null || _f === void 0 ? void 0 : _f.call(_e, '/health'));
|
|
84
|
+
}
|
|
85
|
+
catch ( /* ignore — we just want the token refresh side effect */_k) { /* ignore — we just want the token refresh side effect */ }
|
|
86
|
+
}
|
|
87
|
+
const token = (_j = (_h = (_g = sdk).getToken) === null || _h === void 0 ? void 0 : _h.call(_g)) !== null && _j !== void 0 ? _j : sdk.token;
|
|
88
|
+
if (token) {
|
|
89
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
90
|
+
return client.request(originalRequest);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Promise.reject(error);
|
|
94
|
+
}));
|
|
78
95
|
return { client, workspaceId };
|
|
79
96
|
}
|
|
80
97
|
function registerPageTools(server, sdk, centraliUrl, workspaceId) {
|
|
@@ -125,7 +142,9 @@ function registerPageTools(server, sdk, centraliUrl, workspaceId) {
|
|
|
125
142
|
};
|
|
126
143
|
}
|
|
127
144
|
}));
|
|
128
|
-
server.tool("create_page",
|
|
145
|
+
server.tool("create_page", `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.
|
|
146
|
+
|
|
147
|
+
Navigate-to-page actions can use config.useQueryParams: true to pass selected row fields as URL query params to the target page. Pair with paramConfig: { source: 'row', mode: 'selected', selectedFields: ['id'] } to control which fields are passed. The target detail page can then use variable bindings with { source: 'url', param: 'id' } to read those params.`, {
|
|
129
148
|
name: zod_1.z.string().describe("Display name for the page (e.g., 'Customer List')"),
|
|
130
149
|
slug: zod_1.z.string().describe("URL-safe slug (e.g., 'customer-list')"),
|
|
131
150
|
pageType: zod_1.z
|
|
@@ -192,7 +211,24 @@ function registerPageTools(server, sdk, centraliUrl, workspaceId) {
|
|
|
192
211
|
}
|
|
193
212
|
}));
|
|
194
213
|
// ── Drafts & Versions ─────────────────────────────────────────────
|
|
195
|
-
server.tool("save_page_draft",
|
|
214
|
+
server.tool("save_page_draft", `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.
|
|
215
|
+
|
|
216
|
+
Each block's dataSource can include an optional 'variables' map for runtime variable binding:
|
|
217
|
+
variables: { [varName]: { source, param?, field?, value? } }
|
|
218
|
+
|
|
219
|
+
Variable binding sources:
|
|
220
|
+
- { source: 'url', param: 'id' } — read from URL query param
|
|
221
|
+
- { source: 'auth', field: 'userId' | 'email' | 'name' } — from authenticated user
|
|
222
|
+
- { source: 'record', field: 'fieldName' } — from page's primary record (detail pages)
|
|
223
|
+
- { source: 'static', value: 'active' } — literal default value
|
|
224
|
+
|
|
225
|
+
For query data sources: variables substitute into smart query {{placeholders}}.
|
|
226
|
+
For structure data sources: variables become equality filters.
|
|
227
|
+
|
|
228
|
+
Common patterns:
|
|
229
|
+
- Detail page related list: { requestId: { source: 'url', param: 'id' } }
|
|
230
|
+
- User-scoped view: { assigneeId: { source: 'auth', field: 'userId' } }
|
|
231
|
+
- Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }`, {
|
|
196
232
|
pageId: zod_1.z.string().describe("The page ID (UUID)"),
|
|
197
233
|
definition: zod_1.z
|
|
198
234
|
.record(zod_1.z.string(), zod_1.z.any())
|
package/package.json
CHANGED
package/src/tools/describe.ts
CHANGED
|
@@ -1088,7 +1088,19 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1088
1088
|
"Use generate_starter_pages to auto-generate page proposals from your collections",
|
|
1089
1089
|
"Always validate_page before publish_page — publishing will reject if there are errors",
|
|
1090
1090
|
"Set access policy with set_page_access_policy before publishing if you need auth",
|
|
1091
|
+
"Use variable bindings on data sources to scope data dynamically — bind to URL params, auth context, record context, or static defaults",
|
|
1092
|
+
"For list→detail navigation: set useQueryParams:true on navigate-to-page actions, then bind the detail page's data source variables to { source: 'url', param: 'id' }",
|
|
1093
|
+
"For user-scoped views (e.g., My Approvals): bind a variable to { source: 'auth', field: 'userId' } and use a smart query with {{assigneeId}}",
|
|
1091
1094
|
],
|
|
1095
|
+
multi_page_pattern: {
|
|
1096
|
+
description: "How to build a list→detail app with scoped related data",
|
|
1097
|
+
steps: [
|
|
1098
|
+
"1. Create a smart query with {{variables}} for the detail page's related data (e.g., filter by {{requestId}})",
|
|
1099
|
+
"2. Create a list page with a data-table block and a navigate-to-page action: set config.useQueryParams:true, paramConfig: { source:'row', mode:'selected', selectedFields:['id'] }",
|
|
1100
|
+
"3. Create a detail page with: (a) a record-card block with mode:'single' and variables: { id: { source:'url', param:'id' } }, (b) a related-list block with dataSource type:'query', ref:smartQueryId, variables: { requestId: { source:'url', param:'id' } }",
|
|
1101
|
+
"4. Publish both pages. Clicking a row on the list navigates to /ws/detail-slug?id=rowId, and the detail page scopes all blocks to that ID.",
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1092
1104
|
},
|
|
1093
1105
|
null,
|
|
1094
1106
|
2
|
|
@@ -1180,9 +1192,10 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1180
1192
|
"string[] | null — which columns users can filter on (for data-table blocks)",
|
|
1181
1193
|
},
|
|
1182
1194
|
data_source_shape: {
|
|
1183
|
-
type: "'structure' —
|
|
1184
|
-
ref: "string — the collection ID (UUID
|
|
1185
|
-
|
|
1195
|
+
type: "'structure' | 'query' — 'structure' fetches collection records directly, 'query' executes a smart query with {{variable}} substitution",
|
|
1196
|
+
ref: "string — the collection ID (for structure) or smart query ID (for query) — UUID",
|
|
1197
|
+
mode: "'list' | 'single' | 'aggregate' — optional. 'single' fetches one record (detail pages), 'list' fetches paginated records, 'aggregate' computes metrics",
|
|
1198
|
+
recordSlug: "string | undefined — the collection's record slug for direct resolution (avoids an extra lookup). Recommended for structure type.",
|
|
1186
1199
|
config: {
|
|
1187
1200
|
description: "Optional query configuration",
|
|
1188
1201
|
filter: "object | null — same filter syntax as query_records",
|
|
@@ -1190,6 +1203,22 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1190
1203
|
page: "number | null — for pagination",
|
|
1191
1204
|
pageSize: "number | null — records per page",
|
|
1192
1205
|
},
|
|
1206
|
+
variables: {
|
|
1207
|
+
description: "Optional variable bindings map. Each key is a variable name, each value declares the runtime source.",
|
|
1208
|
+
example: "{ requestId: { source: 'url', param: 'id' }, status: { source: 'static', value: 'active' } }",
|
|
1209
|
+
sources: {
|
|
1210
|
+
url: "{ source: 'url', param: 'paramName' } — reads from URL query params (e.g., ?id=abc)",
|
|
1211
|
+
auth: "{ source: 'auth', field: 'userId' | 'email' | 'name' } — from the logged-in user",
|
|
1212
|
+
record: "{ source: 'record', field: 'fieldName' } — from the page's primary record (detail pages only)",
|
|
1213
|
+
static: "{ source: 'static', value: 'literalValue' } — a hardcoded default",
|
|
1214
|
+
},
|
|
1215
|
+
behavior: {
|
|
1216
|
+
query_type: "For type:'query' — resolved variables substitute into smart query {{placeholders}} before execution",
|
|
1217
|
+
structure_type: "For type:'structure' — resolved variables become equality filters on the collection",
|
|
1218
|
+
unresolved: "If any variable cannot be resolved, the block returns an empty result with a variableError message — NEVER unfiltered data",
|
|
1219
|
+
},
|
|
1220
|
+
precedence: "url > record > auth > static (highest to lowest priority)",
|
|
1221
|
+
},
|
|
1193
1222
|
aggregation: {
|
|
1194
1223
|
description:
|
|
1195
1224
|
"For metric/chart blocks. Defines computations over the data.",
|
|
@@ -1197,7 +1226,6 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1197
1226
|
"Record<string, { count?: '*', sum?: 'fieldName', avg?: 'fieldName', min?: 'fieldName', max?: 'fieldName' }>",
|
|
1198
1227
|
groupBy: "string[] | null — fields to group by",
|
|
1199
1228
|
},
|
|
1200
|
-
mode: "'list' | 'single' | 'aggregate' — optional. Set to 'aggregate' for metric-card and chart blocks that use aggregation. For list and detail pages, mode is inferred from context.",
|
|
1201
1229
|
},
|
|
1202
1230
|
narrative_shape: {
|
|
1203
1231
|
primaryQuestion:
|
|
@@ -1409,7 +1437,7 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
1409
1437
|
type: "'text' | 'number' | 'email' | 'date' | 'boolean' | 'select' | 'textarea' | 'hidden'",
|
|
1410
1438
|
required: "boolean (default: false) — ignored for hidden fields",
|
|
1411
1439
|
placeholder: "string | null",
|
|
1412
|
-
defaultValue: "string | number | boolean | { source: 'auth' | 'system', field: string } — static value or derived default. For hidden fields, this is the value injected at submit time. For visible fields, this pre-populates the input. Derived sources: auth.userId, auth.email, auth.name (from logged-in user), system.now (server timestamp). Derived defaults are resolved server-side.",
|
|
1440
|
+
defaultValue: "string | number | boolean | { source: 'auth' | 'system', field: string } — static value or derived default. For hidden fields, this is the value injected at submit time. For visible fields, this pre-populates the input. Derived sources: auth.userId, auth.email, auth.name (from logged-in user's JWT), system.now (server timestamp), system.uuid (random UUID). Derived defaults are resolved server-side.",
|
|
1413
1441
|
resolveErrorMessage: "string | null — custom error message shown when a derived defaultValue cannot be resolved (e.g. user not signed in). Defaults to 'This form requires you to be signed in' for auth source.",
|
|
1414
1442
|
options: "For 'select' type: { label: string, value: string }[] — inline static options",
|
|
1415
1443
|
optionSource: "For 'select' type (alternative): { type: 'static' | 'dynamic', staticOptions?: [{label, value}], dynamicRef?: 'collection-uuid', labelField?, valueField? }",
|
package/src/tools/pages.ts
CHANGED
|
@@ -54,18 +54,42 @@ function createPagesClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: s
|
|
|
54
54
|
// Attach the SDK's bearer token to every request
|
|
55
55
|
client.interceptors.request.use(async (config) => {
|
|
56
56
|
const token = (sdk as any).getToken?.() ?? (sdk as any).token;
|
|
57
|
-
if (
|
|
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 {
|
|
57
|
+
if (token) {
|
|
64
58
|
config.headers.Authorization = `Bearer ${token}`;
|
|
65
59
|
}
|
|
66
60
|
return config;
|
|
67
61
|
});
|
|
68
62
|
|
|
63
|
+
// Retry on 401/403 after refreshing the token via the SDK
|
|
64
|
+
client.interceptors.response.use(
|
|
65
|
+
(response) => response,
|
|
66
|
+
async (error) => {
|
|
67
|
+
const originalRequest = error.config;
|
|
68
|
+
const isAuthError = error.response?.status === 401 || error.response?.status === 403;
|
|
69
|
+
|
|
70
|
+
if (isAuthError && !originalRequest._hasRetried) {
|
|
71
|
+
originalRequest._hasRetried = true;
|
|
72
|
+
|
|
73
|
+
// Force SDK to fetch a fresh token
|
|
74
|
+
const freshToken = await (sdk as any).getTokenOrFetch?.();
|
|
75
|
+
if (!freshToken) {
|
|
76
|
+
// SDK can't get a token either — trigger a re-auth by making any SDK call
|
|
77
|
+
try {
|
|
78
|
+
await (sdk as any).axios?.get?.('/health');
|
|
79
|
+
} catch { /* ignore — we just want the token refresh side effect */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const token = (sdk as any).getToken?.() ?? (sdk as any).token;
|
|
83
|
+
if (token) {
|
|
84
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
85
|
+
return client.request(originalRequest);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
69
93
|
return { client, workspaceId };
|
|
70
94
|
}
|
|
71
95
|
|
|
@@ -134,7 +158,9 @@ export function registerPageTools(
|
|
|
134
158
|
|
|
135
159
|
server.tool(
|
|
136
160
|
"create_page",
|
|
137
|
-
|
|
161
|
+
`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.
|
|
162
|
+
|
|
163
|
+
Navigate-to-page actions can use config.useQueryParams: true to pass selected row fields as URL query params to the target page. Pair with paramConfig: { source: 'row', mode: 'selected', selectedFields: ['id'] } to control which fields are passed. The target detail page can then use variable bindings with { source: 'url', param: 'id' } to read those params.`,
|
|
138
164
|
{
|
|
139
165
|
name: z.string().describe("Display name for the page (e.g., 'Customer List')"),
|
|
140
166
|
slug: z.string().describe("URL-safe slug (e.g., 'customer-list')"),
|
|
@@ -214,7 +240,24 @@ export function registerPageTools(
|
|
|
214
240
|
|
|
215
241
|
server.tool(
|
|
216
242
|
"save_page_draft",
|
|
217
|
-
|
|
243
|
+
`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.
|
|
244
|
+
|
|
245
|
+
Each block's dataSource can include an optional 'variables' map for runtime variable binding:
|
|
246
|
+
variables: { [varName]: { source, param?, field?, value? } }
|
|
247
|
+
|
|
248
|
+
Variable binding sources:
|
|
249
|
+
- { source: 'url', param: 'id' } — read from URL query param
|
|
250
|
+
- { source: 'auth', field: 'userId' | 'email' | 'name' } — from authenticated user
|
|
251
|
+
- { source: 'record', field: 'fieldName' } — from page's primary record (detail pages)
|
|
252
|
+
- { source: 'static', value: 'active' } — literal default value
|
|
253
|
+
|
|
254
|
+
For query data sources: variables substitute into smart query {{placeholders}}.
|
|
255
|
+
For structure data sources: variables become equality filters.
|
|
256
|
+
|
|
257
|
+
Common patterns:
|
|
258
|
+
- Detail page related list: { requestId: { source: 'url', param: 'id' } }
|
|
259
|
+
- User-scoped view: { assigneeId: { source: 'auth', field: 'userId' } }
|
|
260
|
+
- Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }`,
|
|
218
261
|
{
|
|
219
262
|
pageId: z.string().describe("The page ID (UUID)"),
|
|
220
263
|
definition: z
|