doc-extract
doc-extract
Un servidor MCP que hace exactamente una cosa: PDF de entrada → el documento completo como JSON estructurado validado de salida.
Construido para este flujo de trabajo:
[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 allLos pasos 3, 4 y 5 deliberadamente NO son trabajo de este servidor. No tiene controlador de base de datos y cada herramienta es de solo lectura.
Sin base de datos. Sin efectos secundarios. Sin persistencia. Cada herramienta es de solo lectura. Lo que suceda después — insertar, redactar, enrutar, notificar — es un nodo separado en el flujo de trabajo de MagOneAI.
Alcance, aplicado no solo declarado
Este servidor SÍ hace | Este servidor NO hace |
Leer la capa de texto completa de un PDF | Escribir en cualquier base de datos |
Reconstruir la geometría de las tablas | Enviar correo o notificar |
Reparar celdas envueltas | Redactar o modificar el PDF |
Validar la lectura | Decidir qué sucede después |
Devolver JSON + coordenadas | Almacenar algo entre llamadas |
Aplicación, para que la expansión del alcance sea estructuralmente difícil:
Las tres herramientas están anotadas con
readOnlyHint: true,destructiveHint: false,idempotentHint: true. Un orquestador puede ver que es seguro reintentar.extract()es una función pura de los bytes del PDF. Misma entrada → misma salida.Recargar perfiles es una ruta administrativa HTTP, no una herramienta MCP. Los cambios de configuración son una acción del operador; un agente de flujo de trabajo no debe poder elegir hacer uno.
No se escribe nada en disco excepto un archivo temporal para el PDF entrante.
Related MCP server: MCP PDF Reader Server
Por qué no OCR
Ambas muestras son exportaciones de Crystal Reports desde SAP Business One — fuentes incrustadas, sin imágenes rasterizadas. Cada carácter ya lleva coordenadas de página exactas. El OCR rasterizaría eso y volvería a derivar esas coordenadas con error.
El BP Ref. No. envuelto es un problema de reconstrucción de diseño:
línea | token | x0 | x1 |
278.7 |
| 124 | 164 |
288.4 |
| 124 | 142 |
00007 se encuentra exactamente en el borde izquierdo de la columna BP Ref → misma celda →
SI/08781/CN/00007.
Importa más en el archivo Nutripharm, donde los fragmentos son dígitos desnudos.
Leyendo texto plano, 111 plausiblemente se pega al importe dando
-8,762.513111. Las coordenadas dicen x0=124, no x≈450, así que es la
referencia (N-CINV-01999111) y el importe permanece -8,762.513.
El documento completo, siempre
El análisis de perfil responde "¿cuáles son las líneas de detalle?" e ignora todo lo demás. Eso no es suficiente para los pasos 2, 4 y 5, así que la extracción de documento completo se ejecuta en cada documento, coincida o no, produciendo cuatro vistas del mismo contenido:
Campo | Qué es | Úsalo para |
| El documento renderizado para un LLM | Chatear. Almacena esto. |
| Texto plano | Búsqueda, embeddings |
| Cada | Filtros, búsquedas |
| Bloques tipados, ordenados y posicionados | Uso programático, redacción |
| Markdown dividido en encabezados | Recuperación en documentos largos |
| Cada tabla como columnas + filas | Renderizado, exportación |
| Tipados + validados | Agregación SQL |
Un documento sin perfil ya no es un callejón sin salida. Devuelve
parsed_without_profile con content completamente poblado — para que el equipo pueda
almacenarlo, chatear con él y actuar sobre él antes de que alguien escriba un perfil. Un
perfil solo añade líneas de detalle tipadas y verificaciones cruzadas encima.
Por qué markdown es el artefacto para chatear
Un agente al que se le pregunta "¿cuál es el saldo de cierre para One World?" responde de manera mucho más fiable leyendo esto que reensamblando filas desde JSON o escaneando un volcado de texto sin procesar:
# 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 |
...Los campos tipados y validados van primero. Los campos impresos sin procesar siguen, para que una pregunta que el perfil no modela aún pueda responderse. La tabla analizada se renderiza una vez — no se duplica como texto suelto.
Regla general para el nodo 4: las agregaciones van a SQL, "¿qué dice este documento?" va a markdown. Un estado de cuenta de una página cabe completo en un prompt, y alimentarlo completo supera a recuperar fragmentos del mismo.
Contrato de salida
Los consumidores deben basarse en schema_version en lugar de duck-typing.
{
"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 se incluye para que un nodo de inserción posterior pueda deduplicar sin que este
servidor necesite saber que existe una base de datos. Esa es la separación funcionando:
proporcionamos el hecho, alguien más decide qué hacer con él.
include_coordinates
Desactivado por defecto (aproximadamente duplica la carga útil). Actívalo cuando un nodo posterior necesite
redactar, resaltar o verificar visualmente. bbox es [x0, top, x1, bottom] en
puntos PDF, y abarca todas las líneas que ocupó una celda envuelta — así que una caja
de redacción sobre SI/08781/CN/00007 cubre correctamente ambas líneas visuales.
Las fechas son ISO 8601. Los importes son flotantes, negativos para cuentas por pagar como se imprimen.
Validación, y prueba de que funciona
status: "ok" significa que todas las comprobaciones pasaron. Hay dos familias independientes:
Aritmética — ¿los números que leemos reproducen los números impresos?
running_balance_chain— cada saldo avanza por el importe de su propia fila. Más fuerte que un total: nombra la línea que falla y detecta filas reordenadas o duplicadas que una suma no puede ver en absoluto.sum_equals_last— los importes suman el saldo de cierre.summary_equals_last— el total de antigüedad coincide.
Estructural — ¿la reconstrucción consumió la página?
word_coverage— cada palabra en la región de la tabla aterrizó en exactamente una celda. La invariante central.no_unassigned_words,no_orphan_lines— nada omitido.no_suspicious_rows— marca filas dispersas (un fragmento de continuación confundido con una fila nueva) y filas cosidas a través de un salto de página.field_matches— verificación de forma en números de referencia.
Las comprobaciones estructurales existen porque la aritmética no puede ver la corrupción de texto: un
número de referencia mutilado aún cuadra perfectamente. tests/test_detection.py
corrompe datos de siete maneras y afirma que cada comprobación se activa:
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_fieldsLa línea 5 es el punto de todo el ejercicio. Una comprobación que nunca falla es decoración; estas demostraron activarse.
La garantía para declarar a las partes interesadas no es "el analizador maneja cada diseño" — infalsificable, y alguien encontrará un contraejemplo. Es: cada documento o se analiza y se autoverifica, o se marca. Nada llega al siguiente nodo silenciosamente incorrecto.
Pruebas
TESTING.md tiene la escalera completa. Versión corta:
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 deploymentEl nivel 3 en TESTING.md — poner un agente real frente a él vía Claude
Desktop — es el que vale la pena no saltarse. Las cadenas de documentación de las herramientas son las únicas instrucciones
que el agente de MagOneAI recibirá jamás, y la única forma de probarlas es
dejar que un LLM intente usarlas.
Inicio rápido
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/healthCómo construir un servidor MCP
Cubierto en BUILDING_AN_MCP_SERVER.md — cómo está
montado este servidor y por qué: transportes (por qué HTTP transmisible, no stdio),
diseño de herramientas, cadenas de documentación como prompts, autenticación y los problemas del SDK 2.x.
Qué varía libremente vs. qué necesita una edición de perfil
Medido, no afirmado — tests/test_robustness.py muta el formato un
eje a la vez.
Libre. Sin cambio necesario:
Variación | Resultado |
Diferente proveedor, importes, fechas |
|
Cualquier número de filas, en cualquier número de páginas |
|
Profundidad de envoltura 0, 1, 2, 4+ líneas — mezclada en un documento |
|
Columnas reposicionadas por deriva de diseño |
|
Tamaño de fuente 5pt → 16pt |
|
Deriva de puntuación en encabezados ( |
|
Diferente prefijo de tipo de documento ( |
|
Renderizado falso-negrita / sombra |
|
Nada está fijado a una coordenada: las bandas de columna se reconstruyen por página desde el encabezado de esa misma página, la tolerancia de agrupación de líneas proviene de la mediana del tamaño de glifo del documento, y la fusión de celdas de encabezado proviene de la distribución de espacios de la propia línea limitada por el tamaño de tipo.
Necesita una edición de perfil — y lo dice:
Variación | Resultado | Lo que obtienes |
Columna renombrada ( |
| El nombre faltante + el encabezado como se imprimió |
Columna eliminada |
| Igual |
Columna añadida |
|
|
Ancla ya no coincide ( |
| Cero filas, marcado en lugar de pasar vacío |
Documento completamente diferente |
| Contenido completo, sin filas tipadas |
El caso de columna añadida es el que más importa: el contenido de una columna nueva
se absorbe en una celda vecina, y la aritmética aún puede cuadrar. Así que
all_header_columns_mapped falla el documento explícitamente en lugar de dejarlo pasar silenciosamente.
En cada uno de estos casos content.markdown sigue estando completo, así que el
documento permanece almacenable y chateable mientras alguien arregla el perfil.
Una respuesta profile_mismatch es directamente accionable:
{
"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."
}Arreglarlo es un cambio de una línea en YAML y una recarga — sin redesplegar.
Comportamiento multipágina
Una ejecución de estado de cuenta no es "la misma página N veces". Cada uno de estos se prueba en
tests/test_multipage.py:
Escenario | Resultado |
Encabezado repetido en cada página |
|
Encabezado impreso solo en la página 1 |
|
Una fila cortada por el salto de página |
|
Tamaño / orientación de página cambia a mitad del documento |
|
Página no relacionada (términos, remesa) adjunta |
|
Dos de estos necesitaron correcciones reales.
Encabezado solo en la página 1 perdió silenciosamente cada fila después de la primera página. Ahora
las bandas de la página anterior se arrastran — pero solo se confirman si la página
realmente contiene filas que coinciden con el ancla, así que una página de términos y condiciones no se
fuerza a encajar en una tabla con la que no tiene nada que ver. diagnostics. pages_without_repeated_header lista a qué páginas se aplicó esto.
Fila cortada por el salto de página — la referencia SI/08781/CN/ al final de la
página 1 con 00007 al inicio de la página 2 se reensambla a
SI/08781/CN/00007. La costura se condiciona a que la fila arrastrada realmente haya
estado cerca del final de la página anterior. Sin esa protección, cualquier línea suelta
sobre la primera fila de una página se pegaría a la fila anterior; con ella,
un fragmento suelto en cambio aparece como huérfano y falla el documento:
status: needs_review
refs : ['SI/2000', 'SI/2001', 'SI/2100'] <-- NOT corrupted
FAILED: no_orphan_lines {'text': 'STRAY-FRAGMENT', 'reason': 'before_first_row'}Una costura correcta de salto de página ya no fuerza revisión humana por sí sola — se informa como advertencia. De lo contrario, cada estado de cuenta largo necesitaría aprobación.
Añadir un formato de proveedor: configuración, no código
Los perfiles son YAML en profiles/. Añadir un formato nunca toca layout.py.
extract_documentdevuelveparsed_without_profileprobe_layout→ cada línea con coordenadas x por palabraCopia las etiquetas de cabecera literalmente en
columnsElige un
anchor_column+anchor_patternque coincida con la primera celda de cada fila y con nada másColoca el archivo en
profiles/,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]}Tipos: text, decimal, date (+format), int, token (+index).
Un YAML roto queda aislado: termina en load_errors y los demás perfiles siguen funcionando.
Cableado de MagOneAI
[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 en este repositorio contiene el DDL, el mapeo de JSON-path → columna para el nodo 3 y las consultas que el nodo 4 debe ejecutar. Ten en cuenta que parsed_without_profile sigue insertando: content.markdown está completo, por lo que el documento se puede consultar de inmediato; los elementos de línea tipados llegan más tarde cuando se añade un perfil, y la re-ingesta es idempotente por checksum.
DOC_EXTRACT_TOKENen el servidor, enviado comoAuthorization: Bearer <token>.max_iterations≈ 15. Una llamada en el camino feliz; las ramas de revisión/incorporación encadenan más.Prefiere
source_type="url"; base64 infla los payloads ~33%.El nodo de inserción es el propietario del esquema. Este servidor no sabe que existe.
Notas de ingeniería
La tolerancia de agrupación de líneas se deriva por documento a partir de la mediana del tamaño de los glifos, no está codificada, por lo que el mismo informe a otra escala sigue analizándose. Este cambio sacó a la luz un error real:
BP :yBP:se tokenizan de forma distinta, por lo que las expresiones regulares de metadatos ahora se ejecutan sobre texto normalizado en puntuación.Unión de saltos de página — una celda que se extiende a través de un límite de página se une a la fila arrastrada y se marca como
stitched_across_page_break.Detección de filas dispersas — una fila que ocupa ≤1/3 de sus columnas se marca como posible ancla falsa, el único fallo que la detección de huérfanos no puede capturar.
Limitaciones conocidas
Si un fragmento envuelto aterrizara en la columna de anclaje y coincidiera con el patrón de anclaje, se leería como una fila nueva. La detección de filas dispersas marca los casos probables; una expresión regular de anclaje estricta es la defensa real.
El mapeo de cubos de envejecimiento está verificado en dos documentos, ambos en cubos cercanos. Ejecuta una declaración con envejecimiento real de 90+ antes de confiar en las etiquetas de cubos en producción.
Los PDF cifrados o protegidos con contraseña no se gestionan; aparecen como
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