opencode-document-rag-mcp
# Local Document MCP Server with Marker and ChromaDB
This project implements a Python MCP server for OpenCode. It reads PDF, Word (`.docx`), PowerPoint (`.pptx`), and EPUB files located under `DOCS/`, always excluding `DOCS/mdDB/`. It converts the documents to Markdown with Marker, preserves tables and equations as LaTeX, stores the complete Markdown files under `DOCS/mdDB/`, and creates a persistent semantic index in ChromaDB.
Retrieval is structure-aware. ChromaDB locates the chunks most relevant to a query, but the MCP server does not return an isolated chunk. It uses the result metadata to open the original Markdown file and reconstruct the complete section delimited by headings. The response includes the surrounding text, tables, and equations, together with file paths and line ranges.
## Data Flow
```mermaid
flowchart TD
A["DOCS: PDF, DOCX, PPTX, EPUB"] --> B["Marker 2"]
B --> C["Complete Markdown + images"]
C --> D["DOCS/mdDB"]
C --> E["Structural chunks"]
E --> F["Local ChromaDB"]
G["OpenCode query"] --> F
F --> H["Chunk metadata"]
H --> D
D --> I["Complete Markdown section"]
I --> G
```
At a minimum, each chunk stores `source_path`, `markdown_path`, `section_title`, `section_path`, `section_start_line`, `section_end_line`, `chunk_start_line`, and `chunk_end_line`. It also stores SHA-256 hashes for the source document and Markdown file to detect changes.
## Project Structure
```text
current-project/
├── DOCS/
│ ├── article.pdf
│ ├── manual.docx
│ └── mdDB/
│ ├── article.md
│ ├── manual.md
│ └── .chroma/
├── .opencode/
│ └── MCP/
│ └── opencode-document-rag-mcp/
│ ├── src/doc_rag_mcp/
│ ├── tests/
│ ├── README.md
│ └── pyproject.toml
└── opencode.jsonc
```
Source documents may be placed directly under `DOCS/` or in any of its subdirectories except `DOCS/mdDB/`. Their relative directory structure is preserved in the output. For example, `DOCS/manuals/instrument.pdf` produces `DOCS/mdDB/manuals/instrument.md`. Extracted images are stored next to the Markdown file under `instrument_assets/`, and their links are rewritten as relative paths. The entire `DOCS/mdDB/` tree is excluded from discovery so the MCP server cannot process its own output.
## Requirements
Python 3.10–3.13 and `uv` are required. Marker 2 requires an inference backend for OCR and equations. `llama.cpp` is recommended on macOS or CPU-only systems. Systems with NVIDIA GPUs can use the VLLM backend configured through Surya.
On macOS:
```bash
brew install uv llama.cpp
```
On Linux, install `uv` and a recent `llama-server` binary provided by llama.cpp. For NVIDIA systems, install Docker and the NVIDIA Container Toolkit according to Marker’s requirements.
## Installation
Extract the release archive directly into the root of the current project. The archive already contains the `.opencode/MCP/opencode-document-rag-mcp/` directory structure:
```bash
cd /path/to/current-project
unzip opencode-document-rag-mcp-v1.1.2.zip -d .
uv sync --project .opencode/MCP/opencode-document-rag-mcp
```
After extraction, the MCP server is installed at exactly:
```text
.opencode/MCP/opencode-document-rag-mcp
```
The first conversion and first vectorization download the required models. The ONNX embedding model is stored under `DOCS/mdDB/.chroma/.embedding_models/`. Marker models use the cache configured by Marker and Surya. The initial process may take some time and consume several gigabytes. DOCX, PPTX, and EPUB documents require the `marker-pdf[full]` variant, which is already included in `pyproject.toml`.
## OpenCode Configuration
Copy the configuration from `opencode.example.jsonc` into the `opencode.json` or `opencode.jsonc` file at the project root. If the MCP server is stored elsewhere, change only the path that follows `--project`.
Minimum configuration:
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"document-rag": {
"type": "local",
"command": [
"uv",
"run",
"--project",
".opencode/MCP/opencode-document-rag-mcp",
"doc-rag-mcp"
],
"cwd": ".",
"enabled": true,
"timeout": 30000,
"environment": {
"DOC_RAG_PROJECT_ROOT": ".",
"SURYA_INFERENCE_BACKEND": "llamacpp",
"SURYA_INFERENCE_KEEP_ALIVE": "true"
}
}
}
}
```
The `cwd: "."` setting resolves all paths relative to the root of the project opened in OpenCode. Verify the connection with:
```bash
opencode mcp list
```
The `AGENTS.example.md` file contains an optional policy that instructs OpenCode to query this MCP server before answering questions about the documents. You can incorporate its contents into the project’s `AGENTS.md` file.
## MCP Tools
| Tool | Function |
|---|---|
| `list_documents` | Lists supported files under `DOCS/`, excluding `DOCS/mdDB/`. |
| `ingest_document` | Converts and indexes one file. Setting `force=true` repeats the conversion. |
| `ingest_all_documents` | Synchronizes all source documents and skips unchanged files. |
| `search_documents` | Performs a semantic search and returns complete Markdown sections from disk. |
| `read_markdown_section` | Reads a specific section by its hierarchical path. |
| `index_status` | Reports indexed documents and chunk counts. |
## Using the MCP Server in OpenCode
Place source documents under `DOCS/`, but never under `DOCS/mdDB/`. You can then use requests such as:
```text
Use document-rag to list the available documents.
```
```text
Use ingest_all_documents to convert and index every source document under DOCS, excluding mdDB.
```
```text
Search the documents for the definition of wave energy flux, preserving the related LaTeX equations and tables.
```
```text
Search only manual_tecnico.pdf for the instrument's operating limits and cite the Markdown section and line range.
```
```text
Read the Methods > Statistical analysis section from article.docx.
```
## Expanded Retrieval
`search_documents` accepts `query`, a `top_k` value from 1 through 20, and an optional `document_name`. Internally, it requests additional results from ChromaDB so multiple chunks from the same section do not occupy every result position. It then removes duplicate sections and returns up to `top_k` distinct sections.
Each result contains `context`, which is the complete section read from disk at query time. `index_is_current` indicates whether the Markdown file still has the same hash it had when it was indexed. If this value is `false`, run `ingest_document` or `ingest_all_documents`. When the source document has not changed, the system reindexes the existing Markdown without running Marker again.
## Conversion and Equations
Marker emits formatted tables and LaTeX equations delimited by `$$`. The default mode is `balanced`, which is appropriate when table, OCR, and mathematical fidelity are the priority. On CPU or Apple Silicon systems, reduce processing cost with:
```json
"DOC_RAG_MARKER_MODE": "fast"
```
For scanned documents or unreadable text:
```json
"DOC_RAG_FORCE_OCR": "true"
```
For Marker’s optional hybrid correction through a compatible LLM service:
```json
"DOC_RAG_USE_LLM": "true"
```
The last option requires credentials and a service supported by Marker. It is not required for normal MCP server operation.
## Environment Variables
| Variable | Default | Description |
|---|---:|---|
| `DOC_RAG_PROJECT_ROOT` | `.` | Root of the currently opened project. |
| `DOC_RAG_SOURCE_DIR` | `DOCS` | Source document directory; `DOCS/mdDB/` is excluded. |
| `DOC_RAG_MARKDOWN_DIR` | `DOCS/mdDB` | Complete Markdown storage directory. |
| `DOC_RAG_CHROMA_DIR` | `DOCS/mdDB/.chroma` | Local ChromaDB persistence directory. |
| `DOC_RAG_COLLECTION` | `document_markdown` | ChromaDB collection name. |
| `DOC_RAG_CHUNK_MAX_CHARS` | `2400` | Target size for each chunk. |
| `DOC_RAG_MARKER_MODE` | `balanced` | Marker’s `balanced` or `fast` mode. |
| `DOC_RAG_FORCE_OCR` | `false` | Forces OCR across the entire document. |
| `DOC_RAG_USE_LLM` | `false` | Enables Marker’s hybrid LLM correction. |
## Security and Consistency
The server rejects unsupported extensions, `..` path traversal, sources outside `DOCS/`, any source inside `DOCS/mdDB/`, and Markdown paths outside `DOCS/mdDB/`. A path stored in ChromaDB is never used without being validated again. Markdown writes are atomic, and index replacement is limited to the corresponding document.
If two files in the same directory have the same base name, such as `manual.pdf` and `manual.docx`, both would produce `manual.md`. The server detects this collision and requires one of the source files to be renamed before writing or indexing.
## Tests
The unit tests do not load Marker or ChromaDB. They validate hierarchical segmentation, preservation of tables and equations, section expansion, and path protection:
```bash
PYTHONPATH=src python -m unittest discover -s tests -v
```
You can also check the syntax of the complete source tree with:
```bash
python -m compileall -q src tests
```
## Licenses
This project is distributed under the MIT License. Marker uses the Apache-2.0 License for its code and a separate license for its model weights. Review Marker’s terms before large-scale commercial use.
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: listing documents, ingesting a single document, ingesting all documents, searching, reading a section, and checking index status. The only potential overlap (ingest_document vs. ingest_all_documents) is clearly resolved by singular vs. plural scope, leaving no ambiguity for an agent.
Most tools follow a verb_noun pattern (list_, ingest_, search_, read_), but 'index_status' deviates by starting with a noun. Despite this minor inconsistency, all names are lowercase, underscore-separated, and self-descriptive, so the pattern remains predictable.
With only 6 tools, the server is well-scoped for a document RAG system. Each tool addresses a distinct step in the workflow—discovery, ingestion (single and bulk), retrieval, reading, and status monitoring—without redundancy or excess.
The surface covers the core lifecycle: listing available documents, ingesting new ones (single or all), searching indexed content, reading specific sections, and checking index status. Missing delete/update operations are minor gaps since the server focuses on additive indexing and retrieval, which is acceptable for its purpose.