doc-extract
doc-extract
Ein MCP-Server, der genau eine Sache tut: PDF rein → das gesamte Dokument als validiertes strukturiertes JSON raus.
Gebaut für diesen Workflow:
[1] User drops a document
[2] doc-extract MCP ← this repo. Reads the WHOLE document, returns JSON
[3] DB node → insert into NeonDB (separate node)
[4] Agent node → chats over the NeonDB content (separate node)
[5] Or: the team acts on the JSON directly, with no DB at allDie Schritte 3, 4 und 5 sind bewusst NICHT die Aufgabe dieses Servers. Er hat keinen Datenbanktreiber und jedes Tool ist schreibgeschützt.
Keine Datenbank. Keine Seiteneffekte. Keine Persistenz. Jedes Tool ist schreibgeschützt. Was als Nächstes passiert – Einfügen, Schwärzen, Weiterleiten, Benachrichtigen – ist ein separater Knoten im MagOneAI-Workflow.
Umfang, durchgesetzt und nicht nur behauptet
Dieser Server tut | Dieser Server tut NICHT |
Liest die gesamte Textebene eines PDF | In eine Datenbank schreiben |
Rekonstruiert Tabellengeometrie | E-Mails senden oder benachrichtigen |
Repariert umgebrochene Zellen | Das PDF schwärzen oder verändern |
Validiert das Gelesene | Entscheiden, was als Nächstes passiert |
Gibt JSON + Koordinaten zurück | Etwas zwischen Aufrufen speichern |
Durchsetzung, damit Scope-Creep strukturell schwer ist:
Alle drei Tools sind mit
readOnlyHint: true,destructiveHint: false,idempotentHint: trueannotiert. Ein Orchestrator kann sehen, dass ein erneuter Versuch sicher ist.extract()ist eine reine Funktion der PDF-Bytes. Gleiche Eingabe → gleiche Ausgabe.Das Neuladen von Profilen ist eine HTTP-Admin-Route, kein MCP-Tool. Konfigurationsänderungen sind eine Operator-Aktion; ein Workflow-Agent darf nicht die Wahl haben, eine solche durchzuführen.
Nichts wird auf die Festplatte geschrieben, außer einer temporären Datei für das eingehende PDF.
Related MCP server: MCP PDF Reader Server
Warum keine OCR
Beide Beispieldateien sind Crystal-Reports-Exporte aus SAP Business One – eingebettete Schriftarten, keine Rasterbilder. Jedes Zeichen trägt bereits exakte Seitenkoordinaten. OCR würde das rastern und diese Koordinaten mit Fehlern neu ableiten.
Die umgebrochene BP Ref. No. ist ein Layout-Rekonstruktionsproblem:
Zeile | Token | x0 | x1 |
278.7 |
| 124 | 164 |
288.4 |
| 124 | 142 |
00007 sitzt exakt am linken Rand der BP-Ref-Spalte → gleiche Zelle →
SI/08781/CN/00007.
Am wichtigsten ist das bei der Nutripharm-Datei, wo Fragmente nackte Ziffern sind. Beim Lesen von Fließtext würde 111 plausibel an den Betrag angehängt und ergäbe -8.762,513111. Die Koordinaten sagen x0=124, nicht x≈450, also ist es die Referenz (N-CINV-01999111) und der Betrag bleibt -8.762,513.
Das gesamte Dokument, immer
Die Profilparsung beantwortet „Was sind die Positionszeilen?" und ignoriert alles andere. Das reicht für die Schritte 2, 4 und 5 nicht, daher läuft die Volltext-Extraktion auf jedem Dokument, ob passend oder nicht, und erzeugt vier Ansichten desselben Inhalts:
Feld | Was es ist | Wofür verwenden |
| Das Dokument, gerendert für ein LLM | Chatten. Das speichern. |
| Klartext | Suche, Embeddings |
| Jedes | Filter, Lookups |
| Typisierte, geordnete, positionierte Blöcke | Programmatische Nutzung, Schwärzung |
| Markdown, an Überschriften geteilt | Retrieval bei langen Dokumenten |
| Jede Tabelle als Spalten + Zeilen | Rendering, Export |
| Typisiert + validiert | SQL-Aggregation |
Ein Dokument ohne Profil ist kein toter Endpunkt mehr. Es liefert parsed_without_profile mit vollständig befülltem content – so kann das Team es speichern, damit chatten und darauf reagieren, bevor jemand ein Profil schreibt. Ein Profil ergänzt nur typisierte Positionszeilen und Querprüfungen obendrauf.
Warum Markdown das Artefakt für Chat ist
Ein Agent, der gefragt wird „Wie hoch ist der Saldo für One World?", antwortet weit zuverlässiger, wenn er das hier liest, als wenn er Zeilen aus JSON wieder zusammensetzt oder einen rohen Textdump scannt:
# ONE WORLD TRADING L.L.C.
## Key fields
| Field | Value |
|---|---|
| supplier_code | S00066 |
| currency | AED |
| ageing_date | 2025-07-11 |
### Line items
| document_no | bp_reference_no | due_date | amount | running_balance |
|---|---|---|---|---|
| 131365 | SI/08781/CN/00007 | 2025-07-07 | -43160.25 | -43160.25 |
...
## All document fields (as printed)
| Posting Date | From To 11.07.25 |
| Sales Employee | No Sales Employee |
...Typisierte, validierte Felder führen. Rohe gedruckte Felder folgen, sodass eine Frage, die das Profil nicht modelliert, trotzdem beantwortet werden kann. Die geparste Tabelle wird einmal gerendert – sie wird nicht als loser Text dupliziert.
Faustregel für Knoten 4: Aggregationen gehen an SQL, „Was sagt dieses Dokument?" geht an Markdown. Ein einseitiger Kontoauszug passt vollständig in einen Prompt, und ihn vollständig zu füttern ist besser, als Chunks davon abzurufen.
Ausgabe-Vertrag
Konsumenten sollten auf schema_version prüfen, statt Duck-Typing zu betreiben.
{
"schema_version": "2.0",
"status": "ok", // ok | needs_review | parsed_without_profile
// | profile_mismatch | no_text_layer | error
"profile": "sap_b1_supplier_statement",
"profile_confidence": 1.0,
"document": { "file_name": "...", "checksum": "sha256:...",
"pages": 1, "pages_parsed": [0] },
"metadata": { "supplier_name": "ONE WORLD TRADING L.L.C.",
"supplier_code": "S00066", "currency": "AED",
"ageing_date": "2025-07-11" },
"line_items": [
{ "line_no": 1, "document_type": "PU", "document_no": "131365",
"bp_reference_no": "SI/08781/CN/00007",
"posting_date": "2025-05-31", "due_date": "2025-07-07",
"amount": -43160.25, "running_balance": -43160.25,
"_source": { // only when include_coordinates=true
"page": 0,
"cells": { "BP Ref. No.": { "page": 0, "wrapped": true,
"bbox": [123.7, 278.67, 163.79, 295.02] } }
} }
],
"summary": { "buckets": { "Balance Due": -69966.75 } },
"validation": { "ok": true, "checks": [ ... ] },
"diagnostics": { "rows": 5, "rows_with_wrapped_cells": 1,
"column_fill_rate": { ... }, "page_geometry": [ ... ],
"warnings": [] }
}checksum ist enthalten, damit ein nachgelagerter Insert-Knoten deduplizieren kann, ohne dass dieser Server wissen muss, dass eine Datenbank existiert. Das ist die Trennung in Aktion: Wir liefern die Tatsache, jemand anderes entscheidet, was damit geschieht.
include_coordinates
Standardmäßig aus (verdoppelt die Payload ungefähr). Aktivieren, wenn ein späterer Knoten schwärzen, hervorheben oder visuell verifizieren muss. bbox ist [x0, top, x1, bottom] in PDF-Punkten und umfasst alle Zeilen, die eine umgebrochene Zelle belegt hat – eine Schwärzungsbox über SI/08781/CN/00007 deckt also korrekt beide visuellen Zeilen ab.
Daten sind ISO 8601. Beträge sind Floats, negativ für Verbindlichkeiten wie gedruckt.
Validierung und der Beweis, dass es funktioniert
status: "ok" bedeutet, dass alle Prüfungen bestanden sind. Es gibt zwei unabhängige Familien:
Arithmetik – Reproduzieren die gelesenen Zahlen die gedruckten Zahlen?
running_balance_chain– jeder Saldo schreitet um den Betrag seiner eigenen Zeile fort. Stärker als eine Summe: Es benennt die fehlerhafte Zeile und erkennt umsortierte oder duplizierte Zeilen, die eine Summe gar nicht sehen kann.sum_equals_last– die Beträge summieren sich zum Schlusssaldo.summary_equals_last– die Altersstruktur-Summe stimmt überein.
Strukturell – Hat die Rekonstruktion die Seite verbraucht?
word_coverage– jedes Wort im Tabellenbereich landete in genau einer Zelle. Die Kerninvariante.no_unassigned_words,no_orphan_lines– nichts übersprungen.no_suspicious_rows– kennzeichnet spärliche Zeilen (ein Fortsetzungsfragment, das fälschlich als neue Zeile erkannt wurde) und Zeilen, die über einen Seitenumbruch genäht wurden.field_matches– Formprüfung auf Referenznummern.
Strukturprüfungen existieren, weil Arithmetik Textkorruption nicht sehen kann: Eine verstümmelte Referenznummer stimmt trotzdem perfekt in der Summe. tests/test_detection.py korrumpiert Daten auf sieben Arten und stellt sicher, dass jede Prüfung auslöst:
PASS clean data validates
PASS misread amount on line 2 -> running_balance_chain, sum_equals_last
PASS rows out of order -> running_balance_chain
PASS duplicated row -> running_balance_chain
PASS mangled reference number -> field_matches:bp_reference_no <-- ONLY this
PASS unclaimed words on page -> word_coverage, no_unassigned_words
PASS summary disagrees -> summary_equals_last
PASS missing required field -> required_fieldsZeile 5 ist der Kern der ganzen Übung. Eine Prüfung, die nie fehlschlägt, ist Dekoration; diese hier haben nachweislich ausgelöst.
Die Zusage an Stakeholder ist nicht „der Parser beherrscht jedes Layout" – das ist nicht falsifizierbar, und jemand wird ein Gegenbeispiel finden. Sie lautet: Jedes Dokument wird entweder geparst und verifiziert sich selbst, oder es wird markiert. Nichts erreicht den nächsten Knoten still und falsch.
Testen
TESTING.md enthält die vollständige Leiter. Kurzfassung:
bash scripts/check_repo.sh # is the clone complete?
bash scripts/run_tests.sh # all 6 suites, no server
npx @modelcontextprotocol/inspector python -m src.server # see it as a client
python scripts/smoke_test.py <url> <token> doc.pdf # verify a deploymentLevel 3 in TESTING.md – einen echten Agenten über Claude Desktop davorzusetzen – ist der, den man nicht überspringen sollte. Die Tool-Docstrings sind die einzigen Anweisungen, die der Agent von MagOneAI jemals bekommt, und der einzige Weg, sie zu testen, ist, ein LLM versuchen zu lassen, sie zu verwenden.
Schnellstart
pip install -r requirements.txt
export DOC_EXTRACT_TOKEN=$(openssl rand -hex 32)
MCP_TRANSPORT=http python -m src.server # http://0.0.0.0:8000/mcp
python tests/test_samples.py # parser regression
python tests/test_detection.py # validation fires
python tests/e2e_http.py # real MCP client over HTTPdocker build -t doc-extract .
docker run -p 8000:8000 -e DOC_EXTRACT_TOKEN=$TOKEN doc-extract
curl localhost:8000/healthSo baut man einen MCP-Server
Behandelt in BUILDING_AN_MCP_SERVER.md – wie dieser Server aufgebaut ist und warum: Transports (warum streamable HTTP, nicht stdio), Tool-Design, Docstrings-als-Prompts, Auth und die SDK-2.x-Stolperfallen.
Was frei variiert vs. was eine Profiländerung braucht
Gemessen, nicht behauptet – tests/test_robustness.py mutiert das Format jeweils eine Achse nach der anderen.
Frei. Keine Änderung nötig:
Variation | Ergebnis |
Anderer Lieferant, andere Beträge, andere Daten |
|
Beliebige Zeilenzahl, über beliebig viele Seiten |
|
Umbruchtiefe 0, 1, 2, 4+ Zeilen – gemischt in einem Dokument |
|
Spalten durch Layoutdrift verschoben |
|
Schriftgröße 5pt → 16pt |
|
Interpunktion im Kopf driftet ( |
|
Anderes Dokumenttyp-Präfix ( |
|
Faux-Fett / Schlagschatten-Rendering |
|
Nichts ist an eine Koordinate gebunden: Spaltenbänder werden pro Seite aus dem eigenen Kopf dieser Seite neu aufgebaut, die Zeilen-Clustering-Toleranz stammt aus der medianen Glyphengröße des Dokuments, und die Zusammenführung von Kopfzellen kommt aus der eigenen Lückenverteilung der Zeile, begrenzt durch die Schriftgröße.
Braucht eine Profiländerung – und sagt das auch:
Variation | Ergebnis | Was man bekommt |
Spalte umbenannt ( |
| Der fehlende Name + der Kopf wie gedruckt |
Spalte entfernt |
| Dasselbe |
Spalte hinzugefügt |
|
|
Anker passt nicht mehr ( |
| Null Zeilen, markiert statt leer durchgereicht |
Vollständig anderes Dokument |
| Vollständiger Inhalt, keine typisierten Zeilen |
Der Fall der hinzugefügten Spalte ist der wichtigste: Der Inhalt einer neuen Spalte wird von einer Nachbarzelle absorbiert, und die Arithmetik kann trotzdem stimmen. Deshalb schlägt all_header_columns_mapped das Dokument explizit fehl, statt es still passieren zu lassen.
In jedem dieser Fälle ist content.markdown weiterhin vollständig, sodass das Dokument speicher- und chattbar bleibt, während jemand das Profil repariert.
Eine profile_mismatch-Antwort ist direkt umsetzbar:
{
"status": "profile_mismatch",
"header_missing": "Post. Date",
"header_actual": ["Document","BP Ref. No.","Posting Date","Due Date",
"Details","Amount","Balance"],
"next_step": "Update its `columns` to the printed header, then
POST /admin/reload-profiles."
}Die Behebung ist eine einzeilige YAML-Änderung und ein Neuladen – kein Redeploy.
Mehrseiten-Verhalten
Ein Kontoauszugslauf ist nicht „dieselbe Seite N-mal". Jeder dieser Fälle ist in tests/test_multipage.py getestet:
Szenario | Ergebnis |
Kopf auf jeder Seite wiederholt |
|
Kopf nur auf Seite 1 gedruckt |
|
Eine Zeile durch den Seitenumbruch geschnitten |
|
Seitengröße / Ausrichtung ändert sich mitten im Dokument |
|
Fremde Seite (AGB, Zahlungsverkehr) eingeheftet |
|
Zwei davon erforderten echte Korrekturen.
Kopf nur auf Seite 1 verlor still jede Zeile nach der ersten Seite. Jetzt werden die Bänder der vorherigen Seite weitergetragen – aber nur übernommen, wenn die Seite tatsächlich Anker-matchende Zeilen enthält, damit eine AGB-Seite nicht in eine Tabelle gezwungen wird, mit der sie nichts zu tun hat. diagnostics.pages_without_repeated_header listet auf, auf welche Seiten das zutraf.
Zeile durch den Seitenumbruch geschnitten – die Referenz SI/08781/CN/ am Ende von Seite 1 mit 00007 am Anfang von Seite 2 setzt sich wieder zu SI/08781/CN/00007 zusammen. Die Naht ist daran gekoppelt, dass die weitergetragene Zeile tatsächlich nahe dem unteren Rand der vorherigen Seite saß. Ohne diese Absicherung würde jede verirrte Zeile über der ersten Zeile einer Seite an die vorherige Zeile geklebt; mit ihr taucht ein verirrtes Fragment stattdessen als Waise auf und lässt das Dokument fehlschlagen:
status: needs_review
refs : ['SI/2000', 'SI/2001', 'SI/2100'] <-- NOT corrupted
FAILED: no_orphan_lines {'text': 'STRAY-FRAGMENT', 'reason': 'before_first_row'}Eine korrekte Seitenumbruch-Naht erzwingt nicht mehr allein eine menschliche Prüfung – sie wird als Warnung gemeldet. Sonst müsste jeder lange Kontoauszug abgezeichnet werden.
Hinzufügen eines Lieferantenformats: Konfiguration, kein Code
Profile sind YAML in profiles/. Ein Format hinzuzufügen berührt layout.py nie.
extract_documentgibtparsed_without_profilezurückprobe_layout→ jede Zeile mit x-Koordinaten pro WortKopieren Sie die Kopfzeilenbeschriftungen wörtlich in
columnsWählen Sie ein
anchor_column+anchor_pattern, das mit der ersten Zelle jeder Zeile übereinstimmt und sonst nichtsLegen Sie die Datei in
profiles/ab,POST /admin/reload-profiles
id: acme_invoice
detect:
require: ["Tax Invoice"]
text_contains: ["Tax Invoice", "Invoice No."]
table:
columns: ["Line", "Item Code", "Description", "Qty", "Amount"]
anchor_column: "Line"
anchor_pattern: '^\d+$'
stop_pattern: '^Subtotal\b'
join_with: "" # "" for codes/refs, " " for prose
fields:
- {name: item_code, source: "Item Code", type: text}
- {name: amount, source: "Amount", type: decimal}
validation:
- {type: required_fields, fields: [item_code, amount]}Typen: text, decimal, date (+format), int, token (+index).
Ein defektes YAML wird isoliert — es landet in load_errors, andere Profile
funktionieren weiter.
MagOneAI-Verdrahtung
[1] Trigger: user drops a document / Outlook attachment
↓
[2] Agent node: extract_document(source=<url>, file_name=...)
↓
switch on status:
ok -> [3] insert -> [4] chat agent
needs_review -> human approval -> insert / reject
parsed_without_profile -> [3] insert anyway (content is complete)
+ alert: new vendor format seen
no_text_layer -> OCR queue
error -> retry, then alert
↓
[3] DB node: run neon_schema.sql once, then upsert on document.checksum
↓
[4] Agent node with NeonDB access:
"what does this say?" -> SELECT markdown FROM documents WHERE ...
"how much is past due?" -> SELECT SUM(amount) FROM v_document_lines ...neon_schema.sql in diesem Repository enthält das DDL, die JSON-Pfad-zu-Spalten-Zuordnung
für Knoten 3 und die Abfragen, die Knoten 4 ausführen soll. Beachten Sie, dass
parsed_without_profile weiterhin einfügt: content.markdown ist vollständig, sodass
das Dokument sofort chatbar ist; typisierte Positionen kommen später, wenn ein
Profil hinzugefügt wird, und erneutes Einlesen ist prüfsummen-idempotent.
DOC_EXTRACT_TOKENserverseitig, gesendet alsAuthorization: Bearer <token>.max_iterations≈ 15. Ein Aufruf im Happy Path; Review-/Onboarding- Zweige verketten mehr.Bevorzugen Sie
source_type="url"; base64 bläht die Nutzlasten um ~33 % auf.Der Einfügeknoten besitzt das Schema. Dieser Server weiß nicht, dass es existiert.
Engineering-Notizen
Die Toleranz für Zeilen-Clustering wird pro Dokument abgeleitet aus der mittleren Glyphengröße, nicht hartcodiert, sodass derselbe Bericht in einer anderen Skalierung weiterhin geparst wird. Diese Änderung brachte einen echten Fehler ans Licht:
BP :vs.BP:wird unterschiedlich tokenisiert, daher laufen Metadaten-Regexe jetzt gegen satzzeichennormalisierten Text.Seitenumbruch-Zusammenfügung — eine Zelle, die über eine Seitengrenze umbricht, wird mit der weitergeführten Zeile verbunden und als
stitched_across_page_breakmarkiert.Erkennung spärlicher Zeilen — eine Zeile, die ≤1/3 ihrer Spalten ausfüllt, wird als möglicher falscher Anker markiert, der einzige Fehler, den die Waisen-Erkennung nicht abfangen kann.
Bekannte Einschränkungen
Wenn ein umgebrochenes Fragment in der Ankerspalte landete und dem Ankermuster entspräche, würde es als neue Zeile gelesen. Die Erkennung spärlicher Zeilen markiert die wahrscheinlichen Fälle; ein enges Anker-Regex ist die eigentliche Verteidigung.
Die Zuordnung von Alterungs-Buckets ist an zwei Dokumenten verifiziert, die beide in nahe Buckets landen. Führen Sie eine Abfrage mit echtem 90+-Alter aus, bevor Sie Bucket-Beschriftungen in der Produktion vertrauen.
Verschlüsselte oder passwortgeschützte PDFs werden nicht verarbeitet; sie erscheinen als
error.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables reading and extracting content from PDF documents including text (as Markdown), images, tables, and metadata from both local files and URLs, with OCR support for scanned documents.2
- AlicenseNot gradedqualityDmaintenanceEnables comprehensive PDF processing including text extraction, image extraction, and OCR capabilities for reading text within images across multiple languages.12MIT
- AlicenseNot gradedqualityDmaintenanceEnables 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.10MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI-driven PDF document processing including PDF to Markdown conversion, intelligent text and table extraction, image extraction, format conversion between PDF/Word/Markdown, batch processing, and fuzzy search - optimized for LLM context and RAG workflows.2MIT
Related MCP Connectors
Turn any PDF into structured JSON via AI + OCR: invoices, bank statements, contracts.
Read PDFs and images as markdown or text, with exact costs and hard spend caps. $0.75/1k pages.
Turn a description into a shareable, editable PDF — invoices, certificates, reports, resumes.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/AlanAAG/invoice-extraction-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server