mcp-pdf-tools/pyproject.toml
Ryan Malloy f68e1b77d3
Some checks are pending
Security Scan / security-scan (push) Waiting to run
v2.3.0: XFA (dynamic Adobe LiveCycle) form support
Dynamic XFA forms — real-estate forms, mortgage forms, government forms —
have been a silent failure mode for every tool in this server: the layout
and fields live in an XFA program that only Adobe's runtime executes, so
PyMuPDF/pdfium/MuPDF only see the "Open in Adobe Reader" placeholder page.
extract_form_data returned "document closed", convert_to_images returned
the placeholder, analyze_pdf_health reported total_pages=1 — all
correctly per the visible PDF, but all misleading about what the form
actually contains.

New capabilities:

- src/mcp_pdf/xfa.py — XFA detection, packet extraction, field parsing,
  classification. No new deps (pypdf + stdlib ElementTree). Lifted from
  a working prototype with attribution preserved; parameterized for
  producer profiles.

- is_xfa_pdf — MCP tool to detect XFA presence + classify dynamic vs
  static. Use for branching BEFORE extract_form_data or convert_to_images.

- extract_xfa_fields — MCP tool that parses the XFA template for field
  names, captions, UI types. Splits fields into shared (cross-form
  canonical Global_Info-* vocabulary), positional (opaque codes like
  p01tf022), and plumbing (producer internals, dropped). Defaults to
  the zipForm producer profile; pass profile="generic" + custom regex
  patterns for other producers. canonical_separator selects _ / . / -.

UX fixes for existing tools (no more cryptic failures on dynamic XFA):

- extract_form_data — diagnoses dynamic XFA and returns
  {error, hint: "extract_xfa_fields"} instead of "document closed"
- convert_to_images — still produces the rendered image (caller may
  want the placeholder), but now warns that it's not the real form
- analyze_pdf_health — surfaces is_xfa + xfa_type in document_stats,
  adds a warning for dynamic XFA

Cross-tool alignment:

- extract_form_data field-type strings aligned to the same six-term
  vocabulary as XFA (text/checkbox/radio/dropdown/date/signature +
  button/unknown edge categories). listbox + combobox both collapse
  to "dropdown" — the widget-hover distinction wasn't semantic.

Tests: 31/31 passing against the synthetic XFA fixture
(tests/fixtures/xfa/synthetic_dynamic_xfa.pdf). The fixture is
hand-built, license-clean, ~2 KB, and exercises all three classification
categories including the "denylist beats shared-prefix" invariant
(Global_Info-Invisibind-Test drops despite the shared prefix).

Build hygiene:

- .gitignore exception !tests/fixtures/**/*.pdf so fixtures survive
  the global *.pdf rule
- pyproject.toml force-include for the fixture (uses hatchling's
  force-include not include — the latter is restrictive, not additive)

Coordinated via the agent-thread protocol; full design history is
in docs/agent-threads/xfa-form-support/ (excluded from the sdist).
2026-06-09 00:45:46 -06:00

161 lines
4.4 KiB
TOML

[project]
name = "mcp-pdf"
version = "2.3.0"
description = "Secure FastMCP server for comprehensive PDF processing - text extraction, OCR, table extraction, forms, annotations, and more"
authors = [{name = "Ryan Malloy", email = "ryan@malloys.us"}]
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
keywords = [
"mcp",
"fastmcp",
"pdf",
"ocr",
"text-extraction",
"table-extraction",
"pdf-processing",
"api",
"integration"
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: General",
"Topic :: Office/Business",
]
dependencies = [
"fastmcp>=0.1.0",
"httpx>=0.25.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"PyMuPDF>=1.23.0",
"pdfplumber>=0.10.0",
"pytesseract>=0.3.10",
"pdf2image>=1.16.0",
"pypdf>=6.0.0",
"pandas>=2.0.0",
"Pillow>=10.0.0",
"markdown>=3.5.0",
]
[project.urls]
Homepage = "https://github.com/rsp2k/mcp-pdf"
Documentation = "https://github.com/rsp2k/mcp-pdf#readme"
Repository = "https://github.com/rsp2k/mcp-pdf.git"
Issues = "https://github.com/rsp2k/mcp-pdf/issues"
Changelog = "https://github.com/rsp2k/mcp-pdf/releases"
[project.scripts]
mcp-pdf = "mcp_pdf.server:main"
mcp-pdf-legacy = "mcp_pdf.server_legacy:main"
mcp-pdf-modular = "mcp_pdf.server_refactored:main"
[project.optional-dependencies]
# Form creation features (create_form_pdf, advanced form tools)
forms = [
"reportlab>=4.0.0",
]
# Advanced table extraction (camelot needs Ghostscript, tabula needs Java)
tables = [
"camelot-py[cv]>=0.11.0",
"tabula-py>=2.8.0",
]
# Markdown → PDF conversion (requires pandoc binary + a PDF engine such as
# xelatex, pdflatex, tectonic, weasyprint, or wkhtmltopdf)
markdown = [
"pypandoc>=1.13",
]
# All optional features
all = [
"reportlab>=4.0.0",
"camelot-py[cv]>=0.11.0",
"tabula-py>=2.8.0",
"pypandoc>=1.13",
]
# Development dependencies
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
"build>=0.10.0",
"twine>=4.0.0",
"safety>=3.0.0",
"pip-audit>=2.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Keep dev-only artifacts out of the sdist that ships to PyPI.
# (The PII audit also runs against the unpacked sdist before each publish —
# see ~/.claude/rules/python.md.)
[tool.hatch.build.targets.sdist]
# force-include re-adds files that gitignore (e.g. `*.pdf`) would otherwise
# block — the fixture under tests/fixtures/ is small, deliberate, and required
# by tests/test_xfa.py at runtime. Unlike `include`, force-include is additive,
# not restrictive.
force-include = {"tests/fixtures/xfa/synthetic_dynamic_xfa.pdf" = "tests/fixtures/xfa/synthetic_dynamic_xfa.pdf"}
exclude = [
"CLAUDE.md", # operator-private project context
".env", ".env.local", # never ship credentials
".mcp.json", # contains local filesystem paths
".pytest_cache/",
".ruff_cache/",
"build/",
"dist/",
"examples/page_001.png",
"examples/*.pdf", # demo PDFs are large + not needed by end users
"examples/test_demo.*",
"tests/CopperSprings_DigitalPortfolio.pdf", # large fixture PDF
"**/page_*.png", # convert_to_images output artifacts (test side-effects)
"docs/agent-threads/", # project coordination history, not for end users
"test_security_features.py",
"test_integration.py",
"MCPMIXIN_*.md", # internal architecture/migration notes
"MCP_DOCX_TOOLS_PLAN.md",
"claude-mcp-manager", # personal helper script
"run-mcp-server.sh",
"docker-compose.yml",
"Dockerfile",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "-v --tb=short"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
[tool.hatchling.build.targets.sdist]
include = [
"/src",
"/tests",
"/examples",
"README.md",
"LICENSE",
"MANIFEST.in",
]
[dependency-groups]
dev = [
"pip-audit>=2.9.0",
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
"pytest-cov>=6.2.1",
"reportlab>=4.4.3",
"safety>=3.2.11",
"twine>=6.1.0",
]