<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peter Ndumia</title>
    <description>The latest articles on DEV Community by Peter Ndumia (@peter_ndumia_pdfhaul).</description>
    <link>https://dev.to/peter_ndumia_pdfhaul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061225%2F4742b1bd-0383-4a46-82bd-bbe288603827.png</url>
      <title>DEV Community: Peter Ndumia</title>
      <link>https://dev.to/peter_ndumia_pdfhaul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peter_ndumia_pdfhaul"/>
    <language>en</language>
    <item>
      <title>Why Extracting Tables From a PDF Is Harder Than It Looks (and How We Actually Do It)</title>
      <dc:creator>Peter Ndumia</dc:creator>
      <pubDate>Wed, 19 Aug 2026 00:36:56 +0000</pubDate>
      <link>https://dev.to/peter_ndumia_pdfhaul/why-extracting-tables-from-a-pdf-is-harder-than-it-looks-and-how-we-actually-do-it-2llg</link>
      <guid>https://dev.to/peter_ndumia_pdfhaul/why-extracting-tables-from-a-pdf-is-harder-than-it-looks-and-how-we-actually-do-it-2llg</guid>
      <description>&lt;p&gt;If you have ever copy-pasted a table out of a PDF, you already know what happens. Rows collapse into one long line of text. Columns interleave. Numbers land in the wrong cell, or no cell at all. The table on the page looks perfectly structured, but a PDF has no real concept of "table." It only knows where individual characters sit on a page. Every extraction tool, ours included, has to reconstruct the table from scratch, using nothing but the position of each word.&lt;/p&gt;

&lt;p&gt;That gap between "looks like a table" and "is structured data" is where almost every free PDF tool falls apart. Here is how we handle it, what actually works, and where it still doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different jobs, two different tools
&lt;/h2&gt;

&lt;p&gt;PDFHaul splits this into two separate tools because they solve different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.pdfhaul.com/pdf-to-excel" rel="noopener noreferrer"&gt;PDF to Excel&lt;/a&gt;&lt;/strong&gt; rebuilds the whole document as a single spreadsheet, in the order it appears on the page: form labels, key-value pairs, section titles, and tables all together. It is for documents where you want the full content, not just the numbers, things like invoices, time sheets, and reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.pdfhaul.com/extract-tables" rel="noopener noreferrer"&gt;Extract Tables&lt;/a&gt;&lt;/strong&gt; does the opposite. It ignores everything that isn't a table and hands back one clean sheet per table, nothing else. It is for people who want structured data out, ready to sum, sort, and filter, not a copy of the document.&lt;/p&gt;

&lt;p&gt;Both tools share the same underlying geometry engine. The difference is what each one keeps and what it throws away.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Extract Tables actually decides what's a table
&lt;/h2&gt;

&lt;p&gt;The core problem with table extraction is that "looks tabular" and "is tabular" are not the same thing. A vector chart's axis box, a form's outlined signature field, and a two-column list of allergen names all produce something that a naive extractor will happily read as a grid. None of them are tables.&lt;/p&gt;

&lt;p&gt;Our pipeline handles this in four phases, all before anything is written to a spreadsheet:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: classify the page.&lt;/strong&gt; Every page is scored as bordered (has ruled lines or filled-rectangle grid lines), stream (no borders, but rows visibly span both sides of a column gap), columnar (independent parallel lists that never interact), or plain text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 and 3: run the right extractor for the layout.&lt;/strong&gt; Bordered pages go through Camelot's lattice mode. Borderless but structured pages go through Camelot's stream mode. Neither result is trusted blindly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: catch what the first pass missed, and gate everything.&lt;/strong&gt; This is the important part. Every candidate table, whether it came from Camelot, from an explicit rectangle grid, or from our own zone-based word extraction, has to pass a classifier before it is counted as a table at all. The classifier checks column count, row density, and how numeric or short the cell contents are. A form field's outline box or a chart's axis lines will fail this check and get demoted to plain text instead of becoming a two-cell "table." Without this gate, Camelot's own false positives, grid-shaped chart elements, form outlines, would get counted as extracted tables. With it, only genuine tables make it through.&lt;/p&gt;

&lt;p&gt;Underneath all four phases sits a shared geometry layer that does the actual measuring: it finds page gutters by picking the split point that divides the text most evenly (not just the widest gap, which a table's own label-and-value gap can be wider than), clusters words into lines by their vertical center rather than their top or bottom edge (so bold headers and footnote superscripts don't throw off row grouping), strips dot leaders like "Section..........12" before measuring column gaps (leaders fill the whitespace a gap detector relies on), and merges a wrapped label back onto the row it belongs to before anything gets written out.&lt;/p&gt;

&lt;p&gt;Tables that span a page break get stitched back together too. If two adjacent pages produce tables with the same column count, and the second page's first row looks like a repeat of the first page's header (fuzzy-matched, so "Week No." and "Week Number" still count as the same header), they are joined into one continuous table instead of two separate sheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;We measure this against a verified 20-PDF corpus spanning financial reports, government tenders, laws and regulations, and scientific articles, scored on both cell content and correct row and column placement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Content accuracy&lt;/th&gt;
&lt;th&gt;Structure accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PDF to Excel&lt;/td&gt;
&lt;td&gt;84% average (94% typical)&lt;/td&gt;
&lt;td&gt;79% average (89% typical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract Tables&lt;/td&gt;
&lt;td&gt;84% average (100% typical)&lt;/td&gt;
&lt;td&gt;83% average (100% typical)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average and typical (median) diverge because a handful of document types outside our core use case, dense mathematical notation and pages built around vector charts, drag the average down. Inside the core use case, financial, business, and legal documents, both tools sit at 83 to 100 percent uniformly across every file we tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's not good at yet
&lt;/h2&gt;

&lt;p&gt;We would rather tell you this directly than let you find out the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scanned PDFs&lt;/strong&gt; get real table detection too, not just text. We run OCR automatically before extraction starts, no separate step needed. For tables with visible ruled borders, the table-line detection itself runs directly on the rendered page image, so it works whether the underlying PDF has any vector graphics data or not, a scanned bordered table gets picked up the same way a native one would. The honest limit: table shape detection is one thing, text quality is another. A low-resolution or heavily degraded scan can still produce garbled cell content even when the table's structure is found correctly, since that depends on how much a scanner or camera actually captured, not on our extraction logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pages with charts&lt;/strong&gt; can leak garbage. A vector chart's axis numbers and legend entries currently extract as short text blocks alongside the real content, because the classifier correctly recognizes they aren't tables, but nothing yet recognizes they belong to a chart and should be dropped entirely, keeping only the caption. This is the single biggest gap we're working on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense academic and mathematical notation&lt;/strong&gt; is a genuinely different layout problem, and accuracy drops to 35 to 52 percent on that category. If your PDF is a math-heavy research paper, expect a rougher result than a financial statement.&lt;/p&gt;

&lt;p&gt;Outside those three cases, financial reports, government forms, contracts, business documents, this is what the tool is actually built for, and the numbers above reflect that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;For the harder cases above, badly degraded scans, dense academic layouts, chart-heavy pages, we're looking at a premium tier built on Google's Document AI, a cloud model trained specifically on document structure rather than the geometry-based approach described here. It would not replace what's already free, it would be an option for the documents that currently come out rough.&lt;/p&gt;

&lt;p&gt;So we'll ask directly instead of guessing: would you pay for more accurate extraction on your hardest documents, the ones that don't come out clean today? Tell us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.pdfhaul.com/extract-tables" rel="noopener noreferrer"&gt;Extract Tables&lt;/a&gt; and &lt;a href="https://www.pdfhaul.com/pdf-to-excel" rel="noopener noreferrer"&gt;PDF to Excel&lt;/a&gt; are both free, with no account required for a single file. Upload a PDF and see which one gives you what you need: the full document, or just the numbers.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>datascience</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Built an MCP Server So AI Assistants Can Process PDFs Directly</title>
      <dc:creator>Peter Ndumia</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:35:24 +0000</pubDate>
      <link>https://dev.to/peter_ndumia_pdfhaul/i-built-an-mcp-server-so-ai-assistants-can-process-pdfs-directly-80k</link>
      <guid>https://dev.to/peter_ndumia_pdfhaul/i-built-an-mcp-server-so-ai-assistants-can-process-pdfs-directly-80k</guid>
      <description>&lt;p&gt;I run PDFHaul, a free browser and mobile PDF toolkit, as a solo founder. A few months ago I kept running into the same friction point: every time I wanted an AI assistant to help with a PDF task, I had to manually download the file, upload it somewhere, run the tool, then feed the result back into the conversation. For anything involving more than one file, that loop got old fast.&lt;/p&gt;

&lt;p&gt;So I built an MCP server for PDFHaul. This post covers what it does, how it's built, and a few of the harder decisions along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP Actually Solves Here
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol gives an AI assistant a standard way to call external tools directly, instead of a person acting as the manual bridge between the assistant and every service it needs. For PDF work specifically, that means an assistant like Claude or Cursor can merge, split, compress, or convert a file as part of a conversation, without a human stepping out to run the tool separately.&lt;/p&gt;

&lt;p&gt;The PDFHaul MCP server exposes 12 tools across four categories: file management, editing, conversion, and compression. It's live at &lt;a href="https://www.pdfhaul.com/mcp-server" rel="noopener noreferrer"&gt;pdfhaul.com/mcp-server&lt;/a&gt;, where you can find setup instructions and generate an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The server runs on the same Node.js/TypeScript and GCP Cloud Run infrastructure as the rest of PDFHaul, which kept the initial build simpler than starting a separate service from scratch. The tools themselves wrap the existing PDF processing logic, so I wasn't rebuilding PDF handling, just exposing it through a new interface.&lt;/p&gt;

&lt;p&gt;A few decisions that took more thought than I expected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth.&lt;/strong&gt; API keys are hashed with bcrypt before storage, and requests are authenticated with a combination of the API key and a JWT. I didn't want to store anything resembling a plaintext credential, even for a free product, since API keys tend to get pasted into config files and committed by accident more often than anyone would like to admit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; In-memory rate limiting per key, tuned conservatively at first. Cloud Run's stateless nature means in-memory limits reset on cold starts, which is a real tradeoff worth knowing about if you're building something similar. I accepted it for now rather than standing up Redis for a v1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSRF prevention.&lt;/strong&gt; Several of the tools accept a URL as input (for example, fetching a PDF to process rather than uploading bytes directly). Any tool that accepts a URL from the caller is a potential server-side request forgery vector, so requests are validated against a blocklist of internal IP ranges and non-HTTP(S) schemes before the server ever fetches anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency keys.&lt;/strong&gt; AI assistants sometimes retry tool calls, whether from a timeout, a dropped connection, or the assistant itself deciding to try again. Without idempotency handling, a retried "merge these files" call could produce duplicate output or double-charge a rate limit. Each mutating tool call accepts an idempotency key so a retry returns the original result instead of redoing the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logging.&lt;/strong&gt; Every tool call is logged with enough detail to reconstruct what happened, without logging the file contents themselves. This matters more for a PDF tool than it might for other APIs, since the whole product's trust model is built around not retaining user files longer than necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Listed
&lt;/h2&gt;

&lt;p&gt;The server is listed on the &lt;a href="https://registry.modelcontextprotocol.io/" rel="noopener noreferrer"&gt;official MCP registry&lt;/a&gt;, which so far has been the main discovery channel. Compared to building a plugin or waiting on marketplace approval in other ecosystems, the registry listing process was refreshingly lightweight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Leaves Things
&lt;/h2&gt;

&lt;p&gt;Foxit and Nitro are the other PDF vendors I've seen building MCP servers, both larger companies with existing enterprise PDF products. Being early here as a solo founder is less about outcompeting them head-on and more about being genuinely useful to the AI-assistant-tooling crowd before the space gets crowded.&lt;/p&gt;

&lt;p&gt;If you're working with PDFs inside an AI assistant workflow and want to try it, the server is free to use: &lt;a href="https://www.pdfhaul.com/mcp-server" rel="noopener noreferrer"&gt;pdfhaul.com/mcp-server&lt;/a&gt;. I'm happy to answer questions about the implementation in the comments, especially on the auth or SSRF handling if anyone's building something similar.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Peter, founder of &lt;a href="https://www.pdfhaul.com" rel="noopener noreferrer"&gt;PDFHaul&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>buildinpublic</category>
      <category>node</category>
    </item>
  </channel>
</rss>
