@centrali-io/centrali-mcp 4.2.16 → 4.3.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.
@@ -942,7 +942,7 @@ function registerDescribeTools(server) {
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
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' }",
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' }. Use config.paramMapping to rename fields if source and target use different names (e.g., { requestId: 'id' })",
946
946
  "For user-scoped views (e.g., My Approvals): bind a variable to { source: 'auth', field: 'userId' } and use a smart query with {{assigneeId}}",
947
947
  ],
948
948
  multi_page_pattern: {
@@ -1042,8 +1042,10 @@ function registerDescribeTools(server) {
1042
1042
  },
1043
1043
  behavior: {
1044
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",
1045
+ structure_type: "For type:'structure' — resolved variables become equality filters on the collection. IMPORTANT: the variable name 'id' is special — it matches the system record UUID (used for single-record fetch by ID). ALL OTHER variable names filter against user data fields and are automatically prefixed with 'data.' (e.g., variable 'requestId' filters on data.requestId in the JSONB column). System columns like createdAt, updatedAt, status do NOT get the data. prefix.",
1046
1046
  unresolved: "If any variable cannot be resolved, the block returns an empty result with a variableError message — NEVER unfiltered data",
1047
+ detail_page_pattern: "For a detail page: use variables: { id: { source: 'url', param: 'id' } } on the primary record-card block to fetch by system ID. For related-list blocks, use the actual data field name (e.g., variables: { requestId: { source: 'url', param: 'id' } }) — this filters related records where data.requestId matches the URL param. The 'id' variable fetches a record BY its UUID; other variable names filter records WHERE a field equals the value.",
1048
+ reference_fields: "Reference fields (foreign keys between collections) store the target record's system UUID. To filter a related list by a reference field, bind the variable to the parent record's system ID (from URL param), not a custom field value. Example: approval_steps has a data.requestId field that stores the governance_request's UUID.",
1047
1049
  },
1048
1050
  precedence: "url > record > auth > static (highest to lowest priority)",
1049
1051
  },
@@ -1340,11 +1342,65 @@ function registerDescribeTools(server) {
1340
1342
  requires: "content property (string) — the HTML markup",
1341
1343
  category: "content-block",
1342
1344
  },
1345
+ "data-html": {
1346
+ description: "Data-bound HTML block. Write HTML templates with {{fieldName}} placeholders that resolve against a data source. Supports two modes: LIST mode (default) renders the template ONCE PER RECORD — perfect for card grids, custom list layouts, or any repeating template. SINGLE mode renders the template once for a single record (detail pages). Records are auto-flattened so {{firstName}} works directly — no need for {{data.firstName}}. Output is sanitized via DOMPurify. No JavaScript allowed.",
1347
+ dataSource: "Required — type: 'structure' or 'query'. Data is resolved server-side and injected into the template.",
1348
+ requires: "content property (string) — HTML template with {{field}} placeholders. Must have non-empty content to pass publishing validation.",
1349
+ template_syntax: "{{fieldName}} for top-level fields, {{data.status}} for nested. Unresolved placeholders render as empty string.",
1350
+ modes: {
1351
+ list: "Default. Template repeats for EACH record. Write a single card/row template and it renders N times for N records. Example: a styled customer card template produces a grid of cards.",
1352
+ single: "Template renders once for ONE record. Use with mode:'single' and a variable binding like { id: { source: 'url', param: 'id' } } to fetch a specific record.",
1353
+ },
1354
+ example_list_mode: {
1355
+ blockType: "data-html",
1356
+ dataSource: { type: "structure", ref: "<collection-uuid>", recordSlug: "customers", config: { sort: [{ field: "createdAt", direction: "desc" }], limit: 20 } },
1357
+ content: "<div style='display:inline-block; width:280px; margin:8px; padding:16px; border:1px solid #e5e7eb; border-radius:8px; vertical-align:top;'><h3>{{firstName}} {{lastName}}</h3><p style='color:#6b7280;'>{{email}}</p></div>",
1358
+ },
1359
+ example_single_mode: {
1360
+ blockType: "data-html",
1361
+ dataSource: { type: "structure", ref: "<collection-uuid>", mode: "single", config: {}, variables: { id: { source: "url", param: "id" } } },
1362
+ content: "<div style='padding:16px; border:1px solid #e5e7eb; border-radius:8px;'><h2>{{firstName}} {{lastName}}</h2><p>{{email}}</p><p>Company: {{company}}</p></div>",
1363
+ },
1364
+ IMPORTANT: "No iteration syntax needed for lists. Just write a single template — the runtime repeats it automatically for each record. Records are auto-flattened: use {{fieldName}} directly, not {{data.fieldName}}.",
1365
+ category: "data-block",
1366
+ },
1367
+ "image": {
1368
+ description: "Image block. Displays an image from a URL with responsive sizing, lazy loading, optional caption, alignment, and link-on-click.",
1369
+ dataSource: "Not required",
1370
+ requires: "src property (string) — the image URL. Optional: alt, caption, alignment (left|center|right), maxWidth, linkUrl.",
1371
+ category: "media-block",
1372
+ },
1373
+ "video": {
1374
+ description: "Video block. Embeds YouTube/Vimeo videos via iframe or direct MP4/WebM via native <video>. Auto-detects provider from URL.",
1375
+ dataSource: "Not required",
1376
+ requires: "src property (string) — video URL (YouTube, Vimeo, or direct). Optional: autoplay (boolean, muted only), loop, controls (default true), poster, caption.",
1377
+ category: "media-block",
1378
+ },
1379
+ "modal": {
1380
+ description: "Declarative modal block. Invisible by default — renders as an overlay when triggered by an 'open-modal' action. Content can be static HTML, markdown, or data-bound HTML. Supports actions (buttons) inside the modal for confirm/cancel flows. Uses Radix Dialog.",
1381
+ dataSource: "Optional — only needed if contentType is 'data-html'",
1382
+ requires: "content property (string), contentType ('html' | 'markdown' | 'data-html', default 'html'). Optional: title, modalSize ('sm' | 'md' | 'lg' | 'fullscreen', default 'md').",
1383
+ actions: "Optional actions array — renders as buttons at the bottom of the modal. Supports close-modal (close the modal), invoke-trigger, navigate-to-page, start-orchestration, open-modal (open another modal). Use close-modal with targetRef set to this modal's own ID for a 'Cancel' button.",
1384
+ trigger: "Add an 'open-modal' action on another block with targetRef set to this modal block's ID. The modal opens when the action fires.",
1385
+ category: "interactive-block",
1386
+ example_confirm_modal: {
1387
+ blockType: "modal",
1388
+ title: "Confirm Delete",
1389
+ modalSize: "sm",
1390
+ contentType: "html",
1391
+ content: "<p>Are you sure you want to delete this item? This action cannot be undone.</p>",
1392
+ actions: [
1393
+ { id: "cancel", type: "close-modal", label: "Cancel", targetRef: "THIS_MODAL_ID", presentation: { color: "neutral" } },
1394
+ { id: "confirm", type: "invoke-trigger", label: "Delete", targetRef: "TRIGGER_ID", presentation: { color: "destructive" } },
1395
+ ],
1396
+ },
1397
+ },
1343
1398
  },
1344
1399
  block_categories: {
1345
- "data-block": "Blocks that display data from collections (data-table, chart, metric-card, stat-group, field-display, related-list, activity-feed, kanban, calendar, progress-indicator)",
1400
+ "data-block": "Blocks that display data from collections (data-table, chart, metric-card, stat-group, field-display, related-list, activity-feed, kanban, calendar, progress-indicator, data-html)",
1346
1401
  "form-block": "Use 'field-group' blockType with a 'fields' array for form pages. Do NOT use individual field block types (text-input, select, etc.) — the runtime renders field types from the field-group's fields array.",
1347
1402
  "content-block": "Static content blocks (markdown, static-html). Must have non-empty 'content' property.",
1403
+ "media-block": "Media blocks (image, video). Require a 'src' URL property.",
1348
1404
  "layout-block": "Blocks that control page-level behavior (filter-bar)",
1349
1405
  },
1350
1406
  action_mappings: {
@@ -1431,16 +1487,18 @@ function registerDescribeTools(server) {
1431
1487
  targetRef: "Page slug to navigate to",
1432
1488
  typical_activation: "row-click",
1433
1489
  paramConfig: "Use source: 'row', mode: 'selected', selectedFields: ['id'] to pass the record ID to the detail page",
1490
+ paramMapping: "Optional config.paramMapping: { sourceField: 'targetParam' } to rename fields in the URL. E.g., { requestId: 'id' } passes ?id=<value> instead of ?requestId=<value>. Only applies when useQueryParams is true.",
1434
1491
  example: {
1435
1492
  id: "view-detail",
1436
1493
  type: "navigate-to-page",
1437
1494
  label: "View Details",
1438
1495
  targetRef: "customer-detail",
1439
1496
  activation: "row-click",
1497
+ config: { useQueryParams: true, paramMapping: { requestId: "id" } },
1440
1498
  paramConfig: {
1441
1499
  source: "row",
1442
1500
  mode: "selected",
1443
- selectedFields: ["id"],
1501
+ selectedFields: ["requestId"],
1444
1502
  },
1445
1503
  },
1446
1504
  },
@@ -1477,6 +1535,16 @@ function registerDescribeTools(server) {
1477
1535
  typical_activation: "button",
1478
1536
  executionMode: "'fire-and-forget'",
1479
1537
  },
1538
+ "open-modal": {
1539
+ description: "Open a modal block. Client-side only — no backend call. The modal block must be defined on the same page with blockType 'modal'.",
1540
+ targetRef: "The modal block's ID (UUID) — must match a block with blockType 'modal' on this page.",
1541
+ typical_activation: "button",
1542
+ },
1543
+ "close-modal": {
1544
+ description: "Close an open modal. Client-side only.",
1545
+ targetRef: "The modal block's ID (UUID) to close.",
1546
+ typical_activation: "button",
1547
+ },
1480
1548
  },
1481
1549
  tips: [
1482
1550
  "Every action needs an 'id' — use UUIDs for uniqueness",
@@ -144,7 +144,7 @@ function registerPageTools(server, sdk, centraliUrl, workspaceId) {
144
144
  }));
145
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
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.`, {
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. Use config.paramMapping: { sourceField: 'targetParam' } to rename fields in the URL (e.g., { requestId: 'id' } passes ?id=<value> instead of ?requestId=<value>). The target detail page can then use variable bindings with { source: 'url', param: 'id' } to read those params.`, {
148
148
  name: zod_1.z.string().describe("Display name for the page (e.g., 'Customer List')"),
149
149
  slug: zod_1.z.string().describe("URL-safe slug (e.g., 'customer-list')"),
150
150
  pageType: zod_1.z
@@ -223,12 +223,14 @@ Variable binding sources:
223
223
  - { source: 'static', value: 'active' } — literal default value
224
224
 
225
225
  For query data sources: variables substitute into smart query {{placeholders}}.
226
- For structure data sources: variables become equality filters.
226
+ For structure data sources: variables become equality filters. IMPORTANT: variable name 'id' is special — it fetches the record BY its system UUID. All other variable names filter against data.<fieldName> in the JSONB column (e.g., variable 'requestId' → filters on data.requestId). Reference fields between collections store system UUIDs.
227
227
 
228
228
  Common patterns:
229
- - Detail page related list: { requestId: { source: 'url', param: 'id' } }
229
+ - Detail page primary record: { id: { source: 'url', param: 'id' } } — fetches single record by system ID
230
+ - Detail page related list: { requestId: { source: 'url', param: 'id' } } — filters where data.requestId = URL param
230
231
  - User-scoped view: { assigneeId: { source: 'auth', field: 'userId' } }
231
- - Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }`, {
232
+ - Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }
233
+ - Rename params: action config { useQueryParams: true, paramMapping: { requestId: 'id' } } — passes ?id=<value> instead of ?requestId=<value>`, {
232
234
  pageId: zod_1.z.string().describe("The page ID (UUID)"),
233
235
  definition: zod_1.z
234
236
  .record(zod_1.z.string(), zod_1.z.any())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-mcp",
3
- "version": "4.2.16",
3
+ "version": "4.3.0",
4
4
  "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -1089,7 +1089,7 @@ export function registerDescribeTools(server: McpServer) {
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
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' }",
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' }. Use config.paramMapping to rename fields if source and target use different names (e.g., { requestId: 'id' })",
1093
1093
  "For user-scoped views (e.g., My Approvals): bind a variable to { source: 'auth', field: 'userId' } and use a smart query with {{assigneeId}}",
1094
1094
  ],
1095
1095
  multi_page_pattern: {
@@ -1214,8 +1214,10 @@ export function registerDescribeTools(server: McpServer) {
1214
1214
  },
1215
1215
  behavior: {
1216
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",
1217
+ structure_type: "For type:'structure' — resolved variables become equality filters on the collection. IMPORTANT: the variable name 'id' is special — it matches the system record UUID (used for single-record fetch by ID). ALL OTHER variable names filter against user data fields and are automatically prefixed with 'data.' (e.g., variable 'requestId' filters on data.requestId in the JSONB column). System columns like createdAt, updatedAt, status do NOT get the data. prefix.",
1218
1218
  unresolved: "If any variable cannot be resolved, the block returns an empty result with a variableError message — NEVER unfiltered data",
1219
+ detail_page_pattern: "For a detail page: use variables: { id: { source: 'url', param: 'id' } } on the primary record-card block to fetch by system ID. For related-list blocks, use the actual data field name (e.g., variables: { requestId: { source: 'url', param: 'id' } }) — this filters related records where data.requestId matches the URL param. The 'id' variable fetches a record BY its UUID; other variable names filter records WHERE a field equals the value.",
1220
+ reference_fields: "Reference fields (foreign keys between collections) store the target record's system UUID. To filter a related list by a reference field, bind the variable to the parent record's system ID (from URL param), not a custom field value. Example: approval_steps has a data.requestId field that stores the governance_request's UUID.",
1219
1221
  },
1220
1222
  precedence: "url > record > auth > static (highest to lowest priority)",
1221
1223
  },
@@ -1546,14 +1548,73 @@ export function registerDescribeTools(server: McpServer) {
1546
1548
  requires: "content property (string) — the HTML markup",
1547
1549
  category: "content-block",
1548
1550
  },
1551
+ "data-html": {
1552
+ description:
1553
+ "Data-bound HTML block. Write HTML templates with {{fieldName}} placeholders that resolve against a data source. Supports two modes: LIST mode (default) renders the template ONCE PER RECORD — perfect for card grids, custom list layouts, or any repeating template. SINGLE mode renders the template once for a single record (detail pages). Records are auto-flattened so {{firstName}} works directly — no need for {{data.firstName}}. Output is sanitized via DOMPurify. No JavaScript allowed.",
1554
+ dataSource: "Required — type: 'structure' or 'query'. Data is resolved server-side and injected into the template.",
1555
+ requires: "content property (string) — HTML template with {{field}} placeholders. Must have non-empty content to pass publishing validation.",
1556
+ template_syntax: "{{fieldName}} for top-level fields, {{data.status}} for nested. Unresolved placeholders render as empty string.",
1557
+ modes: {
1558
+ list: "Default. Template repeats for EACH record. Write a single card/row template and it renders N times for N records. Example: a styled customer card template produces a grid of cards.",
1559
+ single: "Template renders once for ONE record. Use with mode:'single' and a variable binding like { id: { source: 'url', param: 'id' } } to fetch a specific record.",
1560
+ },
1561
+ example_list_mode: {
1562
+ blockType: "data-html",
1563
+ dataSource: { type: "structure", ref: "<collection-uuid>", recordSlug: "customers", config: { sort: [{ field: "createdAt", direction: "desc" }], limit: 20 } },
1564
+ content: "<div style='display:inline-block; width:280px; margin:8px; padding:16px; border:1px solid #e5e7eb; border-radius:8px; vertical-align:top;'><h3>{{firstName}} {{lastName}}</h3><p style='color:#6b7280;'>{{email}}</p></div>",
1565
+ },
1566
+ example_single_mode: {
1567
+ blockType: "data-html",
1568
+ dataSource: { type: "structure", ref: "<collection-uuid>", mode: "single", config: {}, variables: { id: { source: "url", param: "id" } } },
1569
+ content: "<div style='padding:16px; border:1px solid #e5e7eb; border-radius:8px;'><h2>{{firstName}} {{lastName}}</h2><p>{{email}}</p><p>Company: {{company}}</p></div>",
1570
+ },
1571
+ IMPORTANT: "No iteration syntax needed for lists. Just write a single template — the runtime repeats it automatically for each record. Records are auto-flattened: use {{fieldName}} directly, not {{data.fieldName}}.",
1572
+ category: "data-block",
1573
+ },
1574
+ "image": {
1575
+ description:
1576
+ "Image block. Displays an image from a URL with responsive sizing, lazy loading, optional caption, alignment, and link-on-click.",
1577
+ dataSource: "Not required",
1578
+ requires: "src property (string) — the image URL. Optional: alt, caption, alignment (left|center|right), maxWidth, linkUrl.",
1579
+ category: "media-block",
1580
+ },
1581
+ "video": {
1582
+ description:
1583
+ "Video block. Embeds YouTube/Vimeo videos via iframe or direct MP4/WebM via native <video>. Auto-detects provider from URL.",
1584
+ dataSource: "Not required",
1585
+ requires: "src property (string) — video URL (YouTube, Vimeo, or direct). Optional: autoplay (boolean, muted only), loop, controls (default true), poster, caption.",
1586
+ category: "media-block",
1587
+ },
1588
+ "modal": {
1589
+ description:
1590
+ "Declarative modal block. Invisible by default — renders as an overlay when triggered by an 'open-modal' action. Content can be static HTML, markdown, or data-bound HTML. Supports actions (buttons) inside the modal for confirm/cancel flows. Uses Radix Dialog.",
1591
+ dataSource: "Optional — only needed if contentType is 'data-html'",
1592
+ requires: "content property (string), contentType ('html' | 'markdown' | 'data-html', default 'html'). Optional: title, modalSize ('sm' | 'md' | 'lg' | 'fullscreen', default 'md').",
1593
+ actions: "Optional actions array — renders as buttons at the bottom of the modal. Supports close-modal (close the modal), invoke-trigger, navigate-to-page, start-orchestration, open-modal (open another modal). Use close-modal with targetRef set to this modal's own ID for a 'Cancel' button.",
1594
+ trigger: "Add an 'open-modal' action on another block with targetRef set to this modal block's ID. The modal opens when the action fires.",
1595
+ category: "interactive-block",
1596
+ example_confirm_modal: {
1597
+ blockType: "modal",
1598
+ title: "Confirm Delete",
1599
+ modalSize: "sm",
1600
+ contentType: "html",
1601
+ content: "<p>Are you sure you want to delete this item? This action cannot be undone.</p>",
1602
+ actions: [
1603
+ { id: "cancel", type: "close-modal", label: "Cancel", targetRef: "THIS_MODAL_ID", presentation: { color: "neutral" } },
1604
+ { id: "confirm", type: "invoke-trigger", label: "Delete", targetRef: "TRIGGER_ID", presentation: { color: "destructive" } },
1605
+ ],
1606
+ },
1607
+ },
1549
1608
  },
1550
1609
  block_categories: {
1551
1610
  "data-block":
1552
- "Blocks that display data from collections (data-table, chart, metric-card, stat-group, field-display, related-list, activity-feed, kanban, calendar, progress-indicator)",
1611
+ "Blocks that display data from collections (data-table, chart, metric-card, stat-group, field-display, related-list, activity-feed, kanban, calendar, progress-indicator, data-html)",
1553
1612
  "form-block":
1554
1613
  "Use 'field-group' blockType with a 'fields' array for form pages. Do NOT use individual field block types (text-input, select, etc.) — the runtime renders field types from the field-group's fields array.",
1555
1614
  "content-block":
1556
1615
  "Static content blocks (markdown, static-html). Must have non-empty 'content' property.",
1616
+ "media-block":
1617
+ "Media blocks (image, video). Require a 'src' URL property.",
1557
1618
  "layout-block":
1558
1619
  "Blocks that control page-level behavior (filter-bar)",
1559
1620
  },
@@ -1663,16 +1724,19 @@ export function registerDescribeTools(server: McpServer) {
1663
1724
  typical_activation: "row-click",
1664
1725
  paramConfig:
1665
1726
  "Use source: 'row', mode: 'selected', selectedFields: ['id'] to pass the record ID to the detail page",
1727
+ paramMapping:
1728
+ "Optional config.paramMapping: { sourceField: 'targetParam' } to rename fields in the URL. E.g., { requestId: 'id' } passes ?id=<value> instead of ?requestId=<value>. Only applies when useQueryParams is true.",
1666
1729
  example: {
1667
1730
  id: "view-detail",
1668
1731
  type: "navigate-to-page",
1669
1732
  label: "View Details",
1670
1733
  targetRef: "customer-detail",
1671
1734
  activation: "row-click",
1735
+ config: { useQueryParams: true, paramMapping: { requestId: "id" } },
1672
1736
  paramConfig: {
1673
1737
  source: "row",
1674
1738
  mode: "selected",
1675
- selectedFields: ["id"],
1739
+ selectedFields: ["requestId"],
1676
1740
  },
1677
1741
  },
1678
1742
  },
@@ -1719,6 +1783,18 @@ export function registerDescribeTools(server: McpServer) {
1719
1783
  typical_activation: "button",
1720
1784
  executionMode: "'fire-and-forget'",
1721
1785
  },
1786
+ "open-modal": {
1787
+ description:
1788
+ "Open a modal block. Client-side only — no backend call. The modal block must be defined on the same page with blockType 'modal'.",
1789
+ targetRef: "The modal block's ID (UUID) — must match a block with blockType 'modal' on this page.",
1790
+ typical_activation: "button",
1791
+ },
1792
+ "close-modal": {
1793
+ description:
1794
+ "Close an open modal. Client-side only.",
1795
+ targetRef: "The modal block's ID (UUID) to close.",
1796
+ typical_activation: "button",
1797
+ },
1722
1798
  },
1723
1799
  tips: [
1724
1800
  "Every action needs an 'id' — use UUIDs for uniqueness",
@@ -160,7 +160,7 @@ export function registerPageTools(
160
160
  "create_page",
161
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
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.`,
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. Use config.paramMapping: { sourceField: 'targetParam' } to rename fields in the URL (e.g., { requestId: 'id' } passes ?id=<value> instead of ?requestId=<value>). The target detail page can then use variable bindings with { source: 'url', param: 'id' } to read those params.`,
164
164
  {
165
165
  name: z.string().describe("Display name for the page (e.g., 'Customer List')"),
166
166
  slug: z.string().describe("URL-safe slug (e.g., 'customer-list')"),
@@ -252,12 +252,14 @@ Variable binding sources:
252
252
  - { source: 'static', value: 'active' } — literal default value
253
253
 
254
254
  For query data sources: variables substitute into smart query {{placeholders}}.
255
- For structure data sources: variables become equality filters.
255
+ For structure data sources: variables become equality filters. IMPORTANT: variable name 'id' is special — it fetches the record BY its system UUID. All other variable names filter against data.<fieldName> in the JSONB column (e.g., variable 'requestId' → filters on data.requestId). Reference fields between collections store system UUIDs.
256
256
 
257
257
  Common patterns:
258
- - Detail page related list: { requestId: { source: 'url', param: 'id' } }
258
+ - Detail page primary record: { id: { source: 'url', param: 'id' } } — fetches single record by system ID
259
+ - Detail page related list: { requestId: { source: 'url', param: 'id' } } — filters where data.requestId = URL param
259
260
  - User-scoped view: { assigneeId: { source: 'auth', field: 'userId' } }
260
- - Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }`,
261
+ - Navigate with params: action config { useQueryParams: true } + paramConfig { source: 'row', mode: 'selected', selectedFields: ['id'] }
262
+ - Rename params: action config { useQueryParams: true, paramMapping: { requestId: 'id' } } — passes ?id=<value> instead of ?requestId=<value>`,
261
263
  {
262
264
  pageId: z.string().describe("The page ID (UUID)"),
263
265
  definition: z