Skip to main content
Glama

A personal local MCP that reads unstructured documents (pdf docx pptx svg png) in a specified folder and helps summarize key content and analyze file structure. It plugs into Claude Code · Codex · Claude Desktop.

Document

What it contains

README.md (this document)

How to use it

AGENTS.md

What is being built — data contracts · tool contracts · guardrails · permanent refusal list

CLAUDE.md

Coding agent workflow — workflow · review checklist · common mistakes

If the same fact appears in two places, AGENTS.md is the source of truth.


This server does not summarize

This is the most important design decision.

Layer

What it does

MCP server

Extraction · structure analysis · grounding anchor attachment · summary verification

Host model (Claude Code / Codex)

Writes the summary — citing anchors while doing so

Human

Approval

If the server also summarized, it would have to call a model again with its own API key, and the host would receive only the summary result, making it impossible to verify the grounding. That opens a path for incorrect summaries to pass silently. So the server only outputs the source text and anchors.


Related MCP server: file-analyzer

Quick start

Prerequisites: Python 3.11+, uv

uv sync --extra dev
uv run python scripts/make_samples.py
uv run python scripts/smoke_stdio.py

If smoke_stdio.py prints PASS, the server is working — it launches the server over the real MCP protocol, checks all 17 harness contracts, and runs a full cycle from DISCOVER to SAVED.

To visually inspect the tools with MCP Inspector:

uv run mcp dev src/file_mcp/server.py

Specifying the folder to analyze

Edit allowed_roots in config/roots.toml. This file is the server's security boundary.

allowed_roots = [
  "data/samples",
  "C:/Users/<사용자>/Desktop/분석대상",
]

Do not add an entire parent folder like C:/Users/<user> — that is effectively no guard at all. The server will never open a path outside this list under any circumstances.

Host connection

Claude Code

claude mcp add file-analysis -- uv --directory "<이-저장소를-클론한-절대경로>" run python src/file_mcp/server.py

Codex — paste the contents of config/codex-config.example.toml into ~/.codex/config.toml.

Claude Desktop — see config/claude_desktop_config.example.json.


Pipeline

flowchart LR
    S["scan_folder<br/><i>추정 등급 B?</i>"] --> I["inspect_document<br/><i>확정 등급 A/B/C</i>"]
    I --> P["build_analysis_prompt<br/><i>앵커 붙은 원문</i>"]
    P --> D(["초안 작성<br/><i>호스트 모델</i>"])
    D --> G["check_summary_grounding<br/><i>GR-01 … GR-04</i>"]
    G --> V["preview_save_report<br/><i>승인 토큰 발급</i>"]
    V --> H{{"사람의 승인"}}
    H --> W["save_approved_report<br/><i>유일한 쓰기</i>"]

    classDef server fill:#ddf4ff,stroke:#54aeff,color:#1f2328
    classDef notserver fill:#ffffff,stroke:#afb8c1,stroke-dasharray:5 4,color:#656d76
    classDef write fill:#fff8c5,stroke:#d4a72c,color:#1f2328
    class S,I,P,G,V server
    class D,H notserver
    class W write

The dashed line marks what the server does not do. The draft is written by the host model, and approval is done by a human.

Step

Tool

Read/Write

DISCOVER

list_allowed_roots

Read

DISCOVER

scan_folder

Read

INSPECT

inspect_document

Read

READ

read_document

Read

READ

read_document_image

Read

DRAFT

build_analysis_prompt

Read

CHECK

check_summary_grounding

Read

PREVIEW

preview_save_report

Read

APPROVE

(human)

SAVED

save_approved_report

Write

save_approved_report is the only write tool. It never writes without an approval token. scripts/smoke_stdio.py checks the write tool list, so if you add more tools, you must update the smoke test as well.


Grades are determined by content, not file extension

flowchart TD
    X["파일"] --> Y{"확장자"}
    Y -->|"docx · pptx"| A["<b>등급 A</b><br/>구조까지"]
    Y -->|"png"| C1["<b>등급 C</b><br/>이미지 판독"]
    Y -->|"pdf"| PQ{"공백 제거 후 페이지 텍스트<br/>8자 이상?"}
    Y -->|"svg"| SQ{"내용 있는<br/>text 노드?"}
    PQ -->|"있음"| B1["<b>등급 B</b><br/>본문만"]
    PQ -->|"없음"| C2["<b>등급 C</b><br/>스캔 PDF"]
    SQ -->|"있음"| B2["<b>등급 B</b><br/>본문만"]
    SQ -->|"없음"| C3["<b>등급 C</b><br/>그림"]

    classDef ga fill:#dafbe1,stroke:#2da44e,color:#1f2328
    classDef gb fill:#ddf4ff,stroke:#54aeff,color:#1f2328
    classDef gc fill:#fff8c5,stroke:#d4a72c,color:#1f2328
    class A ga
    class B1,B2 gb
    class C1,C2,C3 gc

Grade

Meaning

How to read

A

Extracts structure too (heading levels · tables · slide units)

read_document

B

Extracts body text only

read_document

C

No text

read_document_image — host model's vision

B?

Undetermined. You have to open it to know

Exists only in scan_folder's response

scan_folder does not open files, so it cannot confirm the grade. pdf·svg remain B?, and inspect_document opens them to confirm. Do not read B? in scan results as a confirmed value.

The samples are arranged to prove this — 흐름도.svg has text nodes so it is B, while 도형만.svg has only shapes so it is C. Same extension, different grades.

Read it with the host model's vision. No additional dependencies, and Korean accuracy is better than tesseract. If offline batch processing becomes necessary, an extract_text_ocr tool will be added separately.

Scanned PDFs are also read without a rasterizer. A scanned page is entirely one embedded image, so you can extract that image with pypdf — no PyMuPDF (AGPL) or poppler binaries needed.

Pages drawn purely as vectors cannot be extracted, and in that case the PDF_PAGE_HAS_NO_IMAGE error tells the human to "capture the screen." It does not silently return an empty result.

It only gives the table of contents · block count · character count · confirmed grade, and estimated_read_calls (the number of calls needed to read the whole document). Its reason for existing is to prevent dumping the body of a 300-page PDF into the context just to see what it looks like.

The cost of opening a file is the same as read_document — what is saved is not time but context.


Citation anchor contract

Format

Anchor

Meaning

docx

L14

14th block (paragraph or table row)

pptx

s7.2 / s7n

7th slide, 2nd line / speaker notes

pdf

p3

page 3

svg

t2

2nd text node

png

(none)

no text, so no anchor

Each format has a different unit, but read_document's interface is one. All formats are flattened into a 1-dimensional list of blocks, so you only need start/end. The response's unit tells you what a single block is.

If you change the anchor format, you must update grounding.ANCHOR_PATTERN and the golden set together. If they fall out of sync, valid citations will all be blocked by GR-02.


What grounding verification can and cannot check

  • Whether the sentence cites an anchor — GR-01

  • Whether that anchor exists in the document — GR-02

  • Whether figures and dates exist in the cited block's source text — GR-03

  • Whether direct quotes (inside quotation marks) match the source — GR-04

  • Whether the summary faithfully conveys the source's meaning

  • Whether something important was omitted

  • Whether the cited anchor is an appropriate anchor (GR-05 is only a lexical overlap hint)

Passing does not mean 'correct.' The response's not_verifiable states this limitation every time — if you pretend to verify what cannot be verified, people will believe "it passed, so it must be right," and that is more dangerous than no verification at all.

Paraphrasing the source is normal. Verification only looks at anchors, figures, and direct quotes.


Save gate

preview_save_report checks both structure (ST-*) and grounding (GR-*), and issues an approval token only when there are zero errors. The token is sha256(original relative path + draft), so changing even one character of the draft invalidates it — this blocks the path of previewing with a clean draft and saving a different one.

save_approved_report re-checks every gate. It does not trust the model's claim that the preview passed.

Order

Check

On failure

0

Is output_root outside the analysis root?

OUTPUT_INSIDE_ANALYSIS_ROOT

1

Structure (ST-*)

DRAFT_NOT_CLEAN

2

Grounding (GR-*)

DRAFT_NOT_CLEAN

3

Approval token

APPROVAL_TOKEN_MISMATCH

If an existing artifact exists, it is overwritten, and the hash of the previous content is recorded in the audit log. The audit log (data/outputs/_audit.jsonl) is append-only.


Harness layers (CAR)

Divided into three axes: Control–Agency–Runtime. First decide which axis the file you are changing belongs to. If the axis is unclear, that is a sign the design is wrong.

Axis

Question

Files

Control

What is it preventing?

paths.py · verify.py · grounding.py · reports.py · config/roots.toml

Agency

What does the model choose and how?

harness.py · server.py · phase4_tools.py · extract/ · scan.py · images.py · templates/

Runtime

What happened is recorded?

trace.py · evals/ · scripts/

Per-axis detailed contracts and dependency directions are in AGENTS.md chapter 2.

The server does not track progress (how much has been read). The model owns it, and the server only tells next_actions to "continue with start=N." That is why the server is stateless, and the write tool stays as a single save.

Self-verification

Invariants are checked right before returning a response; if they break, the server returns an error instead of a wrong answer.

Check

What it prevents

Anchor uniqueness and non-emptiness

Grounding verification pointing at the wrong block

Body line ↔ block consistency

Truncation cutting mid-block, causing grounding verification to fail

Aggregate sum = row count

The code not counting or double-counting numbers

Grade ↔ block contradiction

Reporting grade B but no blocks to read

What gets caught here is not a user input problem but a server bug. That is why the error message says "this is a server defect — stop the task and report it," not "please check the file."


Observability

Each tool call is recorded as one line in data/traces/YYYY-MM-DD.jsonl.

uv run python scripts/trace_report.py

What is not recorded is more important. When analyzing real internal documents, traces could become copies of those documents.

Rule

Enforcement

No body, excerpt, or TOC text

_sanitize_counters discards strings over 40 characters

No draft body

Not registered in ARG_ALLOWLIST

No absolute paths

Folded as (absolute-path)/filename

No error options

Only summary so the root absolute path list does not leak

This is enforced by code, not by convention, and verified by tests (tests/test_trace.py). If trace_dir is inside allowed_roots, tracing turns itself off — to avoid polluting the analyzed folder with its own records.

All 8 read tools have readOnlyHint: True, yet traces write files.

That hint means the analyzed documents are not modified. Traces are instrumentation logs outside allowed_roots and are not exposed through any tool. The only thing exposed through a write tool is save_approved_report, and the smoke test checks that list.


Evaluation

uv run python scripts/eval_extract.py

Compares the expected values in evals/golden/samples.json against actual extraction results and records the results in evals/reports/. pytest only tells you "does it pass right now," while this report records when and what passed.

The expected values are hand-written by looking at what scripts/make_samples.py put into the files. They are not copies of the extractor's output. If you adjust the golden set to match the results, the evaluation passes itself. The only legitimate reasons to change them are when the anchor contract, grade definitions, or sample contents change.


Dependencies

Package

License

Purpose

mcp[cli]

MIT

FastMCP server

pypdf

BSD

pdf text and embedded images

python-docx

MIT

docx

python-pptx

MIT

pptx

pillow

MIT-CMU

png metadata and image downscaling

svg is read with the standard xml.etree — zero dependencies.

Why not use PyMuPDF (fitz): performance is better, but it is AGPL-3.0, which imposes distribution conditions if included in an internal tool. If table extraction actually becomes necessary, add pdfplumber (MIT).


What is not committed

Path

Reason

data/samples/

Generated by scripts/make_samples.py

data/outputs/

Analysis results and audit log. Contains summaries of real documents

data/traces/

Execution logs. No body text, but filenames and paths remain

evals/reports/

Local run results. The golden set is committed

config/roots.local.toml

Personal paths

Do not place real documents to be analyzed inside this repository.

Install Server
F
license - not found
A
quality
C
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

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to interact with local documents (PDF, Markdown, TXT) through tools for discovery, reading, extraction, summarization, comparison, keyword extraction, search, and analysis, ensuring privacy and offline capability.
  • F
    license
    A
    quality
    C
    maintenance
    Enables read-only analysis of local unstructured documents by scanning a folder, extracting text and structural metadata, and passing content with truncation and error-awareness to an LLM for summarization.
    9
  • A
    license
    A
    quality
    C
    maintenance
    Enables reading and extracting text from local documents (PDF, Word, Excel, PowerPoint, HWP, Markdown, CSV, etc.) without network access, and provides approval-gated summary saving and file organization.
    11
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables local, read-only extraction of text and structure from PDF, DOCX, PPTX, SVG, and PNG files, including OCR for images, directory tree and metadata reporting, with strict path isolation and audit logging.

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/goods9999-ai/personal-file-analysis-mcp_test_20260826'

If you have feedback or need assistance with the MCP directory API, please join our Discord server