auth-floor-rag-demo
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
auth-floor-rag-demo
An MCP server with three things production agent systems actually need and demo projects usually skip:
An auth floor — the server refuses to start without a token, every request is authenticated, and write tools check the caller's tier per call.
A hybrid RAG pipeline — TF-IDF cosine retrieval with a pool-local BM25 re-rank fused by Reciprocal Rank Fusion, over a small bundled corpus.
The eval harness that gates it — a frozen query set, written before any tuning, scoring both rankers; the eval exits non-zero below its MRR gate, so it can sit in CI.
This is a distilled, sanitized miniature of a real production system (an MCP control plane fronting a large indexed knowledge store). The architecture, the auth pattern, and the failure lessons are the production ones; the scale is deliberately small enough to run anywhere.
Run it (under 5 minutes)
Requires Python 3.11+.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DEMO_MCP_TOKENS='dev-read:read:reader, dev-admin:admin:me'
python -m app.server
# listening on http://127.0.0.1:8765/mcp (8 documents, auth floor ON)Try the floor first:
curl http://127.0.0.1:8765/health # public: liveness + data_mode label
curl -X POST http://127.0.0.1:8765/mcp # 401 — no token, no serviceAnd the refusal that defines the floor — start it without tokens:
unset DEMO_MCP_TOKENS
python -m app.server
# FATAL: no MCP token configured ... Refusing to start (exit 2)Connect any MCP client over streamable HTTP with an Authorization: Bearer dev-read header, e.g. a minimal Python client:
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(
"http://127.0.0.1:8765/mcp",
headers={"Authorization": "Bearer dev-read"},
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
print(await session.call_tool("search_documents",
{"query": "how does bread rise without commercial yeast"}))Related MCP server: MCP Knowledge Service
Architecture
Authorization: Bearer <token:tier>
│
MCP client ──HTTP──▶ BearerAuthMiddleware ──401──▶ ✕
│ sets principal (contextvar)
▼
FastMCP (streamable HTTP)
┌───────────────────────────────┐
│ read tools write tools │── require admin tier,
│ (any token) (per-call ✓) │ checked per call
└──────────────┬────────────────┘
▼
Index: chunk → TF-IDF cosine pool
→ pool-local BM25 re-rank → RRF fusion
▲
eval/queries.json (frozen) ── evaluate() scores
both rankers; run_eval tool + CLI expose the gateTools (9): whoami, search_documents (ranker selectable:
hybrid/tfidf/bm25), get_document, list_documents,
summarize_document (extractive), index_stats, run_eval — plus the
admin-tier writes add_document and remove_document.
Auth model. Tokens are configured as token:tier[:label] pairs in
DEMO_MCP_TOKENS (or a single admin token in DEMO_MCP_TOKEN). Tiers are
read < admin. Authentication happens in ASGI middleware on every
request — the MCP transport never sees an unauthenticated call — and the
resulting principal rides a contextvar so write tools can enforce tier per
call, not once at login. Token comparison is constant-time. /health is
public but serves no data, and its payload carries data_mode: "sample-corpus" — see the second lesson below for why that field exists.
Retrieval. Documents are chunked paragraph-aware (~700 chars), indexed as TF-IDF vectors, and queried in two stages: cosine similarity builds a candidate pool, BM25 computed over that pool only re-ranks it, and RRF fuses the two orderings. TF-IDF is the key-free, deterministic stand-in at the embedding seam — the interface is where a neural embedder drops in.
The eval, honestly
python eval/run_eval.py scores a frozen set of 16 queries (12 keyword,
4 plain-language paraphrases) against the 8-document demo corpus:
ranker | hit@1 | hit@3 | MRR@5 |
tfidf | 1.000 | 1.000 | 1.000 |
hybrid | 1.000 | 1.000 | 1.000 |
Perfect scores — because eight documents on eight disjoint topics is an
easy corpus, and pretending otherwise would be exactly the kind of claim
this repo exists to avoid. The demo eval proves the harness runs and the
gate bites (lower the threshold in app/evaluate.py and watch it fail);
it is not evidence about retrieval quality at scale.
For that, here are the production system's numbers, measured on a frozen 16-query headline set against a ~48,500-chunk snapshot of a 4,406-document corpus, before and after the fix this demo's hybrid design distills (pure embedding cosine → + pool-local BM25 re-rank with full-pool RRF):
production ranker | hit@5 | MRR@5 |
v1: embedding cosine only | 0.6875 | 0.3583 |
v2.2: + pool-local BM25/RRF | 0.6875 | 0.4135 |
Two honest caveats the production report itself records: the anchored metrics understate quality (gold chunks were capped at 8 per query while 9–97 were eligible), and on a 3-query broad-control set the anchored numbers for v2.2 were worse even though every control's top-5 stayed inside the expected topic (topic-level hit@5 was 16/16 headline, 3/3 control). The fix was adopted for the headline gain with the tradeoff stated, not hidden.
What broke (the chapter demos usually omit)
1. The pipe, not the destination. A remote host appeared unreachable for hours: the database of "the host is down" evidence kept growing while the host was fine. The actual fault was a local proxy bridge that read its input through a buffered stream — small client writes were swallowed before ever reaching the network, so connections half-opened and died. Fix: pump bytes unbuffered, and add a transport self-test that runs before any "host down" conclusion is allowed. Lesson: verify the layer you're standing on before diagnosing the layer you're looking at.
2. Fixture data wearing live data's clothes. A dashboard rendered
sample fixtures through the same code path as live data, visually
indistinguishable from the real thing — "it loads" was being read as "it
works." Fix: the payload itself now declares its provenance (data_mode
in every health/status response — this demo's /health does the same),
so any consumer can tell fixture from live in-band, without trusting
convention or memory. Lesson: test doubles must be labeled in the data,
not in the documentation.
3. Green by inspection isn't green. A large integration branch looked mergeable on review. Running the full suite in a clean environment found exactly one genuine defect — a missing dependency — hiding among failures that turned out to be environment artifacts (a VM clock-resolution quirk making two timing assertions land on exactly 0.0 ms; a test-runner quirk in one sandbox). Both kinds of red look identical in a summary line. The rule that came out of it: no merge without a recorded green run from a clean checkout, and an "environment artifact" must be proven — reproduced and root-caused — never assumed. This repo follows the rule: the test suite spins up the real server process and exercises the floor over HTTP, because the floor only means something at the transport.
Tests
pip install -r requirements-dev.txt
python -m pytest # 19 tests: auth units, retrieval units,
# and end-to-end HTTP tests against the real
# server process (401s, tier denial, no-start)
python eval/run_eval.py # the frozen eval + gateLayout
app/auth.py token store, tiers, ASGI bearer middleware, no-start floor
app/rag.py chunking, TF-IDF + pool-local BM25, RRF hybrid search
app/evaluate.py frozen-eval scoring (hit@1/@3, MRR@5) + the gate
app/server.py FastMCP wiring: 9 tools, /health, startup
corpus/ 8 sample documents (original text, written for this demo)
eval/queries.json the frozen query set · eval/run_eval.py the CLI gate
tests/ unit + live-transport end-to-end testsLicense
MIT. The corpus documents are original texts written for this demo.
Related MCP Connectors
Team docs served to AI agents over MCP - search, Markdown reads, version pinning, read audit.
Agentic search over your Dewey document collections from any MCP-compatible client.
Agent-driven search: build, import, tune, search, and score result quality — all over MCP.
Political Comms documentation MCP server: search docs, query the docs filesystem. No auth.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables MCP clients to list indexed PDF document collections and perform semantic search queries on them using locally extracted text and embeddings.2AGPL 3.0
- AlicenseNot gradedqualityDmaintenanceProvides hybrid retrieval (dense + BM25 + RRF) with collection-based isolation and document ingestion for private knowledge access via MCP.MIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to search technical document collections using hybrid BM25 and embedding retrieval, returning cited page-level passages to support grounded, evidence-based answers.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients to connect document sources, prepare searchable documents, and run RAG-ready lexical, vector, or hybrid search with account-scoped document access.35 PyPIBSD 3-Clause