Docrag Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Docrag Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Pure deterministic tool server for document processing, chunking, and vector retrieval. No LLM inside — bring your own agent.
Accessible via MCP (Model Context Protocol) for AI agent integration (Claude, Copilot, LangChain, etc.) with streamable-http and stdio transports.
┌────────────────────────────┐
│ AI Agent (Claude, Copilot,│
│ LangChain + LLM) │
└─────────────┬──────────────┘
│ MCP protocol
▼
╔════════════════════════════════════════════════════════════════════════════╗
║ RAG Document Server (no LLM) ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ┌─ MCP Server ──────────────────────────────────────────────────────┐ ║
║ │ FastMCP · /mcp · streamable-http · stdio │ ║
║ └──────────────┬────────────────────────────────────────────────────┘ ║
╠═════════════════╩════════════════════════════════════════════════════════╣
║ MIDDLEWARE ─ request-id · rate-limit · timeout · logging ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ TOOLS (13) RESOURCES (2) ║
║ ├─ query.py ──────────────────┐ ├─ rag://supported-formats ║
║ │ process_document │ └─ rag://tool-descriptions ║
║ │ chunk_document │ ║
║ │ retrieve_chunks │ ║
║ │ query_spreadsheet │ ║
║ ├─ extract.py ────────────────┤ ║
║ │ pdf · docx · pptx │ ║
║ │ xlsx · csv · image │ ║
║ ├─ utility.py ────────────────┤ ║
║ │ detect_language │ ║
║ │ get_system_health │ ║
║ │ manage_cache │ ║
║ └─────────────────────────────┘ ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ┌─ Services ──────────┐ ┌─ Processors ─────────┐ ┌─ Core ──────────┐ ║
║ │ ▸ downloader (3×) │ │ ▸ PDF (PyMuPDF) │ │ ▸ config │ ║
║ │ ▸ cache (3-layer) │ │ ▸ DOCX (python-docx)│ │ ▸ errors │ ║
║ │ ▸ chunking │ │ ▸ PPTX (python-pptx)│ │ ▸ logging │ ║
║ │ ▸ retrieval (FAISS)│ │ ▸ XLSX/CSV (pandas) │ │ ▸ models │ ║
║ │ ▸ language detect │ │ ▸ Image (pytesseract)│ │ ▸ schemas │ ║
║ └────────────────────┘ │ ▸ HTML/TXT (BS4) │ └────────────────┘ ║
║ │ ▸ URL extractor │ ║
║ └──────────────────────┘ ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ML MODELS (eager-loaded at startup · no LLM) ║
║ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ ║
║ │ MiniLM-L6-v2 │ │ BGE-small-en │ │ ms-marco-MiniLM │ ║
║ │ fast embeddings│ │ accurate embed. │ │ cross-encoder reranker│ ║
║ └─────────────────┘ └──────────────────┘ └─────────────────────────┘ ║
╚═════════════════════════════════════════════════════════════════════════════╝flowchart TB
%% ── Clients ──────────────────────────────────────────────────
C1(["🌐 HTTP Client<br/>curl · Postman · Frontend"])
C2(["🤖 AI Agent + LLM<br/>Claude · Copilot · LangChain"])
%% ── Transport ────────────────────────────────────────────────
subgraph Transport[" 🔌 Transport Layer "]
direction LR
MCP["⚡ MCP Protocol<br/>FastMCP · /mcp<br/>streamable-http · stdio"]
end
%% ── Middleware ────────────────────────────────────────────────
subgraph MW[" 🛡️ Middleware Pipeline "]
direction LR
M2["⏱️ Rate Limit<br/>Token bucket"]
M3["✅ Validation<br/>URL · text"]
M4["📋 Logging<br/>JSON · Request-ID"]
M5["⏳ Timeout<br/>30s–300s"]
end
%% ── Tools ────────────────────────────────────────────────────
subgraph ToolsGroup[" 🔧 MCP Tools (13) + Resources (2) "]
direction LR
subgraph TQ[" query.py "]
direction TB
Q1(["process_document"])
Q2(["chunk_document"])
Q3(["retrieve_chunks"])
Q4(["query_spreadsheet"])
end
subgraph TE[" extract.py "]
direction TB
E1(["extract_pdf_text"])
E2(["extract_docx_text"])
E3(["extract_pptx_text"])
E4(["extract_xlsx_tables"])
E5(["extract_csv_tables"])
E6(["extract_image_text"])
end
subgraph TU[" utility.py "]
direction TB
U1(["detect_language"])
U2(["get_system_health"])
U3(["manage_cache"])
end
end
%% ── Services ─────────────────────────────────────────────────
subgraph Services[" ⚙️ Service Layer "]
direction LR
DL["📥 Downloader<br/>HTTP · 3× retry"]
CACHE["💾 3-Layer Cache<br/>Download · Document<br/>Retriever · 30 min TTL"]
CHUNK["✂️ Adaptive Chunking<br/>Type-aware sizes<br/>Importance scoring"]
RET["🔍 Retrieval Engine<br/>FAISS vector search<br/>Cross-encoder rerank<br/>Diversity filter"]
LANG["🌍 Language Detection<br/>3-round sampling"]
end
%% ── Processors ───────────────────────────────────────────────
subgraph Processors[" 📄 Document Processors "]
direction LR
PDF["PDF<br/>PyMuPDF"]
DOCX["DOCX<br/>python-docx"]
PPTX["PPTX<br/>python-pptx"]
XLSX["XLSX · CSV<br/>pandas"]
IMG["Image<br/>pytesseract"]
HTML["HTML · TXT<br/>BeautifulSoup"]
URLP["URL extract<br/>regex"]
end
%% ── Models ───────────────────────────────────────────────────
subgraph Models[" 🧠 ML Models — eager-loaded · no LLM "]
direction LR
EMB1["🚀 MiniLM-L6-v2<br/>Fast embeddings"]
EMB2["🎯 BGE-small-en-v1.5<br/>Accurate embeddings"]
RERANK["📊 ms-marco-MiniLM<br/>Cross-encoder reranker"]
end
%% ── Edges ────────────────────────────────────────────────────
C1 -- "MCP" --> MCP
C2 -- "MCP" --> MCP
MCP --> MW
M2 -.-> M3 -.-> M4 -.-> M5
MW --> ToolsGroup
TQ --> DL & CHUNK & RET
TE --> DL
TU --> LANG & CACHE
DL --> CACHE
DL --> Processors
CHUNK --> RET
RET --> Models
Processors --> LANG
Processors --> URLP
%% ── Styles ───────────────────────────────────────────────────
style C1 fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#0d47a1
style C2 fill:#b3e5fc,stroke:#0277bd,stroke-width:2px,color:#01579b
style Transport fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#e65100
style MCP fill:#ffe0b2,stroke:#f57c00,stroke-width:1px,color:#e65100
style MW fill:#fce4ec,stroke:#c62828,stroke-width:2px,color:#b71c1c
style M2 fill:#ffcdd2,stroke:#e53935,stroke-width:1px,color:#b71c1c
style M3 fill:#ffcdd2,stroke:#e53935,stroke-width:1px,color:#b71c1c
style M4 fill:#ffcdd2,stroke:#e53935,stroke-width:1px,color:#b71c1c
style M5 fill:#ffcdd2,stroke:#e53935,stroke-width:1px,color:#b71c1c
style ToolsGroup fill:#e0f2f1,stroke:#00695c,stroke-width:2px,color:#004d40
style TQ fill:#b2dfdb,stroke:#00897b,stroke-width:1px,color:#004d40
style TE fill:#b2dfdb,stroke:#00897b,stroke-width:1px,color:#004d40
style TU fill:#b2dfdb,stroke:#00897b,stroke-width:1px,color:#004d40
style Services fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
style DL fill:#c8e6c9,stroke:#43a047,stroke-width:1px,color:#1b5e20
style CACHE fill:#c8e6c9,stroke:#43a047,stroke-width:1px,color:#1b5e20
style CHUNK fill:#c8e6c9,stroke:#43a047,stroke-width:1px,color:#1b5e20
style RET fill:#c8e6c9,stroke:#43a047,stroke-width:1px,color:#1b5e20
style LANG fill:#c8e6c9,stroke:#43a047,stroke-width:1px,color:#1b5e20
style Processors fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
style PDF fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style DOCX fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style PPTX fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style XLSX fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style IMG fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style HTML fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style URLP fill:#e1bee7,stroke:#8e24aa,stroke-width:1px,color:#4a148c
style Models fill:#fff8e1,stroke:#f9a825,stroke-width:2px,color:#f57f17
style EMB1 fill:#fff9c4,stroke:#fbc02d,stroke-width:1px,color:#f57f17
style EMB2 fill:#fff9c4,stroke:#fbc02d,stroke-width:1px,color:#f57f17
style RERANK fill:#fff9c4,stroke:#fbc02d,stroke-width:1px,color:#f57f17pip install -r requirements.txtKey packages:mcp[cli]>=1.26.0,fastapi,uvicorn,langchain-huggingface,langchain-community,sentence-transformers,torch,PyMuPDF,python-docx,python-pptx,openpyxl,pandas,pytesseract,beautifulsoup4,faiss-cpu(orfaiss-gpufor CUDA acceleration).
The server uses .env for configuration. No required settings — sensible defaults are built in:
# .env (copy from .env.example and customise)
# MCP_RATE_LIMIT_RPM=60 # requests per minute per user (default: 60)
# MCP_REQUEST_TIMEOUT=300 # seconds per tool call (default: 300)
# GPU_CONCURRENCY=2 # max concurrent FAISS build/retrieval ops (default: 2)Note: No GOOGLE_API_KEY is needed for the server — it contains no LLM. LLM keys are only needed in the client agent.# ── MCP transport (default: streamable-http) ──────────────────────
python -m mcp_server # streamable-http, localhost:8000
python -m mcp_server --transport stdio # stdio (piped)
# ── Production (multi-worker for concurrent users) ────────────────
python -m mcp_server --workers 4 # 4 worker processes
python -m mcp_server --workers 4 --host 0.0.0.0 # expose to network
# ── Development mode (auto-reload on code changes) ────────────────
python -m mcp_server --reload # watches mcp_server/ for changes| CLI Argument | Choices | Default |
|---|---|---|
--transport | streamable-http, stdio | streamable-http |
--host | Any bind address | 127.0.0.1 |
--port | Any port number | 8000 |
--workers | Number of uvicorn worker processes | 1 |
--reload | Flag (no value) | Off |
Note:--reloadand--workers > 1are mutually exclusive (uvicorn limitation). In--reloadmode, workers is always forced to 1. Each worker loads its own copy of ML models (~1.5 GB), so ensure sufficient GPU/RAM when scaling workers.
The server exposes /health and /info endpoints via the MCPRouter. Use any MCP client or the bundled client/agent.py to connect and verify tools are available.
The client/ folder contains a separate process — a LangChain-powered ReAct agent that connects to the running MCP server and uses its tools with its own LLM (Gemini, OpenAI, etc.). All reasoning happens in the client; the server is just a tool provider.
cd client
pip install -r requirements.txt
cp .env.example .env # add your GOOGLE_API_KEY or OPENAI_API_KEY
python agent.py # interactive REPL mode
python agent.py "Summarise https://example.com/report.pdf" # one-shot┌────────────────────┐ MCP (streamable-http) ┌──────────────────────┐
│ client/agent.py │ ◄────────────────────────► │ MCP Server │
│ │ │ (pure tools) │
│ • LLM (Gemini) │ tool calls: │ • extract_pdf_text │
│ • ReAct agent │ – process_document │ • chunk_document │
│ • Reasoning │ – retrieve_chunks │ • retrieve_chunks │
│ • Answers │ – detect_language … │ • FAISS + rerank │
└────────────────────┘ └──────────────────────┘See client/README.md for full details on the agent architecture, LLM selection, environment variables, and example conversations.
This walkthrough shows the full flow — hosting a file, starting the MCP server, and querying it through the LangChain agent.
Step 1 — Serve your documents locally (separate terminal):
cd docs/ # folder containing your files
python -m http.server 9090 # serves files at http://localhost:9090/Step 2 — Start the MCP server (separate terminal):
python -m mcp_server # streamable-http on http://127.0.0.1:8000Step 3 — Run the agent (separate terminal):
cd client
python agent.pyStep 4 — Chat with your data:
LangChain MCP Agent
Type 'quit' to exit
> get the phone number of John Doe from http://localhost:9090/Student_Data.xlsx
[TOOL CALL] query_spreadsheet(search_value='John Doe', document_url='http://localhost:9090/Student_Data.xlsx')
[TOOL RESULT] query_spreadsheet → [{'type': 'text', 'text': '{\n "matches": [\n {\n "NAME": "John Doe",\n "PHONE NUMBER": "9876543210",\n "EMAIL ID": "[email protected]",\n ...
The phone number for John Doe is 9876543210.
> summarise https://example.com/quarterly-report.pdf
[TOOL CALL] process_document(document_url='https://example.com/quarterly-report.pdf')
...
The report covers Q3 revenue growth of 12% ...The agent automatically selects the right MCP tool (query_spreadsheet for row lookups, retrieve_chunks for semantic search, extract_* for raw extraction, etc.) based on your natural-language query.
Tip: You can also pass a one-shot query directly: ``bash python agent.py "Find email of Jane Smith from http://localhost:9090/Student_Data.xlsx" ``| # | Tool | Input | Output | Timeout |
|---|---|---|---|---|
| 1 | process_document | document_url: str | {content (≤50K chars), content_length, metadata, tables[], images[], urls[], detected_language, detected_language_name} | 300 s |
| 2 | chunk_document | document_url: str | {chunks[{text (≤5K), chunk_index, total_chunks, importance_score, content_type}], chunk_count, document_type} | 300 s |
| 3 | retrieve_chunks | document_url: str, query: str, top_k: int (1–20, default 5) | {results[{text, chunk_index, importance_score, content_type}], total_chunks_indexed} | 300 s |
| 4 | query_spreadsheet | document_url: str, search_value: str | {matches[{row data}], match_count, sheets_searched} | 300 s |
`retrieve_chunks` internal pipeline:
top_k best chunkssha256(url)[:16])`query_spreadsheet` — pandas row lookup:
| # | Tool | Input | Output | Timeout |
|---|---|---|---|---|
| 5 | extract_pdf_text | document_url: str | {text (≤50K chars), char_count} | 120 s |
| 6 | extract_docx_text | document_url: str | {text (≤50K chars), char_count} | 120 s |
| 7 | extract_pptx_text | document_url: str | {text (≤50K chars), char_count} | 120 s |
| 8 | extract_xlsx_tables | document_url: str | {tables[{content (≤5K), table_type, location, metadata}], table_count} | 120 s |
| 9 | extract_csv_tables | document_url: str | {tables[{content (≤5K), table_type, location, metadata}], table_count} | 120 s |
| 10 | extract_image_text | image_url: str | {ocr_results[{text, confidence, metadata}]} | 120 s |
| # | Tool | Input | Output | Timeout |
|---|---|---|---|---|
| 11 | detect_language | text: str | {language_code, language_name} | 30 s |
| 12 | get_system_health | (none) | Full health report: status, version, features, security, models, formats, device, cache stats, timestamp | 30 s |
| 13 | manage_cache | action: str ("stats" / "clear") | Cache statistics per layer or eviction counts | 30 s |
| URI | Description |
|---|---|
rag://supported-formats | Human-readable list of all supported document formats |
rag://tool-descriptions | Summary of all 13 tools and their parameters |
├── README.md
├── requirements.txt # Server dependencies (no LLM)
├── .env.example # Example environment variables
├── .gitignore
├── LICENSE # MIT
│
├── mcp_server/ # ─── Server package ───
│ ├── __init__.py
│ ├── __main__.py # CLI: --transport streamable-http|stdio --reload --workers N
│ ├── server.py # FastMCP instance, lifespan, tool registration
│ ├── _asgi.py # ASGI factory for --reload mode (uvicorn)
│ │
│ ├── core/
│ │ ├── config.py # Frozen dataclass configs, feature flags, device detection
│ │ ├── concurrency.py # GPU semaphore, FAISS build coalescing, dedicated thread pool
│ │ ├── logging.py # Structured JSON logging to stderr, request-id ContextVar
│ │ ├── errors.py # Exception hierarchy (6 error types)
│ │ ├── schemas.py # ProcessedDocument, ExtractedTable, ExtractedImage, ExtractedURL
│ │ └── models.py # Eager-loaded ML models (embeddings + reranker only)
│ │
│ ├── middleware/
│ │ ├── __init__.py # @guarded() decorator — full middleware chain
│ │ └── guards.py # Per-user + global rate-limit, URL/text validation, MCPRouter
│ │
│ ├── services/
│ │ ├── cache.py # Generic _TTLCache, 3 singleton layers
│ │ ├── downloader.py # Async httpx downloads with connection pooling + 3× retry
│ │ ├── language.py # Multi-round majority-vote language detection
│ │ ├── chunking.py # Adaptive chunking strategy + importance scoring
│ │ └── retrieval.py # FAISS vector search + cross-encoder reranking + diversity filter
│ │
│ ├── processors/
│ │ ├── __init__.py # detect_document_type(), TargetedDocumentProcessor dispatcher
│ │ ├── pdf.py # PyMuPDF — dict-based extraction with layout preservation
│ │ ├── docx.py # python-docx — heading hierarchy + table extraction
│ │ ├── pptx.py # python-pptx — slides, notes, tables, hyperlinks
│ │ ├── xlsx.py # pandas + openpyxl — header detection, column analysis; also CSV
│ │ ├── image.py # pytesseract — per-word OCR with confidence scores
│ │ └── url.py # Regex URL extraction with context + categorisation
│ │
│ ├── tools/
│ │ ├── query.py # process_document, chunk_document, retrieve_chunks, query_spreadsheet
│ │ ├── extract.py # Per-format extraction (PDF, DOCX, PPTX, XLSX, CSV, Image)
│ │ └── utility.py # detect_language, get_system_health, manage_cache
│ │
│ ├── resources/
│ │ └── __init__.py # rag://supported-formats, rag://tool-descriptions
│ │
│ ├── temp_files/ # Auto-created — temporary download / OCR staging + file uploads
│ ├── faiss_indexes/ # Auto-created — persisted FAISS indexes (survives restarts)
│ └── request_logs/ # Auto-created — structured request logs
│
└── client/ # ─── Separate agent (has LLM) ───
├── README.md
├── requirements.txt # langchain, langchain-google-genai, langchain-mcp-adapters
├── .env.example
└── agent.py # LangChain ReAct agent connecting via MCPAll configuration lives in core/config.py as frozen dataclasses (immutable singletons created at import time). No .yaml or .toml — just Python constants with optional environment variable overrides for security settings.
| Constant | Value | Purpose |
|---|---|---|
BASE_DIR | Parent of mcp_server/ package | Root path for temp/log dirs |
TEMP_FILES_PATH | <BASE_DIR>/temp_files/ | Temporary downloads, OCR staging |
REQUEST_LOGS_PATH | <BASE_DIR>/request_logs/ | Structured request logs |
Both directories are auto-created on import if they don't exist.
Runs once at import time:
torch.cuda.is_available() → "cuda"torch.backends.mps.is_available() → "mps" (Apple Silicon)"cpu" (including when torch is not installed)| Flag | Dependency | Fallback |
|---|---|---|
RERANK_AVAILABLE | sentence_transformers.CrossEncoder | Reranking skipped; similarity results returned as-is |
OCR_AVAILABLE | pytesseract | OCR tools return an error message |
LANG_DETECT_AVAILABLE | langdetect | Always defaults to "en" |
#### ServerConfig
| Field | Type | Default |
|---|---|---|
name | str | "RAG Document Server" |
version | str | "2.1.0" |
host | str | "127.0.0.1" |
port | int | 8000 |
transport | str | "streamable-http" |
#### ModelConfig
| Field | Type | Default |
|---|---|---|
embedding_fast | str | "sentence-transformers/all-MiniLM-L6-v2" |
embedding_accurate | str | "BAAI/bge-small-en-v1.5" |
reranker | str | "cross-encoder/ms-marco-MiniLM-L-6-v2" |
#### CacheConfig
| Field | Type | Default |
|---|---|---|
default_ttl | int | 1800 (30 min) |
max_download_entries | int | 50 |
max_document_entries | int | 50 |
max_retriever_entries | int | 20 |
max_download_bytes | int | 524,288,000 (500 MB) |
#### SecurityConfig
| Field | Type | Default | Env Var |
|---|---|---|---|
rate_limit_rpm | int | 60 | MCP_RATE_LIMIT_RPM |
max_url_length | int | 2048 | — |
max_text_length | int | 100,000 | — |
request_timeout | int | 300 | MCP_REQUEST_TIMEOUT |
Every tool invocation passes through the @guarded(timeout=...) decorator. This decorator implements a complete middleware chain that ensures tools never raise exceptions to the client.
Request → [1] Request ID → [2] Rate Limit → [3] Execute w/ Timeout → [4] Log → Responseuuid4().hex[:12] stored in a ContextVar for logcorrelation across the entire call stack.
check_rate_limit(tool_name, api_key)) — two-tier token-bucket:rate_limit_rpm (default 60) per API keyrpm / 60.0 tokens per secondconsume() call (no background thread)RateLimitError when per-user or global tokens exhaustedasyncio.wait_for(fn(...), timeout=...):TimeoutError (caught by the decorator, returned as {"code": "TIMEOUT"})tool.start, tool.success (with elapsed time),or tool.timeout / tool.known_error / tool.unhandled_error events.
MCPServerError subclass → {"error": exc.message, "code": exc.code}asyncio.TimeoutError → {"error": "...", "code": "TIMEOUT"}Exception → {"error": "...", "code": "INTERNAL_ERROR"}request_id_var.reset(token) in finally block| Validator | Rules | Raises |
|---|---|---|
validate_url(url) | Non-empty string, ≤ 2048 chars, ^https?://[safe-url-chars]+$ | ValidationError |
validate_text(text, field) | Must be a string, ≤ 100,000 chars | ValidationError |
The cache system uses a generic _TTLCache class — thread-safe (threading.Lock), size-bounded, with time-based expiration. Each cache entry is a _CacheEntry dataclass containing value, expires_at (float timestamp), and size_bytes.
| Layer | Key | Stores | TTL | Max Entries | Max Bytes |
|---|---|---|---|---|---|
| Download | URL string | Raw HTTP response bytes | 30 min | 50 | 500 MB |
| Document | sha256(url)[:16] | ProcessedDocument objects | 30 min | 50 | — |
| Retriever | sha256(url)[:16] | EnhancedRetriever (FAISS index + chunks) | 30 min | 20 | — |
On every put() call, the following eviction sequence runs:
now > expires_attotal_bytes > max_download_bytes, evict oldest entrylen(cache) >= max_entries, evict oldest entryexpires_at value// Inspect cache statistics (per-layer hit/miss rates)
{"tool": "manage_cache", "arguments": {"action": "stats"}}
// Clear all three cache layers
{"tool": "manage_cache", "arguments": {"action": "clear"}}| Function | Purpose |
|---|---|
get_cached_download(url) / put_cached_download(url, data) | Download layer |
get_cached_document(key) / put_cached_document(key, doc) | Document layer |
get_cached_retriever(key) / put_cached_retriever(key, ret) | Retriever memory layer |
get_retriever_with_disk_fallback(hash, emb) | Memory → disk → None lookup |
put_retriever_with_disk(hash, ret) | Save to memory + persist to disk |
clear_faiss_disk() | Delete all persisted FAISS indexes |
faiss_disk_stats() | Count & size of on-disk indexes |
clear_all() | Flush all layers (memory + disk) |
cache_stats() | Per-layer hit/miss rates + disk stats |
processors/__init__.py)`detect_document_type(url)` — parses the URL path and maps the file extension:
| Extension(s) | Type | Processor |
|---|---|---|
.pdf | "pdf" | extract_text_from_pdf() |
.doc, .docx | "docx" | extract_text_from_docx() |
.ppt, .pptx | "pptx" | extract_text_from_pptx() |
.xls, .xlsx | "xlsx" | extract_tables_from_xlsx() |
.csv | "csv" | extract_tables_from_csv() |
.txt | "txt" | UTF-8 decode |
.htm, .html | "html" | WebBaseLoader → BeautifulSoup fallback |
.png, .jpg, .jpeg | "image" | extract_text_from_image() |
| anything else | "unknown" | UTF-8 decode with errors="replace" |
Fallback safety: If any format-specific processor throws an exception, the dispatcher catches it and falls back to raw file_content.decode("utf-8", errors="replace").
After extraction, the dispatcher also:
URLExtractordetect_language_robust()ProcessedDocument dataclassprocessors/pdf.py)fitz)page.get_text("dict", sort=True), reassembles text blocks with page markers --- Page N ---page.get_text() on any exceptionprocessors/docx.py)python-docx# heading, ## heading, etc.| cell | cell |processors/pptx.py)python-pptxprocessors/xlsx.py)pandas + openpyxluniqueness × 0.5 + text_ratio × 0.3 + coverage × 0.2processors/xlsx.py)pandaspd.read_csv() with automatic header detectionprocessors/image.py)pytesseract + Pillowimage_to_data for per-word confidence → filter conf > 0 → compute mean confidencefinally block even on failureprocessors/url.py)https?://[^\s<>"']+ or www.[^\s<>"']+.[^\s<>"']+api_endpoint, navigation, image, or general0.9The chunking service (services/chunking.py) uses AdaptiveChunkingStrategy — a set of static methods that determine optimal chunk parameters based on document type and content length.
| Doc Type | Chunk Size | Overlap | Separators |
|---|---|---|---|
pdf | 1500 | 300 | \n\n, \n, . , |
pptx | 800 | 150 | \n---\n, \n\n, \n, . , |
xlsx / csv | 1200 | 200 | \n===, \n---, \n\n, \n, |
docx / html | 1500 | 300 | \n\n, \n, . , |
| Default | 1200 | 250 | \n\n, \n, . , |
| Content Length | Scaling |
|---|---|
| > 100,000 chars | chunk_size × 1.5, overlap × 1.3 |
| < 5,000 chars | chunk_size ÷ 2 (min 400), overlap ÷ 2 (min 50) |
| 5,000 – 100,000 | No scaling |
Each chunk receives an importance score in [0.0, 1.0]:
| Condition | Score Delta |
|---|---|
| Base score | +0.5 |
Headings detected (^#{1,3}\s or ^ALL-CAPS-LINE$) | +0.2 |
Numbers/currency (\d+\.?\d*%, $\d+, €\d+) | +0.15 |
Keywords: important, key, critical, summary, conclusion, result, finding, recommendation | +0.1 |
| Text length < 50 chars | −0.2 |
Result is clamped to [0.0, 1.0] and rounded to 2 decimal places.
Each chunk is classified as one of:
| Type | Detection Rule | |
|---|---|---|
"table" | Contains pipe `\ | ` characters or tabs |
"list" | Contains bullet points (- , • , * ) | |
"heading" | Matches markdown heading syntax | |
"text" | Default |
Each chunk carries: chunk_index, total_chunks, importance_score, content_type, doc_type.
The underlying splitter is LangChain's RecursiveCharacterTextSplitter.
The retrieval service (services/retrieval.py) implements EnhancedRetriever — an on-the-fly FAISS vector search engine with cross-encoder reranking, diversity filtering, disk persistence, and concurrency controls.
Chunks → Embedding → FAISS Index → Save to disk → Similarity Search (3× over-retrieval)
→ Cross-Encoder Reranking → Diversity Filter → top_k resultsFAISS.from_documents(chunks, embeddings) fromlangchain_community.vectorstores. Built on every new document, then persisted to `faiss_indexes/<url_hash>/` and cached in memory. On subsequent queries (even after restart), the index is loaded from disk via FAISS.load_local() instead of being rebuilt.
Concurrency controls (from core/concurrency.py):
run_in_gpu_pool(),limited to GPU_CONCURRENCY (default 2) simultaneous operations. Prevents OOM under burst traffic.
builds the index; the other 9 wait on a per-URL asyncio.Lock, then read from cache. Eliminates redundant embedding work.
get_embeddings_fast() (MiniLM-L6-v2) — cross-encoder reranking compensatesget_embeddings_accurate() (BGE-small-en-v1.5)vectorstore.similarity_search(query, k=min(top_k * 3, 20)).Fetches 3× the requested number of candidates (capped at 20).
RERANK_AVAILABLE and use_reranking=True):[query, chunk_text] pairsCrossEncoder.predict(pairs) using ms-marco-MiniLM-L-6-v2top_k_diversity_filter):importance_score descendingcontent_type valuescontent_type hasn't been seen yet, or if len(selected) < top_ktop_kAll ML models (embeddings + reranker) are loaded eagerly at server startup via _ensure_models_loaded() called during the FastMCP lifespan (or in the _asgi.py factory for --reload mode). Each model logs its name with a ✓ checkmark when loaded.
A module-level _loaded boolean is checked first (fast path), then re-checked inside a threading.Lock (safe path). This ensures models are loaded exactly once even under concurrent requests.
| Variable | Class | Model ID | Key Settings |
|---|---|---|---|
_embeddings_fast | HuggingFaceEmbeddings | sentence-transformers/all-MiniLM-L6-v2 | normalize_embeddings=True, batch_size=32, auto device |
_embeddings_accurate | HuggingFaceEmbeddings | BAAI/bge-small-en-v1.5 | Same settings |
_reranker | CrossEncoder | cross-encoder/ms-marco-MiniLM-L-6-v2 | max_length=512, only loaded if RERANK_AVAILABLE |
| Function | Returns |
|---|---|
get_embeddings_fast() | Fast embedding model instance |
get_embeddings_accurate() | Accurate embedding model instance |
get_reranker() | Cross-encoder reranker (or None) |
models_loaded() | bool — whether models have been initialised |
All logs are structured JSON emitted to stderr (keeping stdout free for MCP stdio transport). Additionally, logs are written to daily rotating files in request_logs/server_YYYY-MM-DD.log.
{
"ts": "2025-01-15T10:30:00.000Z",
"level": "INFO",
"logger": "mcp_server.tools.query",
"msg": "tool.success",
"rid": "a1b2c3d4e5f6",
"tool": "process_document",
"elapsed": 2.45
}| Field | Source | Description |
|---|---|---|
ts | Auto | UTC ISO timestamp |
level | Auto | INFO, WARNING, ERROR |
logger | Auto | Module path |
msg | Code | Event name (tool.start, tool.success, tool.timeout, etc.) |
rid | ContextVar | 12-char request ID (default "system") |
tool | Extra | Tool name |
elapsed | Extra | Execution time in seconds |
url | Extra | Document URL (when relevant) |
code | Extra | Error code |
attempt, wait | Extra | Retry metadata from downloader |
bytes | Extra | Download size |
httpx, httpcore, urllib3, sentence_transformers, filelock — all set to WARNING level to reduce noise.
All custom exceptions inherit from MCPServerError and carry a stable .code string for programmatic matching plus a human-readable .message:
MCPServerError(Exception) code="INTERNAL_ERROR"
├── RateLimitError code="RATE_LIMITED" msg="Rate limit exceeded"
├── ValidationError code="VALIDATION_ERROR"
├── DownloadError code="DOWNLOAD_ERROR"
├── ProcessingError code="PROCESSING_ERROR"
└── ModelLoadError code="MODEL_LOAD_ERROR"The @guarded decorator catches all of these and converts them to {"error": ..., "code": ...} dicts — tools never raise to the MCP client or MCP consumer.
Additional timeout errors are surfaced as {"code": "TIMEOUT"}.
All data objects are plain Python @dataclass instances (no Pydantic in the server core):
| Dataclass | Fields |
|---|---|
ExtractedTable | content: str, table_type: str = "unknown", location: str = "", metadata: Dict |
ExtractedImage | image_path: str, ocr_text: str, metadata: Dict, confidence: float = 0.0 |
ExtractedURL | url: str, context: str, source_location: str, confidence: float = 0.0, url_type: str = "general" |
ProcessedDocument | content: str, metadata: Dict, tables: List[ExtractedTable], images: List[ExtractedImage], extracted_urls: List[ExtractedURL], detected_language: str = "en" |
The detect_language tool (services/language.py) uses a multi-round majority-vote algorithm for robustness:
langdetect.detect() 3 timesCounter.most_common(1)DetectorFactory.seed = 0 for reproducibilityFallbacks:
langdetect not installed → returns "en""en""en"Supported languages (name map): English, Spanish, French, German, Italian, Portuguese, Hindi, Bengali, Telugu, Tamil, Marathi, Malayalam, Kannada, Gujarati, Punjabi, Urdu, Chinese, Japanese (18 languages).
| Category | Formats | Processor Library | Key Features |
|---|---|---|---|
| Documents | PyMuPDF (fitz) | Dict-based extraction with layout preservation, page markers, 2-level fallback | |
| DOCX | python-docx | Heading hierarchy, table extraction as Markdown | |
| PPTX | python-pptx | Per-slide title/body/notes/tables, hyperlink extraction | |
| TXT | Built-in | UTF-8 decode | |
| HTML | BeautifulSoup / WebBaseLoader | Dual fallback | |
| Tables | XLSX | pandas + openpyxl | Header auto-detection (top-10-row scoring), column analysis, cross-sheet relationships |
| CSV | pandas | Same formatting pipeline as XLSX | |
| Images | PNG, JPEG, JPG | pytesseract + Pillow | Per-word OCR with confidence scores |
| Content Type | Max Length |
|---|---|
| Full document content | 50,000 chars |
| Individual chunk text | 5,000 chars |
| Table content per table | 5,000 chars |
| XLSX rows per sheet | 20 rows |
.env at project root)| Variable | Required | Default | Description |
|---|---|---|---|
MCP_RATE_LIMIT_RPM | No | 60 | Per-user rate limit (requests/minute); global cap is 5× this value |
MCP_REQUEST_TIMEOUT | No | 300 | Default tool timeout in seconds |
GPU_CONCURRENCY | No | 2 | Max concurrent FAISS build/retrieval operations (GPU semaphore) |
HUGGINGFACE_TOKEN | No | — | HuggingFace model access (for gated models) |
GOOGLE_API_KEY/OPENAI_API_KEYare only needed in theclient/agent — the server has no LLM.
client/.env)| Variable | Required | Default | Description |
|---|---|---|---|
GOOGLE_API_KEY | Yes (one of) | — | Gemini LLM (default) |
OPENAI_API_KEY | Yes (one of) | — | OpenAI fallback |
MCP_SERVER_URL | No | http://127.0.0.1:8000/mcp | MCP server endpoint |
| Variable | Purpose |
|---|---|
LANGCHAIN_API_KEY | LangSmith tracing key |
LANGSMITH_TRACING | Enable LangSmith tracing |
LANGSMITH_ENDPOINT | Custom tracing endpoint |
LANGCHAIN_PROJECT | LangSmith project name |
.vscode/mcp.json){
"servers": {
"rag-pipeline": {
"type": "http",
"url": "http://127.0.0.1:8000/mcp"
}
}
}claude_desktop_config.json){
"mcpServers": {
"rag-pipeline": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}.cursor/mcp.json){
"mcpServers": {
"rag-pipeline": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}# MCP server (streamable-http)
python -m mcp_server 2>&1
# MCP server (stdio — for piped agent connections)
python -m mcp_server --transport stdio
# Development mode (auto-reload on code changes)
python -m mcp_server --reloadtools/query.py, tools/extract.py, or tools/utility.py@mcp.tool() then @guarded(timeout=...): @mcp.tool()
@guarded(timeout=120)
async def my_new_tool(document_url: str) -> dict:
validate_url(document_url)
# ... implementation ...
return {"result": "..."}server.pyresources/__init__.py to include the tool in rag://tool-descriptionsprocessors/processors/__init__.py → detect_document_type()TargetedDocumentProcessor.process_document()tools/extract.pyservices/chunking.py → _get_chunk_params()| Scenario | Behaviour |
|---|---|
| Missing optional dependency (pytesseract, langdetect, etc.) | Feature flag disables gracefully — no crash |
| PDF extraction failure | 2-level fallback: dict-based → raw text → empty string |
| HTML processing failure | Falls back from WebBaseLoader to BeautifulSoup |
| Unknown document type | Treated as plain text via UTF-8 decode with errors="replace" |
| Any processor throws | Dispatcher catches and falls back to raw UTF-8 decode |
| Empty content | Returns {chunks: [], chunk_count: 0} or {results: [], total_chunks_indexed: 0} |
| Reranking failure | Logged as warning, falls back to truncated similarity results |
| Download failure | 3 retries with 1s / 3s / 5s backoff, then raises DownloadError |
| Output too large | Content capped at 50K chars, tables at 5K, XLSX at 20 rows |
top_k out of range | Clamped: max(1, min(top_k, 20)) |
| Image temp files | Cleaned up in finally even on failure |
| Concurrent model loading | Thread-safe via double-checked locking with threading.Lock |
MIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.