Skip to main content
Glama
saivarun1410

insurance-mcp-poc

by saivarun1410

insurance-mcp-poc

A Model Context Protocol server that exposes a life-insurance back office — policy documents, in-flight applications, and product/underwriting rules — as tools an LLM agent can call.

The point of the POC: an agent shouldn't need a bespoke integration per assistant. Implement the domain once as an MCP server, and any MCP-capable client (Claude Code, Microsoft Foundry agents, an internal chat surface) gets the same twenty-four tools with the same contracts.

All data in this repository is synthetic. The schema, products, rules, and documents were invented for this demo and are not derived from any production system.

Built alongside Microsoft Applied Skills: Integrate model context protocol tools with agents in Microsoft Foundry. That assessment covers the client side — attaching an MCP tool to a Foundry agent and validating its calls. This repository is the other half: the server those tools come from.

Quickstart

Requires Docker and Node 20+.

npm install
npm run setup     # starts Postgres+pgvector, seeds the corpus, runs the smoke test

npm run setup is the whole demo: it stands up the database, embeds and inserts 13 documents, then connects to the MCP server as a real MCP client and exercises all twenty-four tools. Expected tail:

Connected. Server exposes 24 tools:
  - search_policy_documents: Search policy documents
  - get_application_status: Get application status
  ...
  - update_requirement_status: Update requirement status
  - reassign_application: Reassign application
...
All tool calls completed.

Then npm run demo walks the chained flow an agent actually performs — see below. Tear down with npm run db:down.

Related MCP server: mcp-langchain-agent

The tools

Twenty-four tools. The design rule: a tool maps to a decision someone makes, not to a table. There is no get_product or list_events here — an agent that has to assemble answers from CRUD primitives burns turns and invents joins. Each tool below answers a question a person actually asks.

Case handling — one known application

Tool

Answers

get_application_status

"Where is APP-100242 and what is it waiting on?" Status, blocking step, underwriter, full event timeline.

get_outstanding_requirements

"Why isn't it moving, and what do I chase today?" Open requirements with age in days, plus the follow-up action the procedure calls for at 14 / 28 / 90 days.

find_applicant

"What's happening with Priya's application?" Name, whole or partial, to application numbers. Everything else here needs the number — this is how you get it.

add_case_note

"Record that I called the provider." Appends to the timeline.

update_requirement_status

"The APS came in." Marks a requirement received or waived and logs it.

order_requirement

"Order him an EKG." The other half of the requirement lifecycle — update_requirement_status can only close ones that exist.

record_underwriting_decision

"Approve it at standard." The actual decision: approved, declined, referred, or approved with a rating.

create_application

"Take this application." The entry point — every other case tool needs a case that already exists.

withdraw_application

"She's gone with someone else." Closes a case from the customer side, which is not an underwriting outcome.

amend_application

"He wants $4M instead of $750k." Revises cover or product on an in-flight case, re-checking the limits.

Pipeline — across the whole book

Tool

Answers

find_applications

"What's stuck?" Filter by status, product, underwriter, state, or days untouched.

get_underwriter_workload

"Who is overloaded?" Open cases, face amount at risk, oldest untouched case per underwriter.

get_pipeline_metrics

"How are we doing?" Cases and face amount by status, plus outstanding requirements bucketed against the 14 / 28 / 90-day thresholds.

reassign_application

"Move this off D. Lindqvist." Reassigns and records why.

get_requirement_catalog

"How long will an APS hold this up, and which vendors are slow?" Historical turnaround, with the sample size attached so a single data point isn't mistaken for a benchmark.

Sales and pricing — before a case exists

Tool

Answers

find_eligible_products

"What can I sell a 62-year-old in Texas for $2M?" Every issuable product plus the rules that will fire. The inverse of lookup_product_rules.

estimate_premium

"What will it cost?" Annual and monthly premium from the rate table, by age band and risk class.

get_rate_card

"How was that derived?" Every risk class and age band for a product.

compare_products

"What can I sell her, and what does each cost?" Eligible products priced and sorted cheapest first, in one call instead of four.

lookup_product_rules

"What are the limits on UL-200?" Issue limits and underwriting rules; evaluates hard eligibility when given an applicant.

Knowledge

Tool

Answers

search_policy_documents

"What does the contract actually say?" Hybrid search over contracts, riders, guidelines and procedures, returning excerpts with doc_id so answers can cite a source.

list_documents

"What guidance exists for this product?" Enumerates the corpus without searching — also the fallback when a search returns nothing.

find_similar_documents

"What else relates to this clause?" Starts from a document rather than a question, reusing the embeddings already stored.

get_document

"Quote me the exact clause." One document in full, since search truncates excerpts at 600 characters. Also returns the product's underwriting rules, which is usually what a reader needs next.

Sixteen of the twenty-four are read-only and marked readOnlyHint: true. The eight writers differ in a way the annotations capture, because clients use them to decide what needs human confirmation:

Tool

Semantics

Annotations

add_case_note

Appends. Two calls write two notes.

idempotentHint: false

update_requirement_status

Edits a row, but re-applying the same value changes nothing.

idempotentHint: true

reassign_application

Overwrites the assignment; same target twice is a no-op.

idempotentHint: true

order_requirement

Inserts, but refuses to duplicate an outstanding requirement.

idempotentHint: true

record_underwriting_decision

Sets status; recording the same decision twice is a no-op.

idempotentHint: true

create_application

Inserts a new case. Two calls create two applications.

idempotentHint: false

withdraw_application

Closes a case; withdrawing twice changes nothing.

idempotentHint: true

amend_application

Revises cover; an amendment matching current values is a no-op.

idempotentHint: true

None are marked destructive: nothing here deletes, and every writer checks its target exists first and returns a plain "nothing was changed" result rather than throwing.

Guardrails sit at four levels

Worth separating, because only the first is free:

  1. Schema — zod becomes JSON Schema and the SDK validates before the handler runs. A missing required field, a string where a number belongs, an age of 999, an unknown enum value: all rejected with a message naming the offending field, and the database is never touched.

  2. Existence — every writer confirms its target exists and returns a plain result saying nothing changed, rather than throwing.

  3. Domain — the rules no schema can express, because they depend on other rows or on the catalogue. record_underwriting_decision refuses to approve while requirements are outstanding; create_application refuses a product the applicant cannot be issued; withdraw_application refuses a case that has already been decided; amend_application re-checks the revised figure against the product limits and refuses a case that is already closed. Each refusal names what blocked it and which tool moves things forward, so a model can recover instead of stalling.

  4. DatabaseCHECK constraints on every status and class column, foreign keys throughout. The last line of defence if the code above is wrong.

Parameterised SQL everywhere, so '; DROP TABLE applications; -- is searched for as a name and matches nobody.

Try, once connected: "Rowan Kessler's application is stuck — what's it waiting on, and what does the guideline actually say about that requirement?" No single tool answers that. The agent chains all three, and npm run demo shows the same chain step by step:

[1] get_application_status("APP-100242")
      -> Rowan Kessler, age 61, SecureTerm 20-Year
      -> status=pending_underwriting  blocked on: awaiting_paramedical
      -> rules fired: TRM20-AGE-01, TRM20-FACE-01
[2] lookup_product_rules("TRM-20", age=61, face=1500000)
      -> TRM20-AGE-01 [require_evidence]: Applicants over 60 require a paramedical exam…
      -> TRM20-FACE-01 [refer]: Face amounts above $1,000,000 are referred…
[3] search_policy_documents("when is a paramedical examination required")
      -> GUIDE-UW-01  (rrf 0.03154, vector rank 6, text rank 1)

Note step 3: the right document ranked 6th by vector similarity but 1st by full-text. Fusing the two rankings is what surfaces it.

How retrieval works

Hybrid search, fused with Reciprocal Rank Fusion (score = Σ 1/(60 + rank_i)).

Two things forced this design, both found by testing rather than assumed:

  • Weighted score blending doesn't work here. Cosine similarity lands around 0.1–0.4 while ts_rank_cd returns values an order of magnitude smaller, so any fixed weighting lets whichever metric happens to be larger dominate. RRF combines ranks, which are scale-free.

  • websearch_to_tsquery ANDs every term, so a full-sentence question matches zero documents and the hybrid silently degrades to vector-only. The operators are rewritten to OR, making the lexical side rank by how many query terms a document contains.

Each result reports vector_rank and text_rank alongside the fused score, so it stays visible which half did the work — and a text_rank of null means that document matched no query term.

Connecting it to Claude Code

claude mcp add insurance --  node /absolute/path/to/insurance-mcp-poc/src/index.js

Or add to .mcp.json:

{
  "mcpServers": {
    "insurance": {
      "command": "node",
      "args": ["/absolute/path/to/insurance-mcp-poc/src/index.js"],
      "env": { "DATABASE_URL": "postgres://insurance:insurance@localhost:55432/insurance" }
    }
  }
}

What happens when a client calls a tool

Worth being precise about, because the common mental picture — "the model calls my API" — is wrong in two ways. The model never talks to this server. The client does. And there is no HTTP involved: this server speaks JSON-RPC 2.0 over its own stdin and stdout.

Startup, once per session. The client (Claude Code, a Foundry agent) spawns this processnode src/index.js — and holds its stdin/stdout pipes. It sends initialize, the server replies with protocol version and capabilities, the client sends the initialized notification. Then the client calls tools/list, and the SDK answers with all twenty-four tools: name, description, annotations, and a JSON Schema for the arguments, which it generated from the zod schemas in src/index.js.

The client puts those tool definitions into the model's context. This is the step people skip. The description strings above are not documentation — they are the prompt. A tool the model misunderstands is a tool it calls wrongly, which is why each description says when to reach for this one rather than just what it returns.

Per call. The model emits a tool-use request naming a tool and its arguments. The client — not the model — sends:

// stdin →
{"jsonrpc":"2.0","id":7,"method":"tools/call",
 "params":{"name":"get_application_status","arguments":{"application_number":"APP-100242"}}}

The SDK validates arguments against that tool's schema and rejects the call before the handler runs if it doesn't fit — the model gets a schema error back and can retry. On success it invokes the handler, which runs parameterised SQL against Postgres and returns content blocks:

// ← stdout
{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"{ \"application_number\": ... }"}]}}

The client feeds that result back into the model's context as the tool result, and the model decides what to do next — often calling another tool, which is exactly the chain the demo above walks.

Two distinct failure modes, worth keeping straight:

  • A protocol error (unknown tool, malformed arguments) returns a JSON-RPC error. The model sees it went wrong mechanically.

  • A tool-level failure — "no application found with number APP-999999" — is a successful JSON-RPC response whose content says so. That's deliberate: it's information the model should reason about, not a crash. add_case_note does this rather than throwing, and writes nothing.

Concurrency and lifetime. Requests carry an id, so the client may have several in flight at once; responses are matched by id, not by order. The process lives as long as the client session and holds a pg connection pool across calls — so state like the pool is per-session, and anything you want durable belongs in Postgres.

How it fits together

MCP client (Claude Code / Foundry agent)
        │  stdio, JSON-RPC
        ▼
   src/index.js          tool definitions + zod input schemas
        │
        ├── src/embed.js  query → vector
        └── src/db.js     pg pool
                 │
                 ▼
        Postgres 16 + pgvector      docker-compose, port 55432

Layout: src/index.js (server and all twenty-four tools) · src/embed.js (embedding) · src/db.js (pool) · db/init.sql (schema + seed) · scripts/seed.mjs (documents + embeddings) · scripts/smoke.mjs (exercises every tool) · scripts/demo.mjs (the chained flow) · scripts/benchmark.mjs (retrieval quality).

The embedding, and why it was measured

src/embed.js runs all-MiniLM-L6-v2 locally, in-process, via transformers.js (ONNX). No API key, no separate service, no Python. Weights (~23MB quantized) download once on first use and cache under ~/.cache/huggingface; the model loads lazily on the first search, so spawning the server stays instant.

It replaced a hashed bag-of-words stand-in, and npm run benchmark records what that was worth — ten questions with a known-correct document, plus paraphrases that share no vocabulary with their target:

hashed bag-of-words

all-MiniLM-L6-v2

vector only

8/10

10/10

full-text only

9/10

9/10

hybrid (RRF)

9/10

10/10

paraphrases

1/3

2/3

The number that matters is the last row. The old embedding scored 0.0000 cosine between "dies by suicide" and "takes their own life" — identical to its score against an unrelated sentence about grace periods, because it matched shared words and those phrases share none. Full-text search has the same ceiling for the same reason. Only a trained model can close that gap, and it is the sole reason the vector half exists: on the ten literal questions, plain Postgres full-text was already beating vector-only 9 to 8.

The one paraphrase still missed lands at rank 3 of 13, not wildly off, and adding a few words of context ("...within two years of the policy date") puts it first. Short queries are ambiguous.

Changing model means changing EMBEDDING_DIM here and vector(384) in db/init.sql, then npm run db:down && npm run db:up && npm run seed. Old vectors are meaningless under a new model.

Notes and limitations

Worth stating plainly, since they're the things a reviewer would ask about:

  • No approximate index, on purpose. An early version had ivfflat ... WITH (lists = 10) over 12 rows. It silently returned wrong and short result sets — a single probe scans a near-empty partition. Approximate indexes only pay off at volume. At this corpus size an exact scan is both correct and instant; db/init.sql says where to add HNSW once the corpus justifies it.

  • Rules are data, not an engine. underwriting_rules.condition holds plain-language conditions for the agent to reason over. Only the hard limits (age band, face band, state availability) are actually evaluated in code. A production version would compile these to an executable rule set — an LLM interpreting underwriting conditions free-hand is not something to ship.

  • No authentication or tenancy. The server trusts its caller completely. Real deployment needs per-caller authorization, since these tools read customer data.

  • The model has no idea what insurance is. It learned which phrases keep company with which, which is enough for paraphrase but leaves a known blind spot: antonyms and negation sit close together, because "the premium increased" and "the premium decreased" appear in near-identical contexts. For a policy corpus that is not academic — retrieving the opposite clause and citing it confidently is worse than returning nothing. It is the strongest argument for keeping the full-text half, which is dumb about meaning but never confuses "not covered" with "covered".

  • RRF ties are common and were non-deterministic. A document ranked (1,2) scores identically to one ranked (2,1), and without a tie-break Postgres returned whichever the plan emitted first — the same query gave different answers in different contexts. Now broken by vector rank, then doc_id. Worth knowing if you fuse rankings anywhere else.

  • node_modules is 401MB. Running the model in-process means shipping ONNX runtime binaries. A hosted embedding endpoint would trade that for an API key and a network hop.

License

MIT

Available Tools

3 tools
get_application_statusGet application statusA

Look up a life insurance application by its number and return current status, the step it is waiting on, the assigned underwriter, and the full event timeline.

ParametersJSON Schema
NameRequiredDescriptionDefault
application_numberYesApplication number, e.g. APP-100242

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the burden. It clearly states the returned data: status, pending step, underwriter, and event timeline, implying a read-only query. It does not add explicit permission or side-effect notes, but the 'Look up' phrasing adequately conveys a non-mutating operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence that is front-loaded with the verb and resource, then lists the exact information returned. No wasted words or irrelevant details.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple query tool with one parameter and no output schema, the description is complete: it specifies the input and enumerates the exact output fields (status, step, underwriter, timeline). No additional context is needed for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%; the parameter application_number is well-described with a format example. The description reinforces that lookup is by number but adds little beyond the schema. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb 'Look up' with a clear resource 'life insurance application' and a defining attribute 'by its number'. It clearly distinguishes from siblings like search_policy_documents and lookup_product_rules, which focus on different resources and actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The context is clear: use this tool when you have an application number and need status information. While it doesn't explicitly mention alternatives, the sibling tools are obviously different, and the usage context is unambiguous. Exclusions are not necessary here.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

lookup_product_rulesLook up product rulesA

Return a product’s issue limits and underwriting rules. When applicant_age, face_amount or state are supplied, also evaluates hard eligibility and flags which rules would trigger.

ParametersJSON Schema
NameRequiredDescriptionDefault
stateNoTwo-letter state code
face_amountNo
product_codeYesProduct code, e.g. UL-200
applicant_ageNo

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses that supplying applicant_age, face_amount, or state triggers hard eligibility evaluation and rule flagging. This is valuable behavioral context beyond the schema. However, it does not explicitly state the read-only nature, though 'lookup' implies it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the primary function, and contains no filler. Every word contributes to understanding the tool's behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the core purpose and conditional behavior effectively. Given no output schema, it could have described the return format more explicitly, but for a lookup tool with moderate complexity, it is adequate. The absence of side effects or prerequisites is not a significant gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 50%, with descriptions for state and product_code. The description adds meaning by explaining that three optional parameters trigger eligibility evaluation, which is not evident from the schema alone. It stops short of detailing units or semantic constraints for applicant_age and face_amount, but the param names are self-explanatory.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool returns a product's issue limits and underwriting rules, which is a specific verb+resource. It also distinguishes itself from siblings by focusing on product rules rather than policy documents or application status.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use the tool: when product rules or eligibility information is needed. It does not explicitly mention alternatives, but sibling names are sufficiently distinct to avoid confusion. Clear context without explicit exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_policy_documentsSearch policy documentsA

Semantic search over policy contracts, riders, underwriting guidelines and disclosure documents. Use this to answer questions about what a policy says. Returns ranked excerpts with document ids.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax results, default 3
queryYesNatural-language question or keywords
product_codeNoRestrict to one product, e.g. TRM-20

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It discloses that this is a semantic search operation and states the return type: 'Returns ranked excerpts with document ids.' This is sufficient for a read-only search tool, though it omits details like pagination or result limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the core purpose, and every sentence adds value. It is compact, clear, and free of redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a search tool with a well-described schema, the description covers the purpose, the use case, and the return value. The lack of an output schema is mitigated by explicitly mentioning that results are ranked excerpts with document ids.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema covers 100% of parameters with descriptions, so the baseline is 3. The description adds little parameter-specific meaning beyond reinforcing that the query is natural-language based, which the schema already states.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Semantic search over policy contracts, riders, underwriting guidelines and disclosure documents', identifying both the verb and resource. It also distinguishes itself from siblings like lookup_product_rules and get_application_status by specifying that this tool answers questions about policy content.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear usage context: 'Use this to answer questions about what a policy says.' It does not explicitly mention when not to use it or name alternative tools, but the sibling names and the stated purpose make the intended use clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 3 tool updatesv0.1.0
    • First observedget_application_status
    • First observedlookup_product_rules
    • First observedsearch_policy_documents

TDQS

A4.4/5.0
Disambiguation5/5

Each tool targets a distinct aspect of insurance operations: policy document search, application status tracking, and product rule evaluation. There is no overlap in their purposes or outputs.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern with snake_case: search_policy_documents, get_application_status, lookup_product_rules. The verbs (search, get, lookup) are semantically appropriate and uniform.

Tool Count5/5

With only 3 tools, the server is tightly scoped to a specific insurance POC use case. Each tool addresses a core need without redundancy, making the count appropriate.

Completeness4/5

The main workflows of querying policy content, checking application status, and verifying product rules are covered. Minor gaps exist (e.g., no tools for creating/updating applications), but for a POC the surface is reasonably complete.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server exposing internal business operations as tools — task management (create, list, update status) and RAG-style semantic search over an internal knowledge base (leave, expense, and onboarding policies) that any MCP-compatible AI agent can call directly for grounded, non-hallucinated answers.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    MCP server that provides tools for retrieving transaction context and recording AI decisions or creating human reviews for payment risk exceptions. Enables LLM agents to handle exception transactions in a hybrid payment-decisioning workflow.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that answers questions over insurance and regulatory documents using retrieval-augmented generation, returning grounded, cited passages via local embeddings and OpenSearch.
    -

Latest Blog Posts

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/saivarun1410/insurance-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server