Skip to main content
Glama
AlanAAG

doc-extract

by AlanAAG

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 all

Los 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

SI/08781/CN/

124

164

288.4

00007

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

content.markdown

El documento renderizado para un LLM

Chatear. Almacena esto.

content.text

Texto plano

Búsqueda, embeddings

content.key_values

Cada Etiqueta: valor en la página

Filtros, búsquedas

content.blocks

Bloques tipados, ordenados y posicionados

Uso programático, redacción

content.chunks

Markdown dividido en encabezados

Recuperación en documentos largos

tables[]

Cada tabla como columnas + filas

Renderizado, exportación

line_items[], metadata{}

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_fields

La 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 deployment

El 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 HTTP
docker build -t doc-extract .
docker run -p 8000:8000 -e DOC_EXTRACT_TOKEN=$TOKEN doc-extract
curl localhost:8000/health

Có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

ok

Cualquier número de filas, en cualquier número de páginas

ok

Profundidad de envoltura 0, 1, 2, 4+ líneas — mezclada en un documento

ok

Columnas reposicionadas por deriva de diseño

ok

Tamaño de fuente 5pt → 16pt

ok

Deriva de puntuación en encabezados (BP Ref. No.BP Ref No)

ok

Diferente prefijo de tipo de documento (PURC)

ok

Renderizado falso-negrita / sombra

ok

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 (Post. DatePosting Date)

profile_mismatch

El nombre faltante + el encabezado como se imprimió

Columna eliminada

profile_mismatch

Igual

Columna añadida

needs_review

unmapped_header_columns: ["Currency"]

Ancla ya no coincide (INV-2026-001)

needs_review

Cero filas, marcado en lugar de pasar vacío

Documento completamente diferente

parsed_without_profile

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

ok — todas las filas

Encabezado impreso solo en la página 1

ok — bandas arrastradas

Una fila cortada por el salto de página

ok — la cola envuelta se cose a su fila

Tamaño / orientación de página cambia a mitad del documento

ok — bandas reconstruidas por página

Página no relacionada (términos, remesa) adjunta

ok — omitida, sin filas inventadas

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.

  1. extract_document devuelve parsed_without_profile

  2. probe_layout → cada línea con coordenadas x por palabra

  3. Copia las etiquetas de cabecera literalmente en columns

  4. Elige un anchor_column + anchor_pattern que coincida con la primera celda de cada fila y con nada más

  5. Coloca 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_TOKEN en el servidor, enviado como Authorization: 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 : y BP: 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.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    Not graded
    quality
    D
    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.
    10
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    2
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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