Skip to main content
Glama
okra-project

okraPDF PDF MCP server

Official
by okra-project

Render UI

render_ui

Visualize 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().

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesJavaScript 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>"); }
titleNoOptional fallback title for the rendered UI.
heightNoPreferred inline widget height in pixels.
document_idNoOptional primary okraPDF document ID to mount for this visualization. Use after resolve_pdf_url when the user points at a public PDF.
public_doc_idsNoOptional public document IDs to mount alongside the authenticated user library for multi-document visualizations.
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden and delivers: SQL failures are codemode errors, the renderer only receives the UI payload after codemode succeeds, the sandbox is isolated with no network, and external CDN libs are blocked by CSP. It also explains the module ecosystem and return formats, providing substantial behavioral context beyond a simple 'renders UI' statement.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single dense paragraph that front-loads purpose, then workflow, then constraints. Every sentence earns its place—no filler. For a tool with this complexity, the length is justified and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity, the description covers prerequisites (describe_collection, execute_code), execution environment (sandbox, no network), error handling, return types (dashboard, chart, table, metric, html), and critical constraints (CSP, no CDN). The schema enriches with data model details and examples, making the overall context highly complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema coverage is 100%, with descriptions for code and document_id already covering meaning. The description adds workflow-level guidance (validate SQL separately, keep query code separate) but does not add new parameter-specific semantics beyond what the schema already provides. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource: 'Run codemode against docs/sql/citation and render the returned visualization as an MCP App.' It clearly scopes the tool to charts, tables, metrics, and small dashboards derived from PDFs, and distinguishes it from siblings by instructing to call describe_collection first and validate exploratory SQL with execute_code before embedding.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit usage context is given: 'Use for charts, tables, metrics, and small dashboards derived from PDFs after you know the schema.' It provides direct workflow guidance—call describe_collection first, validate with execute_code—and states constraints: return compact data, not screenshots, and avoid CDN scripts. This is clear when-to-use and when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/okra-project/pdf-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server