Skip to main content
Glama
okra-project

okraPDF PDF MCP server

Official
by okra-project

Execute Code

execute_code

Execute JavaScript to query, search, read, extract, and cite PDF documents, returning JSON or HTML views.

Instructions

Execute JavaScript against Code Mode namespaces — the primary tool for exploring, querying, grepping, reading, and citing documents (results come back here). For a finished chart, table, metric, or HTML display, pass the working query to render_ui instead.

Code Mode API: docs.list() → [{id, file_name, status, total_pages}] docs.status({ docId }) → {phase, totalPages, totalNodes, ...} docs.read({ docId, pages? }) → {markdown, page_count, total_pages, truncated?} docs.ask({ docId, question }) → {answer, citations, trace_id} (LLM — slow, use sparingly) docs.extract({ docId, prompt, jsonSchema }) → {data, trace_id} (LLM — slow, use sparingly) sql.query({ docId, sql }) → {rows, count} — read-only SQL on one doc; rows include document_id sql.queryMany({ docIds, sql }) → {results:[{docId, rows, count}], count} — run the same query across docs sql.search({ query, docId?, docIds? }) → FTS5 keyword/phrase search across one doc or the current collection sql.grep({ query, docId?, docIds?, pages? }) → literal substring search across node values; useful when FTS tokenization is too strict citation.create({ claim, nodes, docId?, minQuality? }) → {id, url, verification} — mint a shareable link.okrapdf.com/c/{id} proof card; minQuality defaults to "high". citation.batch({ items }) → {citations:[{id,url,...}], count} — mint many proof links from multi-doc results.

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 tablerowcell 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).

Prefer SQL/FTS for discovery, filtering, keyword search, counting, and reading content. SQL is fast, free, and deterministic. Only fall back to reading full pages when SQL doesn't have what you need. When answering numerical or factual questions, use sql.query() to find exact values, then compute the answer step by step. If the first query doesn't return what you need, try a different query — iterate rather than guessing from partial data. IMPORTANT: nodes_fts rows contain full page text (5KB+ each). Always use substr() and JOIN nodes for page_number: 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 'keyword' LIMIT 10 High-grade citation workflow:

  • Never cite from memory or from a generated answer alone. First use SQL/FTS/search to find exact candidate nodes.

  • For MCP users, prefer verify_source when you have a claim plus rough locator/pages/quote; pass pdf_url + page/pages for eager one-page public-PDF verification without full ingest, or document_id for parsed-node verification. It renders found/not-found/multiple-candidate states as an inline card.

  • verification_requirement controls what counts as verified. The default is verification_requirement="bbox": require coordinate evidence and only mint a proof link when a bbox is available. Pass verification_requirement="text_match" only when deterministic page-text/text-layer matching is sufficient; it returns a found state without a durable proof link, page-image proof, or bbox-backed confidence claim.

  • If you already have exact rows, pass rows that include id, document_id, page_number, value, bbox_x/y/w/h, confidence, and status to verify_source mode="exact_node" or citation.create.

  • For user-facing, audit, legal, or financial claims, visually verify the page/bbox with view_document or review_extraction before presenting the citation as final when the card says multiple_candidates, not_found, or low confidence.

  • Treat the returned verification_grade, verification_score, and verification_warnings as the citation quality gate; if warnings mention missing bbox, low confidence, or unverified status, disclose that or find better evidence. Only use ask/extract when the answer requires LLM reasoning, not data lookup.

Page images: https://api.okrapdf.com/v1/documents/{docId}/pg_{N}.png

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesAsync JavaScript body to execute against your document library. Runs in a V8 isolate. No network, no npm — but ESM imports work at render time (esm.sh). Code Mode namespaces: library.list() → [{id, file_name, status, total_pages}] — your whole document library (docs.list = deprecated alias) library.search({ name?, status?, minPages?, maxPages?, limit?, offset? }) → {documents,count} — filter the whole LIBRARY by name/metadata (vs sql.search, which searches text INSIDE a doc) docs.status({ docId }) → {phase, totalPages, totalNodes, ...} docs.read({ docId, pages? }) → {markdown, page_count, total_pages, truncated?} docs.ask({ docId, question }) → slower LLM answer over one doc docs.extract({ docId, prompt, jsonSchema }) → structured LLM extraction sql.query({ docId, sql }) → {rows, count}; stamps rows with document_id sql.queryMany({ docIds, sql }) → {results:[{docId, rows, count}], count} sql.search({ query, docId?, docIds? }) → FTS5 keyword/phrase search sql.grep({ query, docId?, docIds?, pages? }) → literal substring search citation.create({ claim, nodes, docId? }) → {id, url, verification} citation.batch({ items }) → {citations:[{id,url,...}], count} Action verbs (orchestrate: parse · tools · publish — on docs you OWN): docs.engines() → {engines:[{id,tier,category}], count} — the parse-engine menu (free) docs.parse({ docId, vendor }) → re-parse with an explicit engine id (pin/switch); waits, returns {phase, totalNodes, completed} docs.publish({ docId }) → publish a finished doc to a public read URL → {publicDocId, url} tools.extract({ docId, prompt, jsonSchema }) → okrapdf /extract (structured data) tools.to_json({ docId, schema }) → okrapdf /pdf-to-json (document → JSON in your schema) // Compare engines: call docs.parse per vendor (a dynamic workflow can parallel() them) — each call re-parses the same doc. 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) Return values: return object/array → rendered as JSON viewer on embed return "<div>..." → auto-wrapped in HTML shell (dark theme, system-ui) return "<!DOCTYPE…" → served as full HTML document (you control everything) return { html, scripts } → html body + scripts block, auto-wrapped in shell Rendering HTML views: Return an HTML string. Fragments like "<h1>Hello</h1>" are auto-wrapped in a shell. For full control, return a complete document starting with "<!DOCTYPE html>". For interactive views, use ESM imports — the CSP allows esm.sh, jsdelivr, cdnjs: return `<div id="app"></div> <script type="module"> import { Chart } from 'https://esm.sh/chart.js@4'; // Chart.js, React, D3, Plotly, Three.js — any ESM package works </script>`; Page images available at: https://res.okrapdf.com/v1/documents/{docId}/pg_{N}.png Common SQL: Page content: SELECT value FROM nodes WHERE page_number = 5 ORDER BY sort_order FTS search: 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 Cross-doc: sql.queryMany({ docIds, sql }) IMPORTANT: nodes_fts rows are full page text (5KB+). Always use substr() to avoid huge results.
public_doc_idsNoOptional public doc IDs to mount alongside the authenticated user’s private docs for mixed private/public multi-document analysis.
Behavior5/5

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

With no annotations, the description carries the full burden, and it delivers: it discloses the V8 isolate execution model, network/npm restrictions, ESM import behavior at render time, LLM slowness for ask/extract, return value conventions, and the large FTS row size pitfall. This goes far beyond a basic 'runs code' statement, giving the agent a realistic model of side effects and constraints.

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

Conciseness4/5

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

The description is front-loaded with purpose and well-organized using line breaks and lists. However, it is very long and duplicates the API list already present in the `code` parameter description, which wastes some space. Still, the structure makes it navigable, and the density is justified by the tool's complexity.

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?

For a complex code-execution tool with no output schema, the description is effectively a mini-manual. It covers the execution environment, available namespaces, SQLite schema, node data model, return conventions, page image URLs, and citation workflow, leaving very little ambiguity about how to invoke and interpret the tool.

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

Parameters5/5

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

Although schema coverage is 100%, the description adds deep semantic value: it explains the node data model and two table shapes, gives best-practice SQL patterns (substr + JOIN), clarifies citation verification requirements, and distinguishes library.search from sql.search. This materially helps the agent write correct code for the `code` parameter and understand `public_doc_ids` in a multi-doc context.

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 pair: 'Execute JavaScript against Code Mode namespaces' and immediately positions it as 'the primary tool for exploring, querying, grepping, reading, and citing documents'. It explicitly contrasts with render_ui for finished displays, clearly distinguishing this tool from siblings.

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?

Provides explicit when-to-use guidance: use this for exploration/querying/citing, and 'pass the working query to render_ui instead' for polished UI outputs. Internally, it directs users to prefer SQL/FTS for discovery, to use ask/extract sparingly, and to iterate on queries. It also references verify_source as the preferred citation workflow for MCP users, making alternatives explicit.

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