claims-coverage-assessment-assistant
MCP server that reviews insurance claims against policy documents, providing coverage decisions, policy clause retrieval, claim history lookup, coverage rule checks, and fraud risk scoring via four tools.
README
Claims Coverage Assessment Assistant
An AI assistant that reviews an insurance claim against policy documents and returns a structured coverage decision: coverage outcome, supporting policy clauses, a confidence score, a fraud risk assessment, and a flag for cases that need human review.
Single FastAPI service. A tool-calling LLM agent, backed by RAG and four tools exposed over MCP, running against MongoDB (Atlas locally / Azure Cosmos DB for MongoDB vCore in production). No stub/offline mode for the LLM or embeddings -- one real code path, local and in production.
1. Architecture
┌───────────────────────────────┐
POST /claims/assess ──▶│ FastAPI app │
│ ┌───────────────────────────┐ │
│ │ Agent Orchestrator │ │
│ │ (LLM tool-calling loop) │ │
│ └─────────────┬───────────────┘ │
│ │ real MCP client │
│ │ (loopback HTTP) │
│ ┌─────────────▼───────────────┐ │
│ │ MCP server, mounted /mcp │ │
│ │ exposes 4 tools: │ │
│ │ - retrieve_policy_clauses │ │
│ │ - lookup_claim_history │ │
│ │ - check_coverage_rules │ │
│ │ - score_fraud_risk │ │
│ └─────────────┬───────────────┘ │
└────────────────┼────────────────────┘
▼
MongoDB (Atlas locally / Cosmos DB for
MongoDB vCore in production)
- One process, one container. The MCP server is not a separate
deployable service -- it's mounted at
/mcpinside the same FastAPI app. - The agent is a real MCP client, not a direct function-call shortcut:
it calls
initialize()→call_tool()over loopback HTTP against this app's own/mcpendpoint. Any external MCP client could reuse the same four tools the same way. - LLM: OpenAI or Azure OpenAI (
LLM_PROVIDER), real function-calling, no stub. Embeddings: OpenAI (text-embedding-3-small), no TF-IDF.
2. Data model
Product catalog is kept separate from what a customer actually bought -- avoids duplicating identical rules/clauses per customer, and lets policy wording change over time without breaking already-issued policies.
| Collection | Purpose | Key fields |
|---|---|---|
product_versions |
Reusable product rules + wording (e.g. AUTO-GOLD-V1) |
product_version_id, policy_type, excluded_claim_types, waiting_period_days |
issued_policies |
One customer's actual purchased policy | policy_id, customer_id, product_version_id, sum_insured, inception_date |
policy_clauses |
RAG corpus, shared per product version | clause_id, product_version_id, text, embedding |
claims |
Claims across their whole lifecycle -- also serves as "claim history" | claim_id, policy_id, status, decision |
claims is one collection, not two. A claim is the same entity
throughout its lifecycle (submitted → under_review → approved/rejected);
"claim history" is a query over this same collection for other claims on
the same policy -- excluding the claim currently being assessed, since
it's persisted with status=submitted before the agent loop runs and
would otherwise show up in its own history/fraud-frequency lookup:
{ "policy_id": policy_id, "claim_id": { "$ne": current_claim_id } }
CLM-001
submitted
│
under_review (requires_human_review = true)
│
approved / rejected (based on coverage_outcome)
3. End-to-end request flow
1. POST /claims/assess → validated against ClaimRequest
2. Route → ClaimAssessmentService.assess()
3. a. get_issued_policy(policy_id) -- 404 if missing
b. validate claim.customer_id == policy.customer_id
c. save_claim() -- persisted with status=submitted, BEFORE the
agent loop, because tools 3 & 4 look the
claim up by claim_id, not by receiving it
as an argument
4. Orchestrator.run(claim) -- loop, max 6 iterations:
a. LLM decides: call a tool, or return final JSON
b. tool call → real MCP call (initialize → call_tool) → one of:
- retrieve_policy_clauses(query, policy_id)
→ resolves policy_id → product_version_id → RAG search
- lookup_claim_history(claim_id, lookback_days?)
→ resolves claim → policy_id, excludes claim_id itself
- check_coverage_rules(claim_id)
→ deterministic: exclusions/waiting period from the
product version, sum insured/inception from the
issued policy -- NO LLM in this decision
- score_fraud_risk(claim_id)
→ deterministic weighted heuristic -- NO LLM here either
c. result fed back into the conversation, loop continues
5. LLM returns final JSON → parsed into CoverageDecision
- fraud_risk is ALWAYS re-sourced from the actual tool result,
never trusted from the LLM's own retelling
- malformed/missing JSON → fallback assembly straight from
whatever tool results were gathered (requires_human_review
forced true in that case)
6. save_decision() -- adds `decision` + advances `status`
7. CoverageDecision → JSON response
4. Component reference
| Component | File(s) | Role |
|---|---|---|
| Config | config.py |
All env vars, one place, no os.environ elsewhere |
| Domain models | domain/models.py |
Shared Pydantic contracts across every layer |
| Repositories | db/repositories/*.py |
Only place that talks Mongo query syntax |
| RAG chunking | rag/chunking.py |
Product-version clauses → citable chunks |
| RAG embeddings | rag/embeddings.py |
OpenAI text-embedding-3-small, one implementation |
| RAG indexer | rag/indexer.py |
Ingestion pipeline (scripts/seed_db.py calls this) |
| RAG retriever | rag/retriever.py |
Query-time cosine similarity, scoped to one product_version_id |
| 4 tools | tools/*.py |
Plain async functions, registered on the MCP server |
| MCP server | mcp_server/server.py |
FastMCP, mounted at /mcp, wraps every tool return in one JSON object |
| LLM client | agent/llm_client.py |
OpenAI / Azure OpenAI, real function-calling |
| Prompts | agent/prompts.py |
System prompt + tool schemas (what the model is told) |
| Orchestrator | agent/orchestrator.py |
The loop; ToolExecutor = the real MCP client |
| Service | services/claim_assessment_service.py |
The one use case: assess a claim |
| API | api/routes/*.py, api/deps.py |
Route handlers (no logic) + dependency wiring |
| App factory | main.py |
Builds Mongo, mounts MCP, registers routes |
Design decisions, briefly:
- No LangChain/LangGraph. The whole loop is ~150 lines of plain Python
in
orchestrator.py-- fully explainable, no hidden control flow, and this project has no multi-agent/graph/branching need that would justify the abstraction. - No multi-agent / A2A. One model, one loop, four tools -- sufficient for "assess one claim," and avoids complexity with no corresponding need.
- Coverage rules and fraud scoring are deterministic code, not LLM calls. Eligibility decisions must be auditable; the LLM only combines and explains tool outputs, never decides them.
- Fraud scoring is a transparent weighted heuristic, not a trained ML
model -- explicitly POC-level; interface (
claim_idin,FraudRiskResultout) would stay the same if swapped for a real model later. - Brute-force cosine similarity in Python, not a vector index. Fast enough at POC scale (hundreds of clauses); the retriever's return type is the isolation boundary if a native vector index is added later.
- MCP is genuinely used, not decorative. The orchestrator is a real MCP client calling the mounted server over loopback HTTP -- same protocol an external client would use, not a shortcut.
5. Setup and run (no Docker required)
# 1. Database -- MongoDB Atlas free tier (mongodb.com/atlas), or local mongod
cp .env.example .env
# edit .env: MONGO_URI, OPENAI_API_KEY (required -- no offline mode)
# 2. Install
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# 3. Seed (product versions + issued policies + prior claims)
python scripts/seed_db.py
# 4. Run
cd src && uvicorn app.main:app --reload
- API docs:
http://localhost:8000/docs - MCP endpoint:
http://localhost:8000/mcp/ - Health check:
http://localhost:8000/health
Tests
pytest tests/ -v
25 tests, no external services or API keys needed -- mongomock-motor for
persistence, hand-written FakeLLMClient/FakeToolExecutor doubles for
orchestrator unit tests, and one integration test that spins up a real
MCP server (real protocol, real tools) with only the LLM and database
mocked.
Deployment
docker/Dockerfile builds the single-container image.
.github/workflows/deploy.yml: tests on every push; on main, builds,
pushes to Azure Container Registry, deploys to Azure Container Apps.
Runtime config (MONGO_URI, OPENAI_API_KEY, etc.) is set as environment
variables on the Container App itself, not in the workflow.
6. Testing / cURL examples
Seed the database first (python scripts/seed_db.py). All examples use
the seeded POL-AUTO-1001 / POL-HEALTH-2001 policies. Always include
filed_at explicitly, so the fraud "claim frequency" signal lines up with
the seeded 2024-dated history correctly.
Test 1 — Clean claim: fully covered, low risk
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-1", "policy_id": "POL-AUTO-1001", "customer_id": "CUST-001",
"claim_type": "collision", "description": "Rear-ended at a traffic light",
"amount": 45000, "incident_date": "2024-06-01T00:00:00Z", "filed_at": "2024-06-02T00:00:00Z"
}'
Expected: coverage_outcome: "covered", fraud_risk.risk_level: "low", requires_human_review: false.
Test 2 — Excluded claim type → rejected
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-2", "policy_id": "POL-AUTO-1001", "customer_id": "CUST-001",
"claim_type": "racing", "description": "Damage during a street race",
"amount": 30000, "incident_date": "2024-06-01T00:00:00Z", "filed_at": "2024-06-02T00:00:00Z"
}'
Expected: coverage_outcome: "not_covered", requires_human_review: true.
Test 3 — Waiting period violation
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-3", "policy_id": "POL-AUTO-1001", "customer_id": "CUST-001",
"claim_type": "collision", "description": "Collision shortly after buying the policy",
"amount": 10000, "incident_date": "2024-01-20T00:00:00Z", "filed_at": "2024-01-21T00:00:00Z"
}'
Expected: coverage_outcome: "not_covered" (day 5 of a 15-day waiting period), fraud_risk.risk_level: "medium".
Test 4 — Amount exceeds sum insured
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-4", "policy_id": "POL-AUTO-1001", "customer_id": "CUST-001",
"claim_type": "collision", "description": "Major collision, vehicle totaled",
"amount": 900000, "incident_date": "2024-06-01T00:00:00Z", "filed_at": "2024-06-02T00:00:00Z"
}'
Expected: coverage_outcome: "not_covered" (900,000 > sum insured 800,000), fraud_risk.risk_level: "medium".
Test 5 — Covered by rules, but HIGH fraud risk (best one to demo live)
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-5", "policy_id": "POL-AUTO-1001", "customer_id": "CUST-001",
"claim_type": "theft", "description": "Vehicle stolen from parking lot",
"amount": 600000, "incident_date": "2024-02-01T00:00:00Z", "filed_at": "2024-02-02T00:00:00Z"
}'
Expected: coverage_outcome: "covered" but fraud_risk.risk_level: "high" (high-amount + early-claim signals both fire), requires_human_review: true. Shows coverage and fraud risk are independent axes.
Test 6 — Health policy, different waiting period
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-6", "policy_id": "POL-HEALTH-2001", "customer_id": "CUST-002",
"claim_type": "hospitalization", "description": "Emergency admission for surgery",
"amount": 50000, "incident_date": "2023-07-01T00:00:00Z", "filed_at": "2023-07-02T00:00:00Z"
}'
Expected: coverage_outcome: "not_covered" (day 30 of a 90-day waiting period).
Test 7 — Unknown policy → 404
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{
"claim_id": "TEST-7", "policy_id": "POL-DOES-NOT-EXIST", "customer_id": "CUST-999",
"claim_type": "collision", "description": "Test", "amount": 1000,
"incident_date": "2024-06-01T00:00:00Z", "filed_at": "2024-06-02T00:00:00Z"
}'
Expected: HTTP 404, {"error_code": "policy_not_found", ...}.
Test 8 — Invalid payload → 422
curl -X POST http://localhost:8000/claims/assess -H "Content-Type: application/json" -d '{"claim_id": "X"}'
Expected: HTTP 422.
Test 9 — Health check
curl http://localhost:8000/health
Expected: {"status": "ok", "mongo_connected": true}.
7. Environment variables
| Variable | Default | Purpose |
|---|---|---|
MONGO_URI |
mongodb://localhost:27017 |
Atlas / local / Cosmos DB vCore connection string |
MONGO_DB_NAME |
claims_assistant |
Database name |
OPENAI_API_KEY |
— | Required always (RAG embeddings; also default LLM provider) |
LLM_PROVIDER |
openai |
openai | azure_openai |
OPENAI_MODEL |
gpt-4o-mini |
Model for the agent loop |
AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_API_KEY / AZURE_OPENAI_DEPLOYMENT |
— | Required if LLM_PROVIDER=azure_openai |
RAG_TOP_K |
3 |
Clauses retrieved per query |
FRAUD_HIGH_AMOUNT_THRESHOLD |
500000 |
Fraud signal: high claim amount |
FRAUD_EARLY_CLAIM_DAYS_THRESHOLD |
30 |
Fraud signal: claim shortly after inception |
FRAUD_FREQUENCY_LOOKBACK_DAYS |
365 |
Fraud signal: frequency window |
FRAUD_FREQUENCY_CLAIM_COUNT_THRESHOLD |
3 |
Fraud signal: claim count trigger |
See .env.example for the full list.
Recommended Servers
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.
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.
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.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
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.
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.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
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.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.