rag-mcp-azure
O.M. Health AI β Medical RAG Assistant
A production-ready AI-powered medical assistant built on a full-stack RAG (Retrieval-Augmented Generation) architecture. The backend runs on Azure Container Apps, the frontend is deployed on Vercel. Designed for reliability, security, and a polished end-user experience.
π Live Demo (Frontend): https://om-health-ai.vercel.app
βοΈ Backend API (Azure): https://rag-mcp-azure.redsand-f0795bb6.francecentral.azurecontainerapps.io
β οΈ Notice: The backend API is currently deployed using an Azure Free Tier allocation. If the live demo is unresponsive due to expired cloud credits, please check out the Demo Video or follow the Quick Start instructions below to run the architecture locally in seconds using Docker.
π― Project Overview
What it does:
Answers medical questions in natural language using the MedQuAD dataset (16,000+ validated Q&A pairs from the NIH)
Retrieves the most relevant medical context from a FAISS vector index using MMR search (Maximal Marginal Relevance)
Synthesizes a clear, professional answer via Claude Haiku (Anthropic) through OpenRouter β entirely server-side
Exposes a clean chat UI with dark mode, suggestion chips, and Markdown rendering
Also exposes a MCP (Model Context Protocol) endpoint for agentic AI integrations
Architecture philosophy:
Security by design: the LLM API key never reaches the browser β all synthesis happens in the backend
Privacy by design: zero client-side storage of sensitive data, stateless request processing
Lightweight: CPU-only, no GPU, no heavy vector database β optimized for cost-effective cloud deployment
Production-ready: automated CI/CD, tested endpoints, verified MCP integration
π Tech Stack
Layer | Technology | Notes |
Backend framework | FastAPI + Uvicorn | Async HTTP, Pydantic validation |
RAG engine | LangChain + FAISS | MMR search, in-memory vector store |
Embeddings | HuggingFace | CPU-optimized, 33 MB |
LLM | Claude Haiku 4.5 via OpenRouter | Called server-side only |
HTTP client | httpx | Async OpenRouter calls from backend |
Knowledge base | MedQuAD dataset (NIH) | 16,000+ medical Q&A pairs, pre-built FAISS index |
Document storage | Azure Blob Storage | Source of truth for index files in production |
MCP server | MCP SDK v2, Streamable HTTP | Agentic tool interface for external AI agents |
Containerization | Docker |
|
Backend hosting | Azure Container Apps | Serverless, auto-scaling, managed ingress |
Container registry | Azure Container Registry (ACR) | Image storage |
Frontend hosting | Vercel | Static HTML/CSS/JS, global CDN |
CI/CD | GitHub Actions | Test β build β push β deploy, idempotent |
ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER BROWSER β
β frontend/ (HTML + CSS + JS) β
β Hosted on Vercel (static) β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β POST /chat { query }
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AZURE CONTAINER APPS (Backend) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β FastAPI app/main.py β β
β β β β
β β POST /chat βββΊ rag_engine.search() (MMR, k=3) β β
β β β β β
β β βΌ β β
β β FAISS index (in-memory) β β
β β MedQuAD Q&A embeddings β β
β β β β β
β β βΌ β β
β β httpx βββΊ OpenRouter API β β
β β Claude Haiku 4.5 β β
β β (OPENROUTER_API_KEY β secret Azure) β β
β β β β β
β β βΌ β β
β β { "answer": "..." } β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Also exposes: /query (raw context), /reindex, /upload, /mcp β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
Azure Blob Storage
(FAISS index files: index.faiss + index.pkl)Key security boundary: the OPENROUTER_API_KEY is stored as an Azure Container Apps secret and injected at runtime. It is never sent to the browser, never committed to source, and never logged.
ποΈ Project Structure
rag-mcp-azure/
βββ app/
β βββ data/ # PDF fallback for local dev
β βββ main.py # FastAPI app: /chat, /query, /reindex, /upload, MCP mount
β βββ rag_engine.py # RAG pipeline: MMR search, Blob/local ingestion, FAISS
βββ frontend/
β βββ index.html # Full-page UI: navbar, hero, chat card, features, FAQ
β βββ style.css # Design system: CSS variables, dark mode, responsive
β βββ app.js # Chat logic: POST /chat, Markdown rendering, dark mode toggle
βββ scripts/
β βββ build_index_offline.py # Build FAISS index from medquad.csv locally
β βββ upload_index_to_blob.py # Upload index files to Azure Blob Storage
β βββ deploy-aca.sh # Manual Azure deployment script
βββ tests/
β βββ test_api.py # REST endpoint tests (CI)
β βββ test_mcp_integration.py # MCP client tests (manual, live server)
βββ .github/workflows/deploy.yml # CI/CD pipeline
βββ medquad.csv # Source dataset (NIH MedQuAD)
βββ Dockerfile
βββ requirements.txt
βββ README.mdπ§ RAG Engine β Key Design Decisions
Dataset: MedQuAD
The knowledge base is built from the MedQuAD (Medical Question Answering Dataset) published by the NIH. It contains over 16,000 question-answer pairs covering diseases, symptoms, treatments, and diagnostics across dozens of medical specialties.
Unlike a pure document ingestion pipeline, the FAISS index here embeds both the question and the answer text, giving the retrieval step richer semantic context to match against user queries.
MMR Search (Maximal Marginal Relevance)
The search() method in rag_engine.py uses MMR instead of plain cosine similarity:
results = self.vector_store.max_marginal_relevance_search(query, k=3, fetch_k=20)fetch_k=20: retrieve the top 20 candidates by similarityk=3: from those 20, select the 3 most diverse results
This prevents the top-3 results from being near-duplicate chunks (a common failure mode when multiple similar Q&A pairs exist in the dataset), and ensures the LLM receives varied, complementary context.
Pre-built Index Loading
In production, the FAISS index is pre-built offline from medquad.csv and stored in Azure Blob Storage (INDEX_CONTAINER_NAME). On startup, the engine downloads index.faiss + index.pkl directly β no re-embedding at boot time, which keeps cold start under 30 seconds on 0.5 CPU.
π Security Architecture
API Key β Server-Side Only
The most important security change from the initial prototype: the OpenRouter API key never leaves the server.
Old architecture | Current architecture | |
Who calls OpenRouter? | Browser (JavaScript) | Backend (Python/httpx) |
Where is the key? |
| Azure Container Apps secret |
Key visible in DevTools? | β Yes | β No |
Key in source code? | Risk | Never β |
The key is injected at deploy time:
az containerapp secret set --name rag-mcp-azure --resource-group rg-rag-mcp-azure `
--secrets "openrouter-api-key=sk-or-v1-..."
az containerapp update --name rag-mcp-azure --resource-group rg-rag-mcp-azure `
--set-env-vars "OPENROUTER_API_KEY=secretref:openrouter-api-key"Full Security Checklist
β OPENROUTER_API_KEY β Azure Container Apps secret, never in source or env plaintext
β BLOB_CONNECTION_STRING β Azure Container Apps secret, re-applied on every CI deploy
β GitHub Secrets β Azure, ACR, and Blob credentials stored as Actions secrets
β Service Principal β deployment uses a scoped Azure AD SP, not the subscription owner
β Pydantic validation β all request bodies validated before processing
β MCP DNS rebinding protection β
TransportSecuritySettingsscoped to known hostsβ CORS β open for public demo (MedQuAD is public data); restrict for production use
β Key rotation practiced β Blob Storage key was rotated after accidental log exposure during early debugging
π₯οΈ Frontend β UI/UX
The frontend is a static single-page application (HTML + CSS + JS, no framework) deployed on Vercel.
Features
Full-page layout β navbar, hero section (2-column: pitch + live chat), features grid, security section, FAQ
Functional chat card β real-time POST to
/chat, typing indicator, timestamped messagesMarkdown rendering β bot responses rendered with headers, bold, lists, blockquotes (custom lightweight parser, no library)
Lucide Icons β SVG icon library replacing all native emojis for consistent cross-platform rendering
Dark mode β toggled via a settings dropdown in the navbar, persisted in
localStorage, applied viabody[data-theme="dark"]CSS variable overrides with smooth transitionSuggestion chips β pre-filled question shortcuts that disappear after first use
Privacy by design β zero
sessionStorage/localStorageusage for sensitive data; no API key ever stored client-side
Dark Mode Implementation
/* Light (default) */
:root {
--bg-color: #fdfdfd;
--text-main: #111827;
--white: #ffffff;
--bot-bg: #f3f4f6;
/* ... */
}
/* Dark */
body[data-theme="dark"] {
--bg-color: #111827;
--text-main: #f3f4f6;
--white: #1f2937;
--bot-bg: #374151;
/* ... */
}All colors in style.css use CSS variables β no hardcoded hex values for interface elements β ensuring the dark mode applies globally and consistently.
π‘ API Endpoints
POST /chat β Primary endpoint
Full RAG + LLM pipeline. Retrieves context from FAISS, calls Claude Haiku via OpenRouter server-side, returns a synthesized medical answer.
Request:
{ "query": "What are the symptoms of hypertension?" }Response:
{ "answer": "Hypertension is often called the 'silent killer' because..." }POST /query
Returns raw FAISS context chunks without LLM synthesis. Used internally and for debugging/testing.
Response:
{
"query": "...",
"context_extrait": "Extrait 1:\n...\n\nExtrait 2:\n..."
}GET /health
{ "status": "ok" }POST /reindex
Rebuilds the FAISS index from Blob Storage (or local files). Call after uploading new documents.
POST /upload
Uploads a PDF and adds it to the in-memory index (ephemeral β lost on restart or /reindex).
MCP /mcp-server/mcp
See MCP Protocol Integration below.
π MCP Protocol Integration
Model Context Protocol (MCP) is an open standard for connecting AI agents to external tools. O.M. Health AI exposes a search_documents tool over Streamable HTTP, allowing any MCP-compatible agent (Claude Desktop, custom agents) to query the medical knowledge base directly.
Endpoint
Environment | URL |
Local |
|
Production |
|
Exposed Tool
Tool | Input | Output |
|
| Top-3 MMR-ranked medical context chunks |
Python client example (verified end-to-end)
import asyncio
from mcp.client.streamable_http import streamable_http_client
from mcp import ClientSession
async def main():
url = "https://rag-mcp-azure.redsand-f0795bb6.francecentral.azurecontainerapps.io/mcp-server/mcp"
async with streamable_http_client(url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("search_documents", {"query": "symptoms of diabetes"})
for content in result.content:
if hasattr(content, "text"):
print(content.text)
asyncio.run(main())Claude Desktop config
{
"mcpServers": {
"om-health-ai": {
"url": "https://rag-mcp-azure.redsand-f0795bb6.francecentral.azurecontainerapps.io/mcp-server/mcp"
}
}
}DNS Rebinding Protection
The MCP SDK restricts the Host header to localhost by default. TransportSecuritySettings in app/main.py explicitly allow-lists the production Azure hostname β without this, requests return 421 Misdirected Request.
π Quick Start
Local Development
git clone https://github.com/oumniya03/rag-mcp-azure.git
cd rag-mcp-azure
python -m venv .venv
.venv\Scripts\Activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -r requirements.txtSet environment variables (create a .env or export directly):
# Required for LLM synthesis
export OPENROUTER_API_KEY=sk-or-v1-...
# Optional: load FAISS index from Blob Storage instead of local medquad_index/
export INDEX_CONTAINER_NAME=medquad-index
export BLOB_CONTAINER_URL=<your-connection-string>Run the backend:
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000Open frontend/index.html directly in a browser, or serve it locally:
cd frontend && python -m http.server 3000Test the chat endpoint:
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"query": "What are the symptoms of high blood pressure?"}'π³ Docker
# Build
docker build -t om-health-ai:latest .
# Run (pass the API key at runtime)
docker run --rm -p 8000:8000 \
-e OPENROUTER_API_KEY=sk-or-v1-... \
om-health-ai:latestImage profile: python:3.12-slim base, CPU-only torch wheel, ~500 MB compressed on ACR.
βοΈ Azure Deployment
Production Resources
Resource | Name | Notes |
Container App |
| Backend API |
Resource Group |
| France Central |
ACA Environment |
| Shared environment |
ACR |
| Docker image registry |
Blob Storage |
| FAISS index + source PDFs |
Compute | 0.5 CPU / 1.0 Gi | Sufficient for CPU-only inference |
Required GitHub Secrets
Secret | Description |
| Service principal JSON ( |
| ACR admin username |
| ACR admin password |
| Blob Storage connection string (re-applied as Container App secret on every deploy) |
| OpenRouter API key (applied as Container App secret) |
CI/CD Pipeline (.github/workflows/deploy.yml)
On every push to main:
Install dependencies
Run REST tests (
pytest tests/ -v -m "not integration")Login to Azure (service principal)
Build & push Docker image to ACR
Idempotent deploy:
az containerapp updateif exists,az containerapp createotherwiseRe-apply both secrets (
blob-connection-string,openrouter-api-key) on every deploy
git push origin main # triggers the full pipelineBlob Storage β FAISS Index
The pre-built FAISS index is stored in Blob Storage and loaded at startup (no re-embedding on cold start):
# Upload index files
az storage blob upload-batch `
--destination medquad-index `
--source medquad_index/ `
--account-name ragmcpstorage26 `
--auth-mode key
# Set the index container env var
az containerapp update --name rag-mcp-azure --resource-group rg-rag-mcp-azure `
--set-env-vars "INDEX_CONTAINER_NAME=medquad-index"π§ͺ Testing
Automated (CI)
pytest tests/ -v -m "not integration"Covers /health, /query, /reindex, /upload β runs on every push to main.
MCP Integration (manual, requires live server)
pytest tests/test_mcp_integration.py -v
# Against local instance:
$env:MCP_TEST_URL="http://localhost:8000/mcp-server/mcp"
pytest tests/test_mcp_integration.py -vQuick production smoke test
# Health
Invoke-RestMethod -Uri "https://rag-mcp-azure.redsand-f0795bb6.francecentral.azurecontainerapps.io/health"
# Chat
$body = @{query="What is hypertension?"} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri "https://rag-mcp-azure.redsand-f0795bb6.francecentral.azurecontainerapps.io/chat" `
-ContentType "application/json" -Body $bodyπ§ Configuration Reference
Variable | Required | Description |
| Yes (production) | LLM API key β Azure secret, never plaintext |
| No | Blob Storage connection string for PDF ingestion |
| No | Blob container name for pre-built FAISS index |
Local tweaks (app/rag_engine.py):
chunk_size/chunk_overlapβ text splitter parameters (default: 500 / 50)k/fetch_kβ MMR search parameters (default: k=3, fetch_k=20)Embedding model β
HuggingFaceEmbeddings(model_name=...)
βοΈ Troubleshooting
Symptom | Likely cause | Fix |
|
| Set the Azure Container App secret |
Empty context from | No index loaded | Check startup logs for |
MCP | Host not in allow-list | Add host to |
MCP | MCP not started via | Ensure |
Docker build fails | Dependency conflict | Check Python 3.12 compatibility in |
GitHub Actions fails | Missing secrets | Verify all 5 secrets are set in repository settings |
π Performance Profile
Metric | Value |
Deployment CPU | 0.5 vCPU |
Deployment Memory | 1.0 Gi |
Embedding model |
|
Cold start | ~25β30s (index download + model load) |
Inference | CPU-only, no GPU |
FAISS search | MMR, k=3 from fetch_k=20 |
LLM | Claude Haiku 4.5, temp=0.3, max_tokens=1000 |
π£οΈ Roadmap
MedQuAD medical knowledge base
MMR search for diverse, non-redundant context
Server-side LLM synthesis (
/chatendpoint)API key security β backend-only, Azure secret
Full-page frontend with dark mode and Lucide icons
MCP Streamable HTTP endpoint (verified end-to-end)
Pre-built FAISS index loaded from Blob Storage
Automated CI/CD with GitHub Actions
OIDC federated auth for CI/CD (replace ACR admin credentials)
Rate limiting on
/chatAPI key authentication for
/chatand/uploadAzure Application Insights (latency, error rate, usage)
Persist
/uploaddocuments to Blob StorageMulti-language support
π License
Provided as-is for educational and portfolio purposes.
π€ Author
Oumniya Moutaouakil β AI Engineer, LLM / Agentic AI & RAG Systems.
Project status: β Production-ready β backend on Azure Container Apps, frontend on Vercel, REST + MCP endpoints verified end-to-end.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/oumniya03/rag-mcp-azure'
If you have feedback or need assistance with the MCP directory API, please join our Discord server