ragmcp

ragmcp

Retrieval augmented generation server for policy documents, exposing search, grounded Q&A with verified citations, and source management as MCP tools. Runs with local Ollama models for free or managed AWS backends, with an evaluation harness for retrieval and grounding metrics.

Category
Visit Server

README

ragmcp

A retrieval augmented generation service for policy and procedure documents, exposed to any MCP host as a set of tools, and measured by an evaluation harness rather than by spot checks.

Built to run three ways from the same code path:

  • Local, recommended: Ollama for embeddings and generation against an in memory hybrid index. Real learned embeddings, free, private, no account.
  • Managed: Amazon Bedrock for embeddings and generation, Amazon OpenSearch Service for kNN and BM25 hybrid retrieval.
  • Hermetic: a deterministic hashed embedder and an extractive answerer, so the test suite and a fresh clone run with nothing installed and nothing running.

Hermetic is the default. Every other backend is opt in through an environment variable, never autodetected from the presence of AWS credentials or a running Ollama server.

What it costs to run

Component Service Cost
Embeddings Ollama, nomic-embed-text Free, runs locally
Generation Ollama, llama3.1:8b Free, runs locally
Retrieval index In memory hybrid index Free
Retrieval index, managed Amazon OpenSearch Service, one t3.small.search node, 10 GB EBS Free tier, 750 hours per month for 12 months
Document storage Amazon S3 Free tier, 5 GB for 12 months
API surface AWS Lambda Free tier, 1M requests per month, always free
Embeddings, managed Amazon Bedrock, Titan Text Embeddings V2 Metered. No free tier
Generation, managed Amazon Bedrock, Converse API Metered. No free tier

The whole project runs at zero cost on Ollama. Bedrock has no free tier at any usage level, which is why it is opt in and why the default path never calls it.

Two traps worth knowing on the AWS side: OpenSearch Serverless is not free tier eligible, because it bills a minimum OCU allocation whether or not anything is indexed, so use a managed domain on t3.small.search. And the free OpenSearch tier is 12 months at 750 hours per month, not always free.

Inference stays local rather than going to a hosted free tier for a second reason beyond cost: most free inference tiers reserve the right to train on submitted prompts. That is fine for the synthetic corpus in data/, and it is exactly the question a security review asks about a real document corpus.

Why it is built this way

The interesting problems in a RAG deployment are not the embedding call. They are: what happens when the corpus does not contain the answer, what happens when the model cites a passage that was never retrieved, and how anyone knows whether a retrieval change made things better or worse. Each of those is a component here with a test behind it.

Architecture

document ──> chunking ──> embeddings ──> index ──> retrieval ──> gate ──> generation ──> citation
             heading      Ollama,        Amazon    BM25 +        abstain  Ollama,         verification
             scoped,      Bedrock or     OpenSearch vectors,     when the Bedrock or      drop unmapped
             overlapping  hashing        or local  fused         corpus   extractive      markers
                                                                 is thin  fallback
Module Responsibility
ragmcp/chunking.py Heading scoped, token budgeted chunks with sentence level overlap
ragmcp/embeddings.py Ollama and Bedrock Titan embeddings, plus a deterministic hashed backend
ragmcp/index.py BM25 and dense vector scoring fused with Reciprocal Rank Fusion
ragmcp/opensearch.py Amazon OpenSearch backend, HNSW cosine kNN field and a normalising fusion pipeline
ragmcp/llm.py Ollama and Bedrock generation, plus an extractive fallback for timeouts
ragmcp/ollama_client.py Dependency free HTTP client with actionable errors for a missing model or a stopped server
ragmcp/pipeline.py Ingest, retrieve, abstention gate, citation verification
ragmcp/server.py MCP server over stdio, JSON-RPC 2.0, no SDK dependency
eval/ Labelled question set, retrieval metrics, groundedness metrics

Fusion happens in a different place depending on the backend, which is worth knowing before reading the code. Locally, the two ranked lists are fused client side with Reciprocal Rank Fusion. On OpenSearch, both queries go up as one hybrid query and a search pipeline normalises the two score distributions before combining them, which is necessary because BM25 scores are unbounded while cosine scores sit in [0, 1].

MCP tools

Tool Purpose
search_documents Retrieve passages by hybrid, keyword or vector search
answer_question Grounded answer with verified citations, or an explicit refusal
list_sources What is indexed and how many chunks each document produced
ingest_document Chunk, embed and index a document at runtime

Every call is validated against its input schema before it reaches a handler. Bad input comes back as an MCP tool error with isError: true; a handler exception is caught, logged and returned the same way, so a malformed call from a host never takes the server down.

Register it with any MCP host:

{
  "mcpServers": {
    "ragmcp": {
      "command": "python",
      "args": ["-m", "ragmcp.server", "data"],
      "cwd": "/path/to/ragmcp"
    }
  }
}

Quick start

Hermetic, needs nothing installed:

python -m unittest discover -s tests    # 33 tests, no network, no credentials
python eval/run_eval.py --k 3           # evaluation report as JSON
python -m ragmcp.server data            # MCP server on stdio

With real local models, which is the interesting configuration:

ollama pull nomic-embed-text
ollama pull llama3.1:8b

export RAGMCP_EMBEDDINGS=ollama
export RAGMCP_GENERATOR=ollama
python eval/run_eval.py --k 3 --json eval/results.json

To use the managed backends, install requirements-aws.txt, set the variables in .env.example, and call OpenSearchIndex.create() once to provision the index mapping and the fusion pipeline.

Evaluation

48 questions over an 8 document, 43 chunk corpus. 40 are answerable and 8 are deliberately out of scope. Retrieval is judged at chunk level, not document level: with a corpus this small, document level judgements score every retriever at recall 1.0 and hide every regression worth catching. Gold chunks are weak labelled, meaning a chunk counts as relevant when it sits in a labelled document and contains the answer bearing string.

Retrieval, at k=3, on the hermetic backend (hashed embedder). These are not the numbers you get with Ollama embeddings, see the note below the table:

Mode recall@3 MRR nDCG@3 p50 latency
keyword 0.833 0.838 0.813 0.02 ms
vector 0.783 0.746 0.732 1.40 ms
hybrid 0.808 0.796 0.775 1.50 ms

End to end answers:

Metric Value
Citation support rate, in scope 0.875
Grounded rate, in scope 0.900
False abstention rate, in scope 0.100
Abstention rate, out of scope 1.000

What the numbers actually say

The published table is the hashed embedder, and it is the weakest configuration on purpose. It is the one that runs anywhere with nothing installed, so it is the one that can be reproduced from a clean clone. Rerun with RAGMCP_EMBEDDINGS=ollama to measure the configuration that is actually recommended, and replace the table above with what you get.

Hybrid does not beat keyword search in that table, and that is expected. RRF fuses two ranked lists and lands between them when one retriever is much weaker. The hashed embedder has no learned semantics, so it contributes little and drags the fused ranking down. Weighted RRF and a truncated SVD embedder were both tried; neither moved MRR above keyword alone. The fusion is kept because it is the piece that pays off once real embeddings sit behind it, and the hypothesis is that nomic-embed-text reverses this result. That hypothesis is untested here and is written down rather than assumed, because the whole point of the harness is that claims like it get measured before they get repeated.

The abstention gate was rebuilt after the first version failed. The first gate measured how many query terms appeared in the retrieved passages. It scored some out of scope questions above some in scope paraphrases, because words like "customers" and "review" appear throughout a policy corpus, and it falsely refused 35 percent of answerable questions. The BM25 top score separates the two populations far better through term saturation and length normalisation: 100 percent abstention on out of scope questions at 10 percent false abstention.

The remaining failures are vocabulary mismatch. The questions still missed are paraphrases that share almost no terms with the source text, such as asking what stops someone redirecting funds by fake email when the policy says callback verification against the vendor master record. This is precisely the gap a real embedding model closes, which is the argument for keeping the vector half of the index.

Known limitations

  • The BM25 gate threshold scales with corpus size and needs retuning per index. It is a constructor argument, not a constant, and the tests pin their own.
  • Weak labelling ties gold chunks to a literal string. It is cheap and it survives a chunking change, but it cannot label a question whose answer is spread across two chunks.
  • The extractive fallback returns leading sentences from the top passages. It keeps the tool honest and cited when the model is unreachable, but it is a degradation path, not an answer quality strategy.
  • The Bedrock and OpenSearch backends are exercised by their interface contract, not against live services, so those code paths carry no integration test here.
  • The published metrics come from the hashed embedder. Ollama or Bedrock embeddings change the vector and hybrid rows, so the table must be regenerated before those numbers are quoted for any other configuration.
  • Ollama generation quality is not measured here. The citation support and groundedness numbers come from the extractive answerer, which cannot hallucinate a citation by construction, so they set a floor rather than describing what a generative model does on this corpus.

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