Skip to main content
Glama
Pranavdmg20

pdf-extract-mcp

by Pranavdmg20

pdf-extract-mcp

CI Python License: MIT MCP

A Model Context Protocol (MCP) server that extracts structured data from unstructured PDF documents deterministically — plain text extraction plus regex/heuristic field matching, no LLM API calls at extraction time.

Features

  • Real MCP server — built on the official MCP Python SDK (2.x), speaking the protocol over stdio, SSE, or streamable HTTP. Verified by an end-to-end test that drives the actual server with the official client.

  • Schema-driven extraction — point extract_fields at any JSON Schema and get back structured JSON for exactly the fields you asked for.

  • Deterministic and inspectable — regex/heuristic matching, no LLM API calls, no hidden costs, no black box. Every extraction is repeatable and auditable.

  • Human-readable validation reports — validate_against_schema explains per field why it passed, failed, or is missing.

  • Pre-built schemas — invoice, resume, and purchase_order ship ready to use, plus synthetic sample PDFs so everything is demonstrable out of the box.

  • Graceful errors — corrupted PDFs, missing files, and bad schemas return structured errors, never stack traces.

Related MCP server: StructureAI MCP Server

What is MCP and why this is useful

Model Context Protocol is an open standard that lets AI assistants (Claude, Cursor, etc.) call external tools over a persistent, bidirectional connection. Instead of pasting PDF text into a chat and asking the model to "figure it out", an assistant can call pdf-extract-mcp directly, receive structured JSON matching a schema you supply, and act on it. Because extraction here is deterministic (regex + heuristics) — not a probabilistic model call — every result is inspectable, repeatable, and cheap. That makes it ideal for automated document pipelines (invoices to accounting, resumes to ATS, POs to procurement) where you need to know why a field was extracted a certain way.

Install

cd pdf-extract-mcp
python3 -m venv .venv
source .venv/bin/activate
make install          # pip install -e ".[dev]"  (installs the console script too)

or, with plain pip:

pip install -e ".[dev]"

The server uses the official MCP Python SDK (mcp >= 2.x, the current release line, which provides the MCPServer API). pdfplumber handles text extraction, jsonschema handles validation, and reportlab generates the sample PDFs.

Installing also provides a pdf-extract-mcp console script, so you can run the server from anywhere with:

pdf-extract-mcp                     # stdio (default)
pdf-extract-mcp --transport streamable-http --host 127.0.0.1 --port 8000

Run

python server.py

This serves MCP over stdio (the default, and what Claude Code / Claude Desktop expect). You can also expose it as a network service:

python server.py --transport streamable-http --host 127.0.0.1 --port 8000
python server.py --transport sse --host 127.0.0.1 --port 8001

Connect to Claude Code / Claude Desktop

Claude Code — add a .mcp.json to your project root:

{
  "mcpServers": {
    "pdf-extract": {
      "command": "python",
      "args": ["/absolute/path/to/pdf-extract-mcp/server.py"],
      "env": {}
    }
  }
}

Claude Desktop — add the same block to your Claude Desktop config (claude_desktop_config.json, found under ~/Library/Application Support/Claude/ on macOS):

{
  "mcpServers": {
    "pdf-extract": {
      "command": "python",
      "args": ["/absolute/path/to/pdf-extract-mcp/server.py"]
    }
  }
}

Restart the client after saving. You should see three new tools: extract_fields, validate_against_schema, and list_supported_document_types.

Tools

Tool

Purpose

extract_fields(pdf_path, schema)

Pull structured fields from a PDF matching a JSON Schema -> {"ok": true, "data": {...}}

validate_against_schema(data, schema)

Check extracted data against a schema -> pass/fail/missing report with human-readable reasons

list_supported_document_types()

List document types that ship with pre-built schemas

The schema argument of extract_fields accepts a JSON Schema object, a built-in schema name (e.g. "invoice"), or a path to a .json schema file. Built-in schemas live in schemas/:

  • invoice — vendor_name, invoice_number, total_amount, due_date (required) + issue_date, customer_name

  • resume — name, email (required) + phone, skills

  • purchase_order — po_number, vendor_name, total_amount (required) + issue_date, customer_name

Worked example

First generate the sample PDFs (already present in the repo; regenerate any time with):

python sample_pdfs/generate_samples.py

Now call extract_fields on the sample invoice using the built-in invoice schema name. In Claude Code you can just say "extract the fields from sample_pdfs/invoice.pdf using the invoice schema"; underneath it issues a tool call equivalent to:

{
  "name": "extract_fields",
  "arguments": {
    "pdf_path": "/absolute/path/to/pdf-extract-mcp/sample_pdfs/invoice.pdf",
    "schema": "invoice"
  }
}

Actual expected result:

{
  "ok": true,
  "data": {
    "vendor_name": "Acme Widgets Corp",
    "invoice_number": "INV-2024-0087",
    "total_amount": 1750.0,
    "due_date": "April 1, 2024",
    "issue_date": "March 1, 2024",
    "customer_name": "Globex Industries"
  },
  "text_length": 372
}

Feeding data into validate_against_schema with the same schema:

{
  "ok": true,
  "valid": true,
  "passed": ["customer_name", "due_date", "invoice_number", "issue_date", "total_amount", "vendor_name"],
  "failed": [],
  "missing": [],
  "summary": "Valid: all 6 present field(s) conform to the schema.",
  "error": null
}

Run these from Python directly to see it live:

import json
from tools.extract import extract_fields
from tools.validate import validate_against_schema

schema = json.load(open("schemas/invoice.json"))
result = extract_fields("sample_pdfs/invoice.pdf", schema)
print(result["data"])
print(validate_against_schema(result["data"], schema))

How MCP tool registration works in server.py

This is the heart of the project, so it is worth understanding exactly what the SDK does on your behalf.

1. Create the server object.

from mcp.server.mcpserver import MCPServer

mcp = MCPServer(
    "pdf-extract-mcp",
    title="PDF Extract MCP",
    description="Deterministic structured-data extraction from PDF documents",
    version="0.2.0",
)

MCPServer is the mcp SDK 2.x server class. It implements the MCP wire protocol: it knows how to answer the JSON-RPC messages a client sends during the MCP handshake (initialize, tools/list, tools/call, and so on). The constructor arguments are metadata — the server name (required for the protocol handshake) plus optional title/description/version that clients may surface to the user.

2. Register each tool with a decorator.

@mcp.tool()
def extract_fields(pdf_path: str, schema: dict) -> dict:
    """Extract structured fields from an unstructured PDF ..."""
    return _extract_fields(pdf_path, schema)

The decorator does three jobs for you:

  • Name registration — the function name extract_fields becomes the tool name a client uses to invoke it. (You can override it with @mcp.tool(name="...").

  • Schema inference — the SDK inspects the function's type annotations (pdf_path: str, schema: dict) and generates the tool's JSON input schema automatically. That is why the MCP client knows, before calling, that pdf_path is a string and schema is an object. This is the same pattern FastAPI uses — types are the contract.

  • Description — the docstring becomes the tool's description, which Claude reads to decide when to call the tool and with what arguments.

So when a client asks the server "what can you do?" (tools/list), the SDK responds with the name, description, and inferred input schema for each decorated function — no manual registration table to keep in sync.

3. The function body is just Python.

When a client calls the tool (tools/call with arguments), the SDK deserializes the JSON arguments, calls your function with them, and serializes the return value back over the wire. The return value is what the client sees — which is why the tools always return plain JSON-able dicts and never raise: an exception would become an opaque protocol error, while a structured {"ok": false, "error": "..."} dict is something Claude can read and react to. The actual extraction/validation logic lives in tools/extract.py and tools/validate.py so it stays unit-testable without an MCP client.

4. Run it.

if __name__ == "__main__":
    main()   # argparse -> mcp.run(transport="stdio")

mcp.run(transport="stdio") starts the protocol loop: it reads newline-delimited JSON-RPC requests from stdin, dispatches them to the registered tools, and writes responses to stdout. That is the entire server — no HTTP framework, no routes, no manual request handling. (For streamable-http / sse, the same run() call starts an internal ASGI app.)

One more detail worth noting: extract_fields uses a tiny helper _load_schema that accepts a schema dict, a built-in schema name, or a file path — so the same tool works with "invoice" or a full schema object. The actual extraction function stays strict (dict only) and the server layer handles the convenience conversions.

How extraction works (deterministic, inspectable)

  1. Text extraction — pdfplumber opens the PDF and pulls the plain text from every page.

  2. Field matching — for each property in your schema, an ordered list of regexes is tried; the first match wins (tools/extract.py -> _FIELD_PATTERNS). Patterns are most-specific-first, and unknown field names fall back to a generic "Field Name: value" match, plus a synonym table (_FIELD_ALIASES).

  3. Type coercion — matched strings are coerced to the JSON Schema type (e.g. "$1,750.00" -> 1750.0 for "type": "number"; comma-split for arrays). Coercion failures fall back to the raw string rather than losing data.

  4. Validation — validate_against_schema re-checks the extracted data with the jsonschema package and reports, per field, whether it passed, failed (with a human-readable reason), or is missing entirely.

Because every step is plain code, you can trace exactly why a field was or wasn't extracted — no black box.

Error handling

All three tools return structured JSON on every path — they never raise a stack trace across the MCP boundary:

  • Corrupted/unreadable PDF -> {"ok": false, "error": "Could not read PDF ..."}

  • Missing file -> {"ok": false, "error": "PDF not found: ..."}

  • PDF with no extractable text -> {"ok": false, "error": "... contains no extractable text."}

  • Invalid schema (empty, no properties, or invalid JSON Schema) -> structured error key

  • Missing required fields -> listed in "missing"; malformed values -> listed in "failed" with reasons

Tests

pytest tests/ -v

19 tests covering:

  • Successful extraction for all three document types (invoice, resume, purchase_order)

  • A PDF missing required fields (negative extraction)

  • Schema validation catching a malformed field type, missing required fields, enum/pattern violations

  • Error paths: corrupted PDF, nonexistent file, textless PDF, invalid schema

  • A real end-to-end MCP test (tests/test_mcp_end_to_end.py) that spawns server.py as a subprocess, connects over stdio with the official MCP client, and calls all three tools over the wire — proving this is a genuine MCP server, not a library pretending to be one

Sample PDFs are auto-regenerated by tests/conftest.py if missing.

Repository layout

pdf-extract-mcp/
  server.py                    # MCP server: MCPServer + tool registration + transports
  tools/
    __init__.py
    extract.py                 # pdfplumber text extraction + regex field matching
    validate.py                # jsonschema validation with structured reports
  schemas/
    invoice.json               # pre-built schema: invoice
    resume.json                # pre-built schema: resume
    purchase_order.json        # pre-built schema: purchase_order
  sample_pdfs/
    generate_samples.py        # reportlab generator for the 4 sample PDFs
    invoice.pdf
    invoice_missing_fields.pdf
    resume.pdf
    purchase_order.pdf
  tests/
    conftest.py                # auto-generates sample PDFs if missing
    test_tools.py              # unit tests for extract/validate
    test_mcp_end_to_end.py     # end-to-end test over the real MCP stdio transport
  README.md
  requirements.txt

Troubleshooting

  • ModuleNotFoundError: No module named 'mcp' — you are not in the virtualenv: source .venv/bin/activate (or use ./.venv/bin/python server.py).

  • FastMCP import errors — server.py targets the mcp 2.x API (MCPServer). If your environment has mcp 1.x, reinstall with pip install -U "mcp>=2.0".

  • Tools not showing up in Claude — restart the client after editing the config, and make sure "args" points at the absolute path to server.py, using the venv's python as the command if needed.

  • Extraction misses a field — add a pattern for it in _FIELD_PATTERNS in tools/extract.py (or rely on the generic "Field Name: value" fallback and the synonym table).

Available Tools

3 tools
extract_fieldsB

Extract structured fields from an unstructured PDF using regex/heuristics.

Args: pdf_path: Path to the PDF file. schema: JSON Schema describing the fields to extract (must have a non-empty 'properties' object). A built-in schema name (e.g. "invoice") or a path to a .json schema file is also accepted.

Returns: {"ok": True, "data": {...}, "text_length": N} on success, or {"ok": False, "error": "..."} on failure (corrupt PDF, bad schema, ...).

ParametersJSON Schema
NameRequiredDescriptionDefault
schemaYes
pdf_pathYes

TDQS

B3.4/5.0
Behavior3/5

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

No annotations are provided, so the description carries the full disclosure burden. It does disclose failure modes (corrupt PDF, bad schema), the exact return contract ({ok: True/False}), and the non-empty properties requirement. However, it omits the side-effect profile (e.g., read-only disk access) and edge behaviors such as empty extraction results, so disclosure is solid but not exhaustive.

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?

Purpose is front-loaded in a single line, followed by a clean Args/Returns structure that is scannable. Slightly padded by trailing ellipses and the '...' after the failure list, but overall efficient with no redundancy.

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

Completeness4/5

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

With no annotations and no output schema, the description covers the return format, failure modes, and semantic details of both parameters—enough to call the tool correctly. Gaps are minor: no sibling-routing guidance and the meaning of the returned 'text_length' field is undefined.

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

Parameters4/5

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

Schema coverage is 0%, so the description must supply meaning. It adds substantial context: schema may be a JSON Schema object, a built-in name like 'invoice', or a .json file path, and must have a non-empty 'properties'; pdf_path is explained as a path to the PDF. This goes well beyond the bare type declarations.

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

Purpose4/5

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

The opening sentence states a specific verb (extract), a resource (structured fields), and a source/method (unstructured PDF using regex/heuristics). This clearly distinguishes extraction behavior from list_supported_document_types, though it doesn't explicitly name the differentiation from validate_against_schema.

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

Usage Guidelines2/5

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

No guidance on when to choose this tool versus its siblings. It doesn't mention when validate_against_schema or list_supported_document_types would be the better pick, and states no preconditions or exclusions beyond the inline schema-constraint note in Args. The agent must infer usage context.

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

list_supported_document_typesB

List document types with pre-built schemas (e.g. invoice, resume, purchase_order).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

B3.3/5.0
Behavior2/5

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

With no annotations, the description carries the full burden of behavioral disclosure. It weakly implies a read-only operation (listing) but does not explicitly state side effects, authentication requirements, rate limits, or response behavior. The existence of an output schema covers return format, but not behavioral traits. A simple 'List' implies read-only, but for a complete definition more context is expected.

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, concise sentence that leads with the action and then clarifies with concrete examples. There is no filler, and every word contributes to understanding the tool's purpose.

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

Completeness4/5

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

Given the tool has no parameters, an output schema exists, and the purpose is simple, the description covers the essentials. It explains what the tool does and gives representative examples. It could add a note about usage direction (e.g., that these types can be used with extract_fields or validate_against_schema), but such cross-referencing is not required for basic invocation.

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

Parameters4/5

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

The tool has zero parameters, and the input schema is an empty object. According to the rubric, a parameterless tool gets a baseline 4. The description adds no parameter-specific information, but none is needed; the schema is sufficient.

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

Purpose4/5

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

The description uses a specific verb 'List' and a clear resource 'document types with pre-built schemas', including helpful examples (invoice, resume, purchase_order). It clearly conveys the tool's function, though it does not explicitly distinguish it from siblings like extract_fields or validate_against_schema, so it falls just short of a 5.

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

Usage Guidelines2/5

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

No guidance is provided on when to use this tool versus the sibling tools. The description does not mention alternatives, typical usage scenarios, or any conditions that would make this tool the right choice. This is a clear gap.

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

validate_against_schemaA

Validate extracted data against a JSON Schema.

Args: data: The data to check (e.g. the "data" from extract_fields). schema: JSON Schema to validate against.

Returns: A structured report with passed/failed/missing field lists and a human-readable reason for each failure. Never raises: invalid schemas are reported via the "error" key instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
dataYes
schemaYes

TDQS

A4.3/5.0
Behavior4/5

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

With no annotations, the description carries the full burden. It discloses key behavior: never raises exceptions, invalid schemas are reported via an 'error' key, and the return is a structured report with passed/failed/missing lists and human-readable reasons. This goes beyond a generic 'validates' and gives the agent a clear expectation of failure modes.

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 efficiently formatted with an Args and Returns section, front-loaded with the purpose sentence. It is not verbose—every line contributes to the agent's understanding. It could be tightened (the Returns section is slightly redundant with the behavioral note), but it's well-structured and clear.

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

Completeness4/5

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

Given two required parameters, no output schema, and no annotations, the description covers the essential context: what the tool does, what each parameter is, how errors are handled, and what the response contains. It references the sibling workflow (extract_fields) which aids routing. The only minor gap is that it doesn't specify the expected shape of 'data' beyond being an object, but the example mitigates this.

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

Parameters4/5

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

Schema coverage is 0%, so the description must explain the parameters. It does: 'data' is explicitly the output of extract_fields, and 'schema' is a JSON Schema. This adds meaning beyond the bare 'object' type in the schema and gives the agent the necessary context for both arguments. It could add more detail about expected structures, but it is sufficient for a two-parameter tool.

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 clear verb+resource: 'Validate extracted data against a JSON Schema.' It names the specific tool and its input, and the example referencing extract_fields ties it to the workflow. It is clearly distinct from the siblings (list_supported_document_types and extract_fields) because validation is an entirely separate operation.

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

Usage Guidelines4/5

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

The description gives clear context for when to use it: after extraction, with the 'data' from extract_fields. It implies the pipeline (extract → validate) without explicitly saying 'use this when...' but the example makes the usage obvious. It does not mention when not to use it or name alternatives, so it lacks an explicit exclusion but is not misleading.

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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.2.0
    • First observedextract_fields
    • First observedlist_supported_document_types
    • First observedvalidate_against_schema

TDQS

A3.8/5.0

Scored across 3 tools

Disambiguation5/5

Each tool serves a clearly distinct purpose: listing supported schemas, extracting fields from a PDF, and validating extracted data against a schema. There is no overlap or ambiguity in tool responsibilities.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case: list_supported_document_types, extract_fields, validate_against_schema. The convention is uniform and predictable.

Tool Count4/5

With only 3 tools, the set is at the lower end of the well-scoped range but still appropriate for a focused PDF extraction and validation server. Each tool earns its place without redundancy.

Completeness4/5

The tool surface covers the core workflow: discovering available schemas, extracting fields, and validating results. Minor gaps exist (e.g., no tool to add custom schemas or handle batch processing), but they are not critical for the primary purpose.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    Enables AI-powered extraction and analysis of PDF documents with 40+ specialized tools for text, tables, images, layout analysis, security assessment, and document intelligence. Supports both text-based and scanned PDFs with OCR capabilities.
    134 PyPI
    10
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    Extracts structured JSON data from unstructured text using predefined schemas for receipts, invoices, resumes, and emails. It allows users to transform messy text into organized data through built-in or custom-defined fields.
    1
    -
  • A
    license
    A
    quality
    D
    maintenance
    Enables RAG over messy PDFs — extract, chunk, embed, and search scanned, multi-column, and table-heavy documents.
    6
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Extracts text and tables from PDFs for AI agents via MCP, enabling structured data retrieval from invoices, reports, and statements.
    28 PyPI
    1
    MIT