Render UI
render_uiVisualize PDF data by running JavaScript codemode that queries SQL and renders charts, tables, metrics, or dashboards in the MCP App.
Instructions
Run codemode against docs/sql/citation and render the returned visualization as an MCP App. Use for charts, tables, metrics, and small dashboards derived from PDFs after you know the schema. Call describe_collection first, and validate exploratory SQL with execute_code before embedding it here; SQL failures are returned as codemode errors. The codemode sandbox includes the preinstalled "okra-render-ui" module with querySql() plus prebuilt component helpers like TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), and chart/table/html specs; keep query code separate from reusable UI helpers, return compact data, not screenshots, and do not rely on CDN scripts such as Chart.js inside html().
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"code": "<code>"
},
"name": "render_ui"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | JavaScript codemode source that queries docs/sql/citation and returns a UI spec for the MCP Apps render_ui widget. Runs in the same isolated V8 codemode environment as execute_code: no network, but docs/sql/citation are available. Before writing SQL, call describe_collection to inspect the document list and schema. For exploratory SQL, validate with execute_code first, then move the working query into render_ui. SQL errors are terminal tool errors, and the renderer only gets the UI payload after codemode succeeds. Preinstalled module: const { querySql, render_ui, dashboard, TimeSeriesLineChart, DataTable, MetricCards, barChart, lineChart, areaChart, scatterPlot, vegaLite, proofCard, table, metric, html } = await import("okra-render-ui.js"); SQLite tables per doc: nodes (id, parent_id, type, label, value, status, page_number, confidence, metadata, sort_order) nodes_fts — FTS5 on label+value (use MATCH) meta (key, value) page_ledger (page_number, status, pass, vendor, attempt, confidence, error) Node data model — the `nodes` table is the extracted document tree: - Each row is one element. `type` is the raw parser type and varies by vendor, so always run SELECT DISTINCT type FROM nodes first (common values: text, table, row, cell, figure, heading). - Tables come in TWO shapes depending on the parser; check with SELECT DISTINCT type: (a) a single `table` node with the whole table serialized in its `value` (e.g. the gemini-vision default), or (b) a nested `table` → `row` → `cell` tree: row.parent_id = table.id, cell.parent_id = row.id, and each `cell.value` holds one cell's text (table/row values are usually empty). For shape (b) read a grid by joining BOTH levels and ordering by sort_order; row/column headers are the first/edge cells positionally, NOT stored on `label` (which is usually NULL). - `value` is always raw text exactly as printed ("88,268", "16 %", "Sept 30, 2025") — never a typed number/date. Cast in SQL for arithmetic, e.g. CAST(REPLACE(REPLACE(value,',',''),'$','') AS REAL). Recommended modular style: keep data access in sql.query()/querySql(), then pass the rows into a prebuilt component helper such as TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), barChart(), lineChart(), table(), or metric(). This mirrors product UI components without hardcoding screenshots or CDN chart libraries. Return one of — dashboard()/chart/table/metric helpers render inside Okra's bundled MCP App without external libraries. html() is the flexible escape hatch for fully custom layouts, but external scripts/resources are blocked by the MCP App CSP; do not use CDN libraries such as Chart.js, Plotly, or D3. Use dashboard(), chart helpers, or inline SVG/CSS instead: - html("<h1>...</h1>") or { type: "html", html: "<!doctype html>..." } - TimeSeriesLineChart({ title, labels, series: [{ key, label, data }] }) for PostHog-style time-series data - dashboard({ title, items: [barChart(...), lineChart(...), metric(...), table(...)] }) - barChart({ title, data, x, y, series? }) - lineChart({ title, data, x, y, series? }) - vegaLite({ mark, data: { values }, encoding }) for constrained Vega-Lite-like grouped bars / multi-series lines - proofCard({ claim, page_image, bbox, text, url }) to render citation proof inline - scatterPlot({ title, data, x, y, series? }) - table({ title, rows, columns? }) - metric({ title, metrics: [{ label, value, delta? }] }) - any object/array, which renders as JSON/table fallback Examples: // 1) bar chart of blocks per page async () => { const { querySql, barChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number, count(*) AS blocks FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return barChart({ title: "Blocks per page", data: rows, x: "page_number", y: "blocks" }); } // 2) query first, then use a reusable product-style component async () => { const { querySql, TimeSeriesLineChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number AS day, count(*) AS user_count FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return TimeSeriesLineChart({ title: "Blocks over pages", labels: rows.map((r) => String(r.day)), series: [{ key: "blocks", label: "Blocks", data: rows.map((r) => Number(r.user_count)) }], }); } // 3) nested table → row → cell grid (only when tables are shape (b); cell.value holds each cell) async () => { const { querySql, DataTable } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT r.sort_order AS row, c.sort_order AS col, c.value FROM nodes t JOIN nodes r ON r.parent_id = t.id AND r.type = 'row' JOIN nodes c ON c.parent_id = r.id AND c.type = 'cell' WHERE t.type = 'table' ORDER BY t.id, r.sort_order, c.sort_order LIMIT 100", { docId }); return DataTable({ title: "Table cells", rows }); } // 4) FTS keyword search rendered as custom HTML via html() async () => { const { querySql, html } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT n.page_number, substr(n.value,1,200) AS excerpt FROM nodes_fts f JOIN nodes n ON n.id = f.node_id WHERE nodes_fts MATCH 'revenue' LIMIT 10", { docId }); return html("<ul>" + rows.map((r) => "<li>p" + r.page_number + ": " + r.excerpt + "</li>").join("") + "</ul>"); } | |
| title | No | Optional fallback title for the rendered UI. | |
| height | No | Preferred inline widget height in pixels. | |
| document_id | No | Optional primary okraPDF document ID to mount for this visualization. Use after resolve_pdf_url when the user points at a public PDF. | |
| public_doc_ids | No | Optional public document IDs to mount alongside the authenticated user library for multi-document visualizations. |