mcp-docqa-server

mcp-docqa-server

An MCP server that provides semantic search over a document corpus, enabling AI clients to retrieve and cite relevant chunks from indexed documents via RAG pipelines.

Category
Visit Server

README

mcp-docqa-server

CI Python 3.10+ License: MIT

An MCP server that gives any AI client semantic search over a document corpus — the retrieval half of a RAG pipeline, shipped as reusable infrastructure. Point Claude Desktop (or any MCP host) at it and the model can search, read, and cite your documents autonomously; generation stays in the client, retrieval lives here.

"What does the corpus say about the hour-1 sepsis bundle?"
        │
        ▼                      MCP (stdio / HTTP)
┌──────────────┐   search_documents("hour-1 sepsis bundle", k=5)   ┌───────────────┐
│  Claude /    │ ────────────────────────────────────────────────► │ docqa server  │
│  any MCP     │ ◄──────────────────────────────────────────────── │ embed → ANN   │
│  host        │     top-k chunks + titles, urls, scores           │ search → rank │
└──────────────┘                                                   └──────┬────────┘
                                                                          │
                                                          SQLite (embedded, exact)
                                                          or Postgres + pgvector (HNSW)

Why this exists

Most RAG demos hard-wire retrieval into one chatbot. Exposing retrieval through MCP inverts that: index once, query from anywhere — Claude Desktop, an IDE agent, a CI job, your own client. The server is deliberately boring infrastructure: typed tools, two interchangeable storage backends, pluggable embeddings, an eval harness, and loud failures where silent ones usually live (see Design decisions).

Features

  • Four typed MCP toolssearch_documents, fetch_document, corpus_stats, ping — with docstrings written for the calling model, because tool descriptions are the interface.
  • Two vector stores, one contract: embedded SQLite (zero infrastructure, exact brute-force cosine) and Postgres + pgvector (HNSW index, production posture). Both pass the same behavioral test battery.
  • Pluggable embeddings: OpenAI text-embedding-3-small, or a deterministic keyless hashing embedder so a fresh clone works with no API key, no database, no network.
  • Idempotent ingestion: per-document content hashes mean re-runs skip unchanged docs — nothing gets re-embedded (or re-billed) by accident.
  • Corpus fetcher for PubMed abstracts via the keyless NCBI E-utilities API (rate-limit aware).
  • Retrieval eval harness: recall@k and MRR against a labeled testset, usable as a CI quality gate (docqa-eval --min-recall5 0.9).
  • stdio + Streamable HTTP transports, Dockerfile included.
  • CI that means it: lint, unit tests, a real MCP client round-trip over stdio, and an integration job against a live pgvector service container that ends by gating on retrieval recall.

Quickstart — 60 seconds, no API key

git clone https://github.com/saivarun161/mcp-docqa-server.git
cd mcp-docqa-server
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

docqa-ingest index --sample   # bundle of 12 healthcare docs -> SQLite index
docqa-eval                    # recall@1/3/5 + MRR against the bundled testset

You now have a working index at data/index.db. Talk to it over real MCP with the Inspector:

npx @modelcontextprotocol/inspector .venv/bin/docqa-server

Wire it into Claude Desktop

  1. Open ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).
  2. Add the block from claude_desktop_config.example.json with absolute paths — hosts launch servers from their own working directory, so relative paths break.
  3. Fully restart Claude Desktop and ask: "Search the docqa corpus: what counts as stage 2 hypertension?"

The model will call search_documents, read the chunks, and answer with sources.

A real corpus

Index a few hundred PubMed abstracts on any topic (keyless, public data):

docqa-ingest fetch --query "semaglutide cardiovascular outcomes" --max-docs 200
docqa-ingest index --corpus data/corpus.jsonl
docqa-ingest stats

Any JSONL with id, title, url, text fields works — swap PubMed for arXiv, EDGAR filings, or your own notes.

Production posture

Semantic embeddings — put an OpenAI key in .env (see .env.example) and re-index; DOCQA_EMBEDDINGS=auto picks it up:

pip install -e ".[openai]"
docqa-ingest index --corpus data/corpus.jsonl --force

Postgres + pgvector — vectors move into an HNSW-indexed table; search runs inside the database:

docker compose up -d        # pgvector/pgvector:pg16 with the extension enabled
export DOCQA_STORE=pgvector DATABASE_URL=postgresql://docqa:docqa@localhost:5432/docqa
pip install -e ".[pg]"
docqa-ingest index --sample

HTTP transport — for network-reachable deployments instead of stdio:

docqa-server --transport http --host 0.0.0.0 --port 8000
# or containerized:
docker build -t mcp-docqa-server . && docker run -p 8000:8000 mcp-docqa-server

MCP tools

Tool Arguments Returns
search_documents query, k=5 top-k chunks with doc_id, title, url, text, score
fetch_document doc_id the full source document
corpus_stats doc/chunk counts, backend, embedder that built the index
ping "pong" (connectivity check)

Retrieval quality

docqa-eval retrieves for every testset question and reports where the expected document ranked:

Retrieval eval — 12 questions, k=5
store=sqlite  embedder=hash-v1-512

  [rank 1] What blood pressure reading counts as stage 2 hypertension?  (expects sample-001)
  ...
recall@1=1.00  recall@3=1.00  recall@5=1.00  MRR=1.00

The bundled corpus is small and topically distinct, so even the lexical fallback embedder scores perfectly — that run proves the plumbing. The interesting experiments start when you index a few hundred PubMed abstracts and compare hash vs openai embeddings on your own testset; CI runs the eval against a live pgvector container and fails the build if recall@5 drops below 0.9.

Design decisions

  • Embedder identity is persisted and enforced. Vectors from different embedders live in unrelated spaces; querying an OpenAI-built index with hash vectors doesn't error mathematically — it just returns garbage. The store records which embedder built it and the retriever refuses a mismatch with an actionable message. Silent failure → loud failure.
  • Brute force is a feature at SQLite scale. Exact cosine over a few thousand chunks is milliseconds with NumPy and has zero recall loss; ANN indexes buy speed at scale, not correctness. The pgvector backend adds HNSW when the corpus outgrows brute force.
  • Chunks carry their title. Each chunk is prefixed with its document title before embedding, so a chunk ripped out of context still knows what it's about.
  • One behavioral battery, two backends. The SQLite and pgvector stores pass the identical test suite (tests/store_suite.py), which is what "interchangeable" actually means.
  • The MCP layer is tested with a real MCP client. CI spawns the server over stdio and drives it with an mcp.ClientSession — the same handshake Claude Desktop performs — not by calling Python functions directly.
  • Public data only. PubMed abstracts and original sample docs. Never index proprietary or employer documents into a demo corpus.

Project structure

src/docqa/
├── server.py            # FastMCP server + tool definitions
├── retriever.py         # embed query -> store search, with embedder guard
├── embeddings.py        # OpenAIEmbedder | HashingEmbedder (keyless fallback)
├── chunking.py          # word windows with overlap
├── config.py            # env-driven settings (.env aware)
├── store/
│   ├── base.py          # VectorStore contract + embedder guard
│   ├── sqlite_store.py  # embedded, exact brute-force cosine
│   └── pgvector_store.py# Postgres + pgvector, HNSW
├── ingest/
│   ├── pubmed.py        # NCBI E-utilities fetcher (keyless, rate-limited)
│   ├── pipeline.py      # chunk -> embed -> upsert, content-hash idempotent
│   └── cli.py           # docqa-ingest fetch | index | stats
├── eval/run_eval.py     # recall@k + MRR, CI-gateable
└── data/                # bundled 12-doc sample corpus + labeled testset
tests/                   # unit + store battery + MCP stdio round-trip
.github/workflows/ci.yml # lint, tests, live pgvector integration + recall gate

Roadmap

  • [x] MCP tools over stdio + Streamable HTTP
  • [x] SQLite and pgvector backends behind one contract
  • [x] Idempotent ingestion + PubMed fetcher
  • [x] Eval harness with CI recall gate
  • [ ] Hybrid retrieval (BM25 + vector, reciprocal rank fusion)
  • [ ] Cross-encoder reranking stage
  • [ ] More corpus adapters (arXiv, EDGAR)
  • [ ] Bearer-token auth for the HTTP transport

License

MIT — see LICENSE.

Built by Varun Kammadanam — backend + GenAI engineer (Java, Python, AWS, RAG systems).

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured