lending-data-mcp-server

lending-data-mcp-server

Exposes a governed lending portfolio (loans, customers, risk-tier history) to any MCP-compatible AI client via read-only tools, schema resources, and analysis prompts, wrapping an existing API gateway instead of connecting directly to the database.

Category
Visit Server

README

Lending Data MCP Server

Stack: Python · MCP (Model Context Protocol) · httpx · FastMCP

An MCP server that exposes a lending portfolio - loans, customers, risk-tier history - to any MCP-compatible AI client (a desktop AI assistant, a LangGraph agent, any future agent framework) through a single standardized protocol, instead of a bespoke integration per client.

The business problem: A lending team wants to let an AI assistant answer questions about loans and customers - "how many high-risk loans do we have," "has this customer's risk tier changed" - instead of someone pulling a report by hand. The bank already has a hard rule for this kind of thing: nothing touches the loan database directly. Every system that reads it goes through one gateway that checks who's asking, limits how often they can ask, and logs every request - the same control that exists around any sensitive customer data. The problem is that this rule makes adding a new AI tool slow: today, connecting a new assistant means writing custom, one-off integration code against that gateway, then getting it security-reviewed before anyone can use it. Do that for a desktop assistant, then again for a different agent framework six months later, and again for whatever comes after that - each one is bespoke, each one is a fresh review, and each one is a place someone under deadline pressure could quietly cut a corner around the gateway's safeguards to ship faster.

The technical solution: Wrap the existing gateway in an MCP server - a small translation layer speaking the emerging industry-standard protocol (MCP) for connecting AI assistants to tools and data. Build the safe connection once, and any MCP-compatible client gets it immediately, with no new integration code and no new security review each time. This server exposes three things: Tools to query loans/customers/portfolio and run validated SQL, a Resource exposing the DB schema as readable reference data, and a Prompt template for a common analysis task. The gateway itself is untouched - this project adds a protocol layer in front of it, not a new access path into the data.


Architecture

flowchart LR
    subgraph clients["MCP Clients"]
        CD["MCP Desktop Client\n(this build's demo surface)"]
        AG["Future LangGraph agent\n(same protocol, no new integration)"]
    end

    subgraph mcp["This Project - MCP Server"]
        T["Tools\nget_loan · list_loans · get_customer\nget_customer_loan_history\nget_portfolio_summary · run_validated_query"]
        R["Resource\nlending://schema"]
        P["Prompt\ncustomer_risk_narrative"]
    end

    subgraph gw["lending-data-api (unmodified, external project)"]
        AUTH["X-API-Key auth\n+ per-key rate limiting"]
        SQL["/query/validated\n3-layer SQL safety"]
        SCHEMA["/schema"]
    end

    subgraph db["PostgreSQL"]
        MARTS["dim_customers · dim_customers_history\nfct_loans_type2\n(from scd-type2-lending dbt models)"]
    end

    clients -->|"stdio (MCP protocol)"| mcp
    T -->|"HTTP + X-API-Key"| AUTH
    R -->|"HTTP + X-API-Key"| SCHEMA
    AUTH --> SQL --> MARTS
    SCHEMA --> MARTS

Portfolio chain: scd-type2-lending (dbt SCD2 marts) → lending-data-api (governed gateway) → this MCP server → any MCP client.

Demo - natural-language question answered live via the tools above:

A desktop AI client answering a lending portfolio question through this MCP server


Tools, Resource, and Prompt

Type Name Wraps Notes
Tool get_loan GET /loans/{id} Single loan lookup
Tool list_loans GET /loans/ Filter by status, risk tier, date range
Tool get_customer GET /customers/{id} Current profile
Tool get_customer_loan_history GET /customers/{id}/loan-history Full SCD2 tier history
Tool get_portfolio_summary GET /portfolio/summary Aggregate stats
Tool run_validated_query POST /query/validated Read-only SQL; validated server-side by the gateway
Resource lending://schema GET /schema Table/column reference, read once per session rather than re-fetched as a tool call every time
Prompt customer_risk_narrative - Guided template: pull a customer's tier history + loans, narrate the risk story

No write tool is exposed (POST /loans/ has no MCP equivalent) - this server is read-only by design, discussed below.


Key Design Decisions

1. Wrap the existing gateway instead of connecting to the database directly

The gateway already enforces per-consumer auth, rate limiting, a 3-layer SQL validator (no DDL, no writes for read-only keys, table whitelist), and audit logging. A direct database connection from this server would duplicate that logic in a second place - and second implementations of a safety layer drift out of sync with the first. Every tool call here goes out over HTTP with the same X-API-Key header any other consumer team uses. MCP is a protocol layer on top of a governed path, not a new path.

Trade-off: one extra network hop per call versus a direct DB connection. Worth it - the alternative is two independently-maintained SQL validators.

2. Read-only tool surface, by design

POST /loans/ (loan creation, finance-key only) has no corresponding MCP tool. An AI agent with a create-loan capability is a materially bigger blast radius than one that can only query - and none of this server's target use cases (an analyst asking questions, a Text-to-SQL agent) need to write. If a future use case needs it, it should be a deliberate, separately-reviewed addition, not something that ships by default because the underlying endpoint existed.

3. All three MCP primitives, not just Tools

Most MCP servers are tools-only. lending://schema is deliberately a Resource, not a seventh tool - schema is reference data an agent reads once and holds in context, not an action it repeatedly invokes. customer_risk_narrative is a Prompt - a reusable analysis template, versioned and named, rather than the same instructions retyped into every conversation.

4. Reused API key, not a dedicated consumer profile

This server authenticates with the gateway's existing read-only underwriting key rather than a new dedicated key. The gateway's consumer config only defines three profiles (underwriting, collections, finance) - none scoped to "AI/MCP traffic" specifically. Adding one would mean changing the gateway itself, which this project deliberately leaves untouched. Documented here as the honest state of things, not fixed silently.

Production alternative: add a fourth consumer profile (e.g. mcp_agent) to the gateway's config with its own key and rate limit, isolating MCP traffic from Underwriting's operational queries - the same reasoning that justified giving Underwriting, Collections, and Finance separate keys in the first place.

5. Absolute-path launch, not a relative one

MCP clients (desktop AI assistants, and presumably most others) launch a server by an absolute path to its entry file, with no guarantee about the working directory. A plain from app.lending_client import ... breaks the moment the project root isn't already on sys.path - which is exactly what happens when the client, not a developer's shell, starts the process. The server resolves its own project root from __file__ and inserts it into sys.path before the package-style imports, so it works regardless of how or from where it's launched.


Who Else Has This Problem

Industry Same Pattern
Healthcare Patient record systems already gate access per care team; an MCP wrapper lets a clinical AI assistant query the same governed path instead of a bespoke EHR integration
Insurance Claims data serving fraud, actuarial, and customer service teams - same gateway-plus-protocol shape for an AI claims assistant
HR / HRIS Employee data APIs with strict read/write separation - an internal AI assistant becomes just another consumer, not a new access route
Retail Order/inventory APIs already serving multiple internal systems - same wrapper pattern for a merchandising or demand-forecasting agent

Production Gaps

Gap This project Production alternative
MCP consumer identity Reuses the gateway's underwriting key Dedicated mcp_agent consumer profile with its own key + rate limit
Transport stdio only (desktop AI clients, local agents) Add streamable-http transport for a remote agent (e.g. a hosted Text-to-SQL service) to connect over the network instead of spawning a local subprocess
Secrets API key in a local .env / desktop client config AWS Secrets Manager / Vault, matching the gateway's own production-gap note
Observability No tracing Wire tracing (e.g. Langfuse) around each tool call once this feeds an actual agent, not just a desktop client
SQL validation Delegated entirely to the gateway (by design) No change needed - this is the correct place for it to live

How to Run

1. Start the gateway

In the multi-consumer-ai-data-gateway project (separate repo, not part of this one):

docker compose up --build
docker compose exec api python scripts/seed_db.py

Confirm it's up: curl http://localhost:8000/health

If seeding via docker compose exec fails because scripts/ isn't present in the running container, copy it in first: docker cp scripts/. <container_name>:/app/scripts/, then re-run. If a local Postgres is already listening on port 5432 outside of Docker, seeding from the host can silently write to the wrong database - seed from inside the container to be sure.

2. Set up this project

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # fill in the gateway's base URL + an API key

3. Test it stand-alone (MCP Inspector)

mcp dev app/server.py

Opens an interactive browser UI - call get_portfolio_summary, list_loans, read the lending://schema resource, and try the customer_risk_narrative prompt without needing any AI client.

4. Connect a real MCP client (desktop AI assistant)

Add to your MCP client's config file (most desktop AI assistants use a similar JSON structure):

{
  "mcpServers": {
    "lending-data": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/app/server.py"],
      "env": {
        "LENDING_API_BASE_URL": "http://localhost:8000",
        "LENDING_API_KEY": "your-underwriting-key-here"
      }
    }
  }
}

Restart the desktop client fully (quit, not just close the window - MCP servers are spawned at app startup). Then ask something like "Show me high-risk loans" or "What's the current portfolio summary?"


Sample Output

Three real exchanges from a live session, unedited apart from trimming for length.

1. "Show me high-risk loans" - list_loans(risk_tier="High-Risk")

Here are all 17 loans tagged High-Risk at origination: [17-row table: loan ID, customer, type, principal, origination date, status]

A few things worth noting:

  • Customer 27 has the most exposure - 6 loans, including the one defaulted loan (#104) and two large Home/SMB loans.
  • Customer 40 holds two very large Home loans (~$9.4M and ~$9.1M), both still active.
  • Customer 42 has the largest single loan in this list - an SMB loan for ~$3.5M.
  • This reflects risk tier at origination. If a customer's tier has since changed, their current risk status may differ - I can pull a customer's tier history if you want to check that for anyone specific.

Correctly distinguished "risk tier at origination" from current tier without being asked to - the exact SCD2 nuance get_customer_loan_history exists to resolve - and offered the follow-up instead of assuming.

2. "What's customer 6's risk profile and history? Has anything changed over time?" - get_customer + get_customer_loan_history + get_loan

Here's the full picture for Customer 6 - Arunima Setty (Delhi, member since Dec 5, 2025):

Risk tier history

Tier From To
Standard 2025-12-05 2026-11-10
Premium (current) 2026-11-10 -

So yes - something has changed. Arunima started as Standard risk when she joined and was upgraded to Premium as of Nov 10, 2026 - a positive shift.

Loan portfolio: one loan (#12, $268,695.71, originated 2025-12-12, active) - originated during her Standard tier window, before the Premium upgrade, so it was underwritten at Standard risk, not Premium.

Three separate tool calls chained into one coherent narrative from a single natural-language question, correctly reasoning about when the loan originated relative to the tier change - not just listing both facts side by side.

3. "Give me a risk narrative for customer 15" - the customer_risk_narrative Prompt, not a single tool call

Here's the risk narrative for Customer 15 - Wriddhish Bhardwaj (Pune):

Tier trajectory

  • High-Risk: Jan 8, 2026 – Aug 11, 2026 (initial tier since becoming a member)
  • Standard (current): Aug 11, 2026 – present

This is a customer who has migrated down in risk - moved from High-Risk to Standard within about 7 months of joining, suggesting improved repayment behavior or credit profile.

Loan portfolio (4 loans, all currently active) - total outstanding exposure: ~$2.37M, heavily concentrated in one large SMB loan ($1.73M - about 73% of total exposure).

Key observations:

  • No defaults or closures - all four loans remain active and current, a positive signal despite the High-Risk origination tier.
  • Data flag: all four loans originated between Sep 2024 and May 2025, but the customer's member_since date is Jan 8, 2026 - the same date the tier history begins. These loans predate the tier history on file, so the tier that actually applied at each loan's origination can't be confirmed from this data. Worth checking with the data team if this is a backfill/migration artifact.
  • Concentration risk: the SMB loan dominates total exposure - worth weighing alongside the recent tier improvement, not instead of it.

Bottom line: presents as a High-Risk-to-Standard "improving" borrower with a clean active-loan track record, but a meaningful chunk of exposure sits in one large facility, and there's a data gap around tier-at-origination that's worth resolving before treating the "improving risk" story as fully verified.

The risk-narrative Prompt output, including the agent flagging a data inconsistency between loan origination dates and the customer's tier history

This is the Prompt template doing its job: one canned instruction produced a structured, multi-part risk write-up (trajectory → portfolio → concentration → observations) instead of a one-off free-form answer - the reusable-analysis-template value a Prompt is meant to add over Tools alone. It also caught something worth catching: a genuine data-consistency question (loan dates predating the tier history) that a simpler tool-call response wouldn't have surfaced, instead of accepting the "improving risk" narrative at face value.


Stats

  • 6 Tools, 1 Resource, 1 Prompt
  • 0 write operations exposed - read-only by design
  • 1 external dependency (the gateway) - no direct database access

Recommended Servers

playwright-mcp

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured