NOEMA-AI MCP Server
Exposes document ingestion, retrieval (vector, vectorless, hybrid), and multi-turn chat tools for a LangGraph-powered RAG pipeline with streaming answers.
README
NOEMA-AI
An end-to-end Retrieval-Augmented Generation pipeline that ingests documents and answers queries grounded in their content. It supports three retrieval paths side by side — vector-based (embeddings + vector store), vector-less (keyword/BM25 structured retrieval), and hybrid (Reciprocal Rank Fusion of both) — orchestrated with LangChain and LangGraph, traced with LangSmith, and exposed to external clients via MCP (Model Context Protocol). It also supports multi-turn chat with session memory and streaming answers over Server-Sent Events.
Document ─▶ Ingestion ─▶ Chunking ─▶ ┬─▶ Embeddings ─▶ Vector Store ─┐
└─▶ BM25 Index (vector-less) ─┤─▶ LangGraph ─▶ LLM ─▶ Answer
│ (retrieve node)
Query ───────────────────────────────────────────────────────────────────┘
Screenshots
<!-- Drop your screenshot images into docs/screenshots/ using these exact filenames and they'll render automatically on GitHub — no other changes needed. Recommended: 1280px+ wide PNGs, dark mode (matches the UI theme). -->
Home page — landing view of the NOEMA UI

Console — ask a question, watch retrieval + generation trace live

Hybrid retrieval mode — fused vector + BM25 results with match source

Chat — multi-turn conversation with session memory

API reference — full endpoint interface view

Streaming answer — tokens arriving live over SSE

Features
- Vector RAG — chunking → embeddings → persistent vector store (Chroma) → similarity search. Ships with a zero-dependency offline hash embedding by default, and a real local semantic embedding option (
EMBEDDING_PROVIDER=sentence-transformers) for actual semantic search - Vector-less RAG — BM25 keyword ranking over raw chunks, no embedding model required
- Hybrid RAG — fuses the vector and BM25 result lists with Reciprocal Rank Fusion (RRF), so a chunk that ranks well in either retriever surfaces, and one that ranks well in both surfaces first
- Multi-turn chat with memory —
POST /chatkeeps a rolling window of prior turns per session, so follow-up questions ("what about the second one?") resolve against earlier turns instead of being answered in a vacuum - Streaming answers —
GET /query/stream(Server-Sent Events) streams the answer token-by-token instead of waiting for the full response - Document management —
GET /documentslists every indexed source;DELETE /documents/{source}removes it from every retrieval path at once - Query result caching — a short-lived in-memory TTL cache serves repeated identical questions without re-running retrieval + the LLM; automatically invalidated on ingest/delete
- Persistence across restarts — the BM25 corpus and the ingested-document manifest are saved to
PERSIST_DIRand reloaded on startup, so a restart no longer silently wipes what was ingested - LangGraph pipeline — ingestion, retrieval (vector / vectorless / hybrid), and generation modeled as composable graph nodes, with automatic fallback from vector to vector-less
- LangSmith tracing — every LLM call and retrieval step is traceable/evaluatable when configured
- LLM Gateway / Middleware — a single layer that routes requests, logs, and rate-limits before calls reach the LLM
- Guardrails — input/output checks with normalized (whitespace/zero-width-character-resistant) prompt-injection pattern matching; still pattern-based, not a full classifier — see the note below
- API key auth — optional
X-API-Keyheader enforcement on/ingest,/query/*,/documents/*, and/chat/*, off by default so the app still runs with zero setup - MCP server — exposes
ingest_document,delete_document,list_documents,query_vector_rag,query_vectorless_rag,query_hybrid_rag, andchat_with_memoryas MCP tools for external LLM clients - Docker support — a
Dockerfileanddocker-compose.ymlfor one-command containerized deployment with a persistent volume - 52 unit/API tests covering the chunker, guardrails (including evasion attempts), the BM25 store, hybrid RRF fusion, the query cache, chat memory, and the FastAPI endpoints (including auth enforcement, document deletion, and chat sessions)
Project layout
noema/
├── app/
│ ├── config.py # environment / settings
│ ├── main.py # FastAPI entrypoint (REST API)
│ ├── middleware.py # LLM gateway / logging / rate-limit middleware
│ ├── guardrails.py # input & output guardrails
│ ├── cache.py # TTL query-result cache
│ ├── chat.py # session-based conversational memory
│ ├── ingestion/
│ │ ├── loader.py # load .txt / .pdf / .docx into raw text
│ │ └── chunker.py # sliding-window chunking with overlap
│ ├── retrieval/
│ │ ├── embeddings.py # embedding provider (API or local fallback)
│ │ ├── vector_store.py # Chroma-backed Vector RAG
│ │ ├── vectorless_store.py # BM25-backed Vector-less RAG (+ persistence)
│ │ └── hybrid.py # Reciprocal Rank Fusion of vector + BM25
│ ├── llm/
│ │ └── client.py # LLM client (Groq / LLaMA 3.3 70B, mockable, streaming + history-aware)
│ ├── graph/
│ │ └── pipeline.py # LangGraph state machine wiring it all together
│ ├── mcp/
│ │ └── server.py # MCP server exposing the pipeline as tools
│ └── static/ # web UI (served at "/" by app/main.py)
│ ├── index.html
│ └── assets/
│ ├── style.css
│ └── app.js
├── docs/screenshots/ # drop screenshot images here (see Screenshots above)
├── data/sample_docs/sample.txt # example document to ingest
├── scripts/ingest.py # CLI: ingest a document from the command line
├── tests/ # 52 tests: chunker, guardrails, stores, hybrid, cache, chat, API
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env.example
Setup
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then fill in your keys
Environment variables (.env)
| Variable | Purpose |
|---|---|
GROQ_API_KEY |
Key for Groq-hosted LLaMA 3.3 70B calls (leave blank to use the built-in mock LLM for local testing) |
LANGCHAIN_TRACING_V2 |
true to enable LangSmith tracing |
LANGCHAIN_API_KEY |
LangSmith API key |
LANGCHAIN_PROJECT |
LangSmith project name, e.g. noema |
VECTOR_DB_PATH |
Local path for the persistent Chroma store, default ./chroma_db |
EMBEDDING_PROVIDER |
hash (default, deterministic offline bag-of-words embedding, zero deps) · sentence-transformers (real local semantic embeddings — pip install sentence-transformers, downloads a small model on first use) · remote (placeholder for a hosted embedding API you wire in) |
EMBEDDING_MODEL_NAME |
Model name used when EMBEDDING_PROVIDER=sentence-transformers, default all-MiniLM-L6-v2 |
API_KEY |
Leave blank to run the API in open dev mode. Set a value to require a matching X-API-Key header on /ingest and /query/* |
PERSIST_DIR |
Where the BM25 corpus and ingested-document manifest are saved, default ./storage. Restoring on startup means a restart no longer wipes what was ingested |
QUERY_CACHE_TTL_SECONDS / QUERY_CACHE_MAX_ENTRIES |
How long identical (mode, query, top_k) requests are served from cache, and how many entries the cache holds before evicting |
CHAT_HISTORY_MAX_TURNS / CHAT_SESSION_TTL_SECONDS |
How many turns of conversation /chat keeps per session, and how long an idle session is kept before being swept |
HYBRID_RRF_K |
Reciprocal Rank Fusion constant for /query/hybrid; higher values reduce how much rank position 1 vs 2 matters |
Running it
1. Ingest a document
python scripts/ingest.py data/sample_docs/sample.txt
2. Start the API (and the UI)
uvicorn app.main:app --reload
Open http://127.0.0.1:8000 — this now serves the NOEMA web UI directly
from the FastAPI app (app/static/). Drop a file on the ingest panel, pick a
retrieval mode, and ask a question; the pipeline trace strip lights up as the
request moves through guardrails → retrieval → generation, and results show
which lane (vector / vector-less) answered along with the retrieved chunks
and their scores.
Endpoints:
GET /health— liveness + uptimeGET /stats— indexed document list, BM25 chunk count, vector store availability, active chat sessions, query cache statsGET /documents— list every indexed source with its chunk countDELETE /documents/{source}— remove a document from every retrieval pathPOST /ingest— upload/ingest a documentPOST /query/vector— answer a query using Vector RAGPOST /query/vectorless— answer a query using Vector-less (BM25) RAGPOST /query/hybrid— answer a query using Reciprocal Rank Fusion of bothPOST /query/graph— run the full LangGraph pipeline (vector / vectorless / hybrid, with fallback)GET /query/stream— Server-Sent Events: streams the answer token-by-tokenPOST /chat— multi-turn conversational endpoint with session memoryGET /chat/{session_id}/history— read back a session's turnsDELETE /chat/{session_id}— clear a session
The UI is plain HTML/CSS/JS (no build step) so it runs from the same
uvicorn process with no extra tooling. If you'd rather host it separately,
set window.__DOCUMIND_API_BASE__ in app/static/index.html before
app.js loads, and point it at wherever the API is running — CORS is
already open on the API side.
3. Run the MCP server (so an MCP-compatible client can call this pipeline as tools)
python -m app.mcp.server
4. Or run it in Docker
docker compose up --build
This builds the image, mounts a named volume at /data for the BM25
corpus/Chroma store to persist across container restarts, and serves the
API + UI on http://localhost:8000. Configuration still comes from .env
(referenced via env_file in docker-compose.yml).
Notes on the current state
This is a working scaffold, not a black-box demo: the chunker, guardrails,
BM25 retrieval, hybrid fusion, query cache, chat memory, and the FastAPI
endpoints (52 tests total) run fully offline and are unit-tested. The
vector store, LLM client, and MCP server are wired up with real
integrations but need a GROQ_API_KEY (and, optionally,
chromadb/LangSmith credentials) to hit real APIs. Without keys, the LLM
client falls back to a mock responder so the pipeline still runs end-to-end
for development and demos.
Known, honestly-stated limitations:
- Embeddings: the default
EMBEDDING_PROVIDER=hashis a bag-of-words hash, not real semantic search — good for wiring/testing the pipeline offline, not for retrieval quality. SetEMBEDDING_PROVIDER=sentence-transformersfor real semantic embeddings (needs an install + a one-time model download). - Guardrails are regex/pattern-based prompt-injection checks, not a trained classifier — they catch common phrasings and simple spacing/unicode evasions, not a determined adversary. Swap in a managed guardrails service for anything beyond a demo.
- Persistence is file-based, not a real database: the BM25 corpus and
ingested-document manifest are now saved to
PERSIST_DIRand reloaded on startup (previously they were in-memory only and a restart silently wiped them), but this is still a JSON file on local disk, not a proper store — it won't behave correctly behind multiple worker processes or hosts. Chat sessions and the rate limiter are still in-memory-only by design (short-lived, session-scoped state that doesn't need to survive a restart). - API auth is a single shared
X-API-Key, not per-user auth — fine for a demo or an internal tool, not a substitute for real auth in a multi-tenant deployment. - The query cache is a simple in-memory TTL dict, not a distributed cache — fine for a single-process deployment, and it's invalidated on every ingest/delete so it can't serve a stale answer past a document change, but it won't be shared across multiple worker processes.
A dependency-free web UI sits in front of all of this (app/static/, served
at /), and several functional bugs were fixed while wiring it up so the
demo actually works out of the box: python-multipart was missing from
requirements.txt (FastAPI needs it for file uploads — /ingest would 500
on boot without it), the BM25 vector-less path was silently dropping
every hit on small corpora (with only one or two chunks indexed, BM25's IDF
term can go negative for common words, so the old score <= 0 filter
discarded true matches — it now ranks by score but only discards chunks that
share no terms with the query), and the frontend was reading
stats.vectorless_chunk_count / stats.ingested_documents while /stats
returned differently-named fields, so the document list and chunk count in
the UI always silently showed zero regardless of what had actually been
ingested.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。