MCP server
A read-only MCP server over the wiki. Point any MCP client at it to let an agent query the insurance knowledge base.
Install & run
The server is keyless and read-only; the dataset is the repo itself. From a clone:
python3 -m venv .venv && . .venv/bin/activate
pip install -r mcp/requirements.txt
python mcp/insurance_wiki_mcp.py
Or install it as a console script (pip install . from the repo root, or from anywhere with
pip install git+https://github.com/sluyasu/OpenInsurance.git). When the script does not run
from inside a clone, point it at one:
INSURANCE_WIKI_REPO=/path/to/OpenInsurance insurance-wiki-mcp
Register with a client
Claude Code one-liner (from the repo root):
claude mcp add insurance-wiki -- "$(pwd)/.venv/bin/python" "$(pwd)/mcp/insurance_wiki_mcp.py"
Or in any MCP client config (e.g. a project .mcp.json):
{
"mcpServers": {
"insurance-wiki": {
"command": "/absolute/path/to/OpenInsurance/.venv/bin/python",
"args": ["/absolute/path/to/OpenInsurance/mcp/insurance_wiki_mcp.py"]
}
}
}
Tools
| Tool | Purpose |
|---|---|
list_countries() |
Countries covered: products, insurers, branches covered vs taxonomy vs written overviews. |
list_branches(country) |
Branches for a country, with mandatory flag and product counts. |
search(query, country, type, branch, insurer, limit) |
Search pages by title/content, filterable; announces when results were truncated. |
get_page(path) |
Full text of one knowledge page. |
get_product(country, insurer_slug, product_name, document_type?, edition?) |
A product's full structured data (coverages, exclusions, ...) + source_url. |
get_coverage(country, insurer_slug, product_name, topic, document_type?, edition?) |
Compact answer to one question: only the coverages/exclusions relevant to topic, with limits, conditions, quotes and pages. |
verify_claim(country, insurer_slug, product_name, claim) |
Verbatim excerpts that share the claim's terms, so the calling agent can fact-check its own draft. Retrieval, not judgment. |
compare_products(country, product_names, on, insurer_slugs?) |
Compare products on coverages / exclusions / deductibles. |
find_overlap(country, product_names, on, insurer_slugs?) |
Flag candidate duplicate cover when combining 2+ products (e.g. home + family liability). |
get_branch_overview(country, branch) |
The hand-authored branch overview. |
One commercial product usually maps to several documents (its general conditions and its IPID share the
name, and several editions can coexist). Tools that take a product name resolve deterministically - general
conditions over summaries, non-superseded, newest edition, then reference order as the final tie-break - and
every response identifies the chosen document (document_type, edition_date, reference, superseded):
the single-product tools list the alternatives under other_documents, the multi-product tools carry the
identifiers inside each entry. Use document_type= / edition= on get_product / get_coverage to pick a
specific one.
What keeps the answering LLM honest
The LLM consuming these tools is the layer that can hallucinate, so the response shape works against that:
- Grounding contract + citation line. Every response carrying product facts starts with a short contract ("state only what's here, never invent dates/articles/figures, cite source_url, no advice or ranking"); single-product responses add a compact CITATION line carrying the real product, insurer, document type, edition date (or "not stated"), reference and source_url. The model copies real values instead of reconstructing them.
- Ambiguity is refused, not guessed. A name matching several distinct products ("Assurance Auto" exists at several insurers) returns the candidates instead of silently picking one. An exact name wins over products that merely contain it, and the response lists the similar products it skipped.
- Ties are flagged. When two documents of the same type carry the same edition date, the response says the choice fell back to reference order and points at the alternative.
- Gaps and caps are explicit. Superseded documents say so in the citation, empty topic matches say "not proof of absence", truncated search results announce the real total.
- Self-verification. A chatbot can (and should) re-check its own draft: extract each factual assertion,
call
verify_claimwith it, and drop or soften anything whose evidence does not literally say it. The recommended system-prompt rules for any chatbot built on this server:- State a fact (date, article, amount, deductible, formula) only if it appears in a tool response; otherwise say "not stated in the document".
- Never pick an insurer for the user: if none is named, ask or list neutrally.
- No advice verbs (recommend, choose, drop, best): information, not advice.
- End with the
source_urlthe tools returned.
Measured latency
Warm server, in-process, median of 50 runs per tool (Apple silicon, Belgium dataset, 162 documents;
reproduce with .venv/bin/python tests/bench_tools.py):
| Tool | median | p95 |
|---|---|---|
list_countries |
0.09 ms | 0.10 ms |
list_branches |
0.04 ms | 0.04 ms |
search (title hit) |
3.0 ms | 3.2 ms |
search (full-text body scan) |
3.1 ms | 3.2 ms |
get_page |
0.04 ms | 0.04 ms |
get_product |
0.4 ms | 0.4 ms |
get_coverage |
2.2 ms | 2.3 ms |
verify_claim |
2.8 ms | 2.9 ms |
compare_products |
0.9 ms | 0.9 ms |
find_overlap |
0.9 ms | 0.9 ms |
get_branch_overview |
0.01 ms | 0.01 ms |
The server reads every file once (index, extractions, page bodies, country metadata, category keywords) and serves from memory, so no file is re-read per query. The first call after startup pays the cache fill (tens of ms). In a real chatbot, the user-perceived time is dominated by the LLM's own inference (seconds), not by these tools: keep the server process warm (any MCP client does) and the tool side is imperceptible. Full-text search scans the corpus in memory (~3 ms for one country); an inverted index only becomes worth it with several countries.
Security & testing
Every product/coverage response carries the source_url and a no-advice disclaimer. The server never writes,
and get_page only reads text files in the knowledge folders (wiki/, data/, _meta/, sources/,
schema/, root *.md) - never dotfiles, code or git internals, even through path tricks. Without a dataset
(installed via pip/uvx but no clone configured) the server refuses to start with instructions, instead of
serving empty results.
The test suite (tests/, run in CI) covers the get_page confinement, deterministic document selection,
accent/apostrophe-insensitive matching, ambiguity refusals, honest caps and a smoke test of every tool:
.venv/bin/pip install pytest && .venv/bin/python -m pytest tests/ -q
Overlap detection - and how to build an analysis tool like it
find_overlap answers a real question: "If I take policy A and policy B, which covers am I paying for twice?"
(the classic case: a home policy and a family-liability policy both cover civil liability). It's also a
worked example of how to add your own analysis tool to this server. Read it before writing one.
The design principle: the MCP tool stays a deterministic data provider
The tool does not call an LLM and does not give advice. It shapes the data so the calling agent can reason. Semantic judgment ("is this really the same cover?") is the agent's job; the tool's job is to surface strong, deterministic candidates with their sources. That keeps the server keyless, reproducible, and honest.
How find_overlap is built (4 pieces)
- A controlled vocabulary -
schema/coverage_categories.jsonmaps insurance concepts (responsabilite_civile,assistance,protection_juridique,vol,incendie, …) to keyword patterns (accent-insensitive, fr/nl). This is data, committed and versioned - extend it without touching code. - A categorizer -
_categorize(text)normalizes a coverage'sname + description(lowercase, strip accents) and returns every category whose keywords appear. Pure string matching, no model. - Grouping - each product's coverages are tagged; a candidate overlap is any category that appears in 2 or more of the products. Groups are returned with the offending items per product, sorted by how many products share them.
- Honesty - the response carries the no-advice disclaimer plus a
note: candidates are heuristic (a shared category), must be confirmed against the actual descriptions, and absence of a category is not proof of no overlap (the taxonomy isn't exhaustive). The tool has good recall and modest precision by design - it is a lead generator for an agent, not a verdict.
Worked example
find_overlap("be", ["Police habitation pour le propriétaire", "La Police familiale"])
→ candidate_overlaps: Responsabilité civile (both), Dommages / responsabilité animaux,
Incendie et périls connexes, Vol / vandalisme, Relogement / frais supplémentaires, …
The top hit is the true one: home cover (RC immeuble / locataire) and family cover (RC vie privée) both carry civil liability - a real potential duplication for the policyholder to check.
How to extend it
- Improve coverage without code: add categories or keywords to
coverage_categories.json, bump itsversion. - Higher precision (the deterministic upgrade): instead of keyword-matching at query time, tag one primary
coverage_categoryper coverage during extraction/build (a normalization pass), then overlap becomes an exact set intersection with no keyword noise. The samecoverage_categories.jsonis the seed for that pass.
How to add a brand-new tool
- Write a function in
mcp/insurance_wiki_mcp.pydecorated with@mcp.tool(). The docstring is the tool's description the agent sees - state what it does and its limits. - Read from
data/<cc>/extracted/*.json(structured product data) and/orwiki/(Markdown) via the existing_extracted()/_read_index()/_safe_repo_path()helpers. Never write. - Return a string (JSON or Markdown). For anything about coverages, prepend
DISCLAIMERand includesource_url. - Restart the server; the new tool is exposed automatically. No registration step.
Publishing to the MCP registry (maintainers)
The server is packaged by the root pyproject.toml (console script
insurance-wiki-mcp, PyPI name openinsurance-wiki-mcp). To list it on the
official MCP registry (which the public
directories like PulseMCP ingest automatically):
- Publish the package to PyPI:
python -m build && twine upload dist/*. - Install the publisher CLI and generate the manifest:
mcp-publisher init(namespaceio.github.sluyasu/*, packageopeninsurance-wiki-mcp, transport stdio, document theINSURANCE_WIKI_REPOenvironment variable). mcp-publisher login githubthenmcp-publisher publish.
See https://github.com/modelcontextprotocol/registry for the current publishing guide.
mcp-name: io.github.sluyasu/openinsurance-wiki
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openinsurance_wiki_mcp-0.2.0.tar.gz.
File metadata
- Download URL: openinsurance_wiki_mcp-0.2.0.tar.gz
- Upload date:
- Size: 36.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cab1f5ec2d470ad49f2a3488760fe75f9849e7de07582affe6dfc47ce83176bc
|
|
| MD5 |
937761ec766824d7e19576d49a7b3284
|
|
| BLAKE2b-256 |
d59467edabf94ccafa243146669f84815f84ca90ec4d5cb01c734e41cf8cb3fd
|
File details
Details for the file openinsurance_wiki_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: openinsurance_wiki_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 22.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e2bba52f082fbc6cf0ca4ea51ff992a224989e77c41bde5f7c4c22a736298f0
|
|
| MD5 |
9a949b9f528031208dcc81bb69cccb1e
|
|
| BLAKE2b-256 |
f2c297a8012ceaf4aaa8007756ac99ac81d449ed918e8c1ea61e8e7e01f9b854
|