Skip to main content
Glama
AlanAAG

doc-extract

by AlanAAG

doc-extract

정확히 한 가지 작업만 수행하는 MCP 서버: PDF 입력 → 검증된 구조화 JSON으로 전체 문서 출력.

다음 워크플로우를 위해 구축됨:

[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

3, 4, 5단계는 의도적으로 이 서버의 작업이 아니다. 데이터베이스 드라이버가 없으며 모든 도구는 읽기 전용이다.

데이터베이스 없음. 부작용 없음. 영속성 없음. 모든 도구는 읽기 전용이다. 그 다음에 일어나는 일(삽입, 수정, 라우팅, 알림)은 MagOneAI 워크플로우의 별도 노드이다.


범위, 선언만이 아닌 강제

이 서버가 수행하는 작업

이 서버가 수행하지 않는 작업

PDF의 전체 텍스트 레이어 읽기

데이터베이스에 쓰기

테이블 기하 구조 재구성

이메일 전송 또는 알림

줄바꿈된 셀 복구

PDF 수정 또는 변경

읽기 결과 검증

다음 단계 결정

JSON + 좌표 반환

호출 간 상태 저장

범위 확장을 구조적으로 어렵게 만드는 강제 장치:

  • 세 가지 도구 모두 readOnlyHint: true, destructiveHint: false, idempotentHint: true로 주석 처리되어 있다. 오케스트레이터는 재시도해도 안전함을 알 수 있다.

  • extract()는 PDF 바이트의 순수 함수이다. 동일한 입력 → 동일한 출력.

  • 프로필 재로딩은 MCP 도구가 아닌 HTTP 관리 라우트이다. 구성 변경은 운영자 작업이며, 워크플로우 에이전트가 이를 선택할 수 없어야 한다.

  • 수신 PDF용 임시 파일 외에는 디스크에 아무것도 기록되지 않는다.


Related MCP server: MCP PDF Reader Server

OCR을 사용하지 않는 이유

두 샘플 모두 SAP Business One의 Crystal Reports 내보내기이다 — 임베디드 폰트, 래스터 이미지 없음. 모든 문자가 이미 정확한 페이지 좌표를 가지고 있다. OCR은 이를 래스터화하고 오류가 있는 좌표를 다시 도출할 것이다.

줄바꿈된 BP Ref. No.레이아웃 재구성 문제이다:

line

token

x0

x1

278.7

SI/08781/CN/

124

164

288.4

00007

124

142

00007은 BP Ref 열의 정확히 왼쪽 가장자리에 위치한다 → 같은 셀 → SI/08781/CN/00007.

이는 조각이 숫자만으로 이루어진 Nutripharm 파일에서 가장 중요하다. 평문 텍스트로 읽으면 111이 금액에 자연스럽게 붙어 -8,762.513111이 된다. 좌표는 x0=124를 나타내며 x≈450이 아니므로, 이는 참조 번호(N-CINV-01999111)이고 금액은 -8,762.513으로 유지된다.


항상 전체 문서

프로필 파싱은 "라인 항목이 무엇인가?"에 답하고 나머지는 무시한다. 이는 2, 4, 5단계에 충분하지 않으므로, 전체 문서 추출은 일치 여부와 관계없이 모든 문서에 대해 실행되며 동일한 콘텐츠의 네 가지 뷰를 생성한다:

필드

정의

용도

content.markdown

LLM용으로 렌더링된 문서

채팅. 이것을 저장하라.

content.text

일반 텍스트

검색, 임베딩

content.key_values

페이지의 모든 Label: value

필터, 조회

content.blocks

타입화되고, 정렬되고, 위치된 블록

프로그래밍적 사용, 수정

content.chunks

제목에서 분할된 마크다운

긴 문서의 검색

tables[]

각 테이블을 열 + 행으로

렌더링, 내보내기

line_items[], metadata{}

타입화 + 검증

SQL 집계

프로필이 없는 문서는 더 이상 막다른 길이 아니다. parsed_without_profile로 반환되며 content가 완전히 채워진다 — 따라서 누구든 프로필을 작성하기 전에 팀이 이를 저장하고, 채팅하고, 조치를 취할 수 있다. 프로필은 그 위에 타입화된 라인 항목과 교차 검증을 추가할 뿐이다.

채팅에서 마크다운이 산출물인 이유

"One World의 기말 잔액은 얼마인가?"라는 질문을 받은 에이전트는 JSON에서 행을 재조립하거나 원시 텍스트 덤프를 스캔하는 것보다 이것을 읽는 것이 훨씬 더 신뢰성 있게 답한다:

# 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 |
...

타입화되고 검증된 필드가 먼저 나온다. 원시 인쇄 필드가 뒤따르므로, 프로필이 모델링하지 않는 질문에도 답할 수 있다. 파싱된 테이블은 한 번 렌더링된다 — 느슨한 텍스트로 중복되지 않는다.

노드 4의 경험칙: 집계는 SQL로, "이 문서가 무엇을 말하는가?"는 마크다운으로. 한 페이지 분량의 명세서는 프롬프트 전체에 들어가며, 전체를 제공하는 것이 청크를 검색하는 것보다 낫다.

출력 계약

소비자는 덕 타이핑보다 schema_version을 기준으로 단언해야 한다.

{
  "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은 다운스트림 삽입 노드가 이 서버가 데이터베이스의 존재를 알 필요 없이 중복을 제거할 수 있도록 포함된다. 이것이 분리의 작동 방식이다: 우리는 사실을 제공하고, 다른 누군가가 그것으로 무엇을 할지 결정한다.

include_coordinates

기본적으로 꺼져 있음(페이로드가 약 2배 증가). 이후 노드가 수정, 강조 또는 시각적 검증이 필요할 때 켜라. bbox는 PDF 포인트 단위의 [x0, top, x1, bottom]이며, 줄바꿈된 셀이 차지한 모든 줄에 걸쳐 있다 — 따라서 SI/08781/CN/00007 위의 수정 상자는 두 시각적 줄을 모두 올바르게 덮는다.

날짜는 ISO 8601이다. 금액은 부동소수점이며, 인쇄된 대로 지급액은 음수이다.


검증, 그리고 작동한다는 증거

status: "ok"는 모든 검사가 통과했음을 의미한다. 두 개의 독립적인 계열이 있다:

산술 — 우리가 읽은 숫자가 인쇄된 숫자를 재현하는가?

  • running_balance_chain — 각 잔액이 해당 행의 금액만큼 진행된다. 합계보다 강력하다: 실패한 줄을 명명하며, 합계로는 전혀 감지할 수 없는 재정렬되거나 중복된 행을 잡아낸다.

  • sum_equals_last — 금액의 합이 기말 잔액과 일치한다.

  • summary_equals_last — 노화 합계가 일치한다.

구조적 — 재구성이 페이지를 소비했는가?

  • word_coverage — 테이블 영역의 모든 단어가 정확히 하나의 셀에 들어갔다. 핵심 불변 조건.

  • no_unassigned_words, no_orphan_lines — 건너뛴 것이 없음.

  • no_suspicious_rows — 희소 행(새 행으로 오인된 연속 조각)과 페이지 나눔을 가로질러 이어진 행을 표시.

  • field_matches — 참조 번호의 형태 검사.

구조적 검사가 존재하는 이유는 산술은 텍스트 손상을 볼 수 없기 때문이다: 손상된 참조 번호도 교차 합계는 완벽하게 맞는다. tests/test_detection.py는 데이터를 일곱 가지 방식으로 손상시키고 각 검사가 발동함을 단언한다:

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

5번째 줄이 이 전체 작업의 핵심이다. 실패하지 않는 검사는 장식일 뿐이다; 이것들은 실제로 발동함이 입증되었다.

이해관계자에게 진술할 보장은 "파서가 모든 레이아웃을 처리한다"가 아니다 — 반증 불가능하며, 누군가 반례를 찾을 것이다. 그것은 다음과 같다: 모든 문서는 파싱되어 자체 검증되거나, 플래그가 지정된다. 아무것도 조용히 잘못된 상태로 다음 노드에 도달하지 않는다.


테스트

TESTING.md에 전체 사다리가 있다. 요약:

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

TESTING.md의 레벨 3 — Claude Desktop을 통해 실제 에이전트를 앞에 두는 것 — 은 건너뛰지 말아야 할 가치가 있는 것이다. 도구 docstring은 MagOneAI의 에이전트가 받게 될 유일한 지침이며, 이를 테스트하는 유일한 방법은 LLM이 그것을 사용해 보게 하는 것이다.

빠른 시작

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

MCP 서버 구축 방법

BUILDING_AN_MCP_SERVER.md에서 다룬다 — 이 서버가 어떻게 구성되고 왜 그런지: 전송(왜 stdio가 아닌 streamable HTTP인지), 도구 설계, 프롬프트로서의 docstring, 인증, SDK 2.x 함정.


자유롭게 변하는 것 vs. 프로필 편집이 필요한 것

단언이 아닌 측정 — tests/test_robustness.py는 형식을 한 번에 한 축씩 변형한다.

자유. 변경 불필요:

변형

결과

다른 공급업체, 금액, 날짜

ok

임의의 행 수, 임의의 페이지 수에 걸쳐

ok

줄바꿈 깊이 0, 1, 2, 4+ 줄 — 한 문서에 혼합

ok

레이아웃 이동으로 인한 열 위치 변경

ok

글꼴 크기 5pt → 16pt

ok

헤더 구두점 변동 (BP Ref. No.BP Ref No)

ok

다른 문서 유형 접두사 (PURC)

ok

유사-굵게 / 그림자 렌더링

ok

어떤 것도 좌표에 고정되지 않는다: 열 밴드는 해당 페이지의 자체 헤더에서 페이지별로 재구축되고, 줄 클러스터링 허용 오차는 문서의 중앙 글리프 크기에서 나오며, 헤더 셀 병합은 해당 줄의 자체 간격 분포에서 유형 크기로 제한되어 나온다.

프로필 편집 필요 — 그리고 그렇게 명시:

변형

결과

제공되는 것

이름 변경 (Post. DatePosting Date)

profile_mismatch

누락된 이름 + 인쇄된 헤더

제거

profile_mismatch

동일

추가

needs_review

unmapped_header_columns: ["Currency"]

앵커가 더 이상 일치하지 않음 (INV-2026-001)

needs_review

행 0개, 빈 상태로 통과가 아닌 플래그 지정

완전히 다른 문서

parsed_without_profile

전체 콘텐츠, 타입화된 행 없음

열 추가 사례가 가장 중요하다: 새 열의 콘텐츠가 인접 셀에 흡수되고 산술 합계는 여전히 맞을 수 있다. 따라서 all_header_columns_mapped는 문서가 조용히 통과하는 대신 명시적으로 실패시킨다.

이 모든 경우에 content.markdown은 여전히 완전하다, 따라서 누군가 프로필을 수정하는 동안 문서는 저장 가능하고 채팅 가능한 상태로 유지된다.

profile_mismatch 응답은 직접 실행 가능하다:

{
  "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."
}

수정은 한 줄의 YAML 변경과 재로딩일 뿐이다 — 재배포 없음.

다중 페이지 동작

명세서 실행은 "같은 페이지의 N회 반복"이 아니다. 다음 각각은 tests/test_multipage.py에서 테스트된다:

시나리오

결과

모든 페이지에 헤더 반복

ok — 모든 행

헤더가 1페이지에만 인쇄됨

ok — 밴드가 이월됨

페이지 나눔으로 잘린 행

ok — 줄바꿈된 꼬리가 해당 행에 이어붙음

문서 중간에 페이지 크기 / 방향 변경

ok — 밴드가 페이지별로 재구축됨

관련 없는 페이지(약관, 송금)가 끼워짐

ok — 건너뜀, 행이 생성되지 않음

이 중 두 가지는 실제 수정이 필요했다.

헤더가 1페이지에만 있음 — 첫 페이지 이후의 모든 행이 조용히 손실되었다. 이제 이전 페이지의 밴드가 이월되지만 — 페이지에 실제로 앵커 일치 행이 포함된 경우에만 커밋되므로, 약관 페이지가 관련 없는 테이블에 강제로 맞춰지지 않는다. diagnostics.pages_without_repeated_header는 이것이 적용된 페이지를 나열한다.

페이지 나눔으로 잘린 행 — 1페이지 하단의 참조 SI/08781/CN/과 2페이지 상단의 00007SI/08781/CN/00007로 재조립된다. 이어붙임은 이월된 행이 실제로 이전 페이지 하단 근처에 있었는지에 따라 제한된다. 이 보호 장치가 없으면 페이지 첫 행 위의 임의의 떨어진 줄이 이전 행에 붙을 것이다; 보호 장치가 있으면 떨어진 조각은 대신 고아로 표면화되어 문서를 실패시킨다:

status: needs_review
refs  : ['SI/2000', 'SI/2001', 'SI/2100']      <-- NOT corrupted
FAILED: no_orphan_lines  {'text': 'STRAY-FRAGMENT', 'reason': 'before_first_row'}

올바른 페이지 나눔 이어붙임은 더 이상 자체적으로 인간 검토를 강요하지 않는다 — 경고로 보고된다. 그렇지 않으면 모든 긴 명세서에 서명이 필요할 것이다.

공급업체 형식 추가: 코드가 아닌 구성

프로필은 profiles/의 YAML이다. 형식 추가는 layout.py를 건드리지 않는다.

  1. extract_documentparsed_without_profile을 반환합니다

  2. probe_layout → 단어별 x좌표가 포함된 모든 줄

  3. 헤더 라벨을 그대로 columns에 복사합니다

  4. 각 행의 첫 번째 셀과만 일치하는 anchor_column + anchor_pattern을 선택합니다

  5. 파일을 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]}

유형: text, decimal, date (+format), int, token (+index). 손상된 YAML은 격리됩니다 — load_errors에 기록되며 다른 프로필은 계속 작동합니다.


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에는 DDL, 노드 3의 JSON-path → 컬럼 매핑, 노드 4가 실행해야 할 쿼리가 들어 있습니다. parsed_without_profile도 여전히 삽입된다는 점에 유의하세요: content.markdown이 완전하므로 문서는 즉시 채팅이 가능하며, 유형화된 라인 항목은 프로필이 추가된 후에 도착하고, 재수집은 체크섬 기준으로 멱등적입니다.

  • DOC_EXTRACT_TOKEN 서버 측, Authorization: Bearer <token>로 전송됩니다.

  • max_iterations ≈ 15. 정상 경로에서는 한 번 호출하며, 검토/온보딩 분기에서는 더 연결됩니다.

  • source_type="url"을 선호하세요; base64는 페이로드를 ~33% 증가시킵니다.

  • 삽입 노드가 스키마를 소유합니다. 이 서버는 그 존재를 알지 못합니다.


엔지니어링 노트

  • 줄 클러스터링 허용 오차는 문서별로 중앙 글리프 크기에서 파생되며 하드코딩되지 않으므로, 다른 배율의 동일한 보고서도 여전히 파싱됩니다. 이 변경으로 실제 버그가 드러났습니다: BP :BP:는 토큰화 방식이 다르므로, 이제 메타데이터 정규식은 구두점이 정규화된 텍스트에 대해 실행됩니다.

  • 페이지 나눔 스티칭 — 페이지 경계를 넘어 줄바꿈되는 셀은 이월된 행에 결합되고 stitched_across_page_break로 표시됩니다.

  • 희소 행 감지 — 컬럼의 ≤1/3만 채우는 행은 가능한 거짓 앵커로 표시되며, 이는 고아 감지가 잡을 수 없는 유일한 실패입니다.

알려진 제한 사항

  • 줄바꿈된 조각이 앵커 컬럼에 들어가고 그리고 앵커 패턴과 일치하면 새 행으로 읽힐 수 있습니다. 희소 행 감지가 가능성 있는 경우를 표시하며, 엄격한 앵커 정규식이 실제 방어 수단입니다.

  • 에이징 버킷 매핑은 두 문서에서 검증되었으며, 둘 다 근접 버킷에 속합니다. 프로덕션에서 버킷 라벨을 신뢰하기 전에 실제 90+ 에이징이 있는 명세서로 실행하세요.

  • 암호화되거나 비밀번호로 보호된 PDF는 처리되지 않으며 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