Loan Server
БесплатноНе проверенA production-grade MCP server for a Loan Management System backed by Microsoft Dataverse, exposing read and analytics tools for loan applications.
Описание
A production-grade MCP server for a Loan Management System backed by Microsoft Dataverse, exposing read and analytics tools for loan applications.
README
A production-grade Model Context Protocol (MCP) server for a Loan Management System backed by Microsoft Dataverse. It exposes read/analytics tools that MCP clients (Claude Desktop, Copilot Studio, etc.) can call to look up and report on loan applications.
- Transports: stdio (for Claude Desktop) and Streamable HTTP (for
Copilot Studio / remote clients) — same server, same tools, JSON-RPC 2.0
(official
@modelcontextprotocol/sdk) - Auth: Microsoft Entra ID Client Credentials flow (
@azure/msal-node) - Data: Dataverse Web API v9.2 directly (no Power Automate, no connectors)
- Language: TypeScript (strict) on Node.js 22+
Table of contents
- Tools
- Architecture
- Authentication flow
- Dataverse integration
- Prerequisites
- Setup
- Environment variables
- Running locally
- Testing
- Connect to Claude Desktop
- Connect to Copilot Studio
- Example requests & responses
- Error handling
- Troubleshooting
Tools
All tools return business-friendly field names — Dataverse logical names are never exposed.
Customer tools
| Tool | Input | Returns |
|---|---|---|
GetLoanSummary |
referenceNumber |
Full loan summary (applicant, amounts, status, officer, …) |
GetLoanStatus |
referenceNumber |
Status, eligibility, assigned officer, review-required |
SearchLoansByPhone |
phoneNumber |
All loans for a phone number |
GetApplicantHistory |
applicantEmail |
All applications for an applicant |
TrackLoanTimeline |
referenceNumber |
Chronological milestones (created, eligibility, officer, …) |
ExplainEligibility |
referenceNumber |
Eligibility status/remarks + business-friendly explanation |
GetRequiredDocuments |
loanType |
Required document checklist for the loan type |
Internal tools
| Tool | Input | Returns |
|---|---|---|
ListPendingReviews |
— | Loans currently Under Review |
GetOfficerWorkload |
officerName |
Assigned loans, pending reviews, status breakdown |
SearchLoansByStatus |
status |
All loans matching a status label |
SearchLoansByOfficer |
officerName |
All loans assigned to an officer |
GetLoanAnalytics |
— | Totals by status; average/highest/lowest loan amount |
Architecture
┌──────────────────────────────┐
MCP client │ index.ts (stdio) │
(Claude / Copilot) ◄───►│ JSON-RPC 2.0 over stdio │
└───────────────┬──────────────┘
│
┌───────────────▼──────────────┐
│ server.ts │ composition root (DI)
│ wires config → auth → │
│ service → tool registry │
└───────┬───────────────┬───────┘
│ │
┌───────────────▼──┐ ┌──────▼──────────────┐
│ tools/* (12) │ │ auth/auth.ts │
│ defineTool() │ │ Entra ID tokens │
│ schemas (zod) │ │ (MSAL, cached) │
└───────┬──────────┘ └──────┬──────────────┘
│ │ bearer token
┌───────▼───────────────────────▼──────────────┐
│ services/dataverseService.ts │
│ executeQuery / finders / choice metadata │
│ Axios (reused) · retry-on-401 · pagination │
└───────┬──────────────────────────────────────┘
│ raw records (logical names)
┌───────▼──────────┐ ┌────────────────────┐
│ models/loan.ts │ │ errors/index.ts │
│ mappers + │ │ AppError + typed │
│ domain logic │ │ subclasses │
└──────────────────┘ └────────────────────┘
config/ (env · dataverse names · tool names · documents) utils/logger.ts (pino → stderr)
Design principles
- Single source of truth for logical names —
config/dataverse.tsholds every table/column logical name;models/loan.tsmappers are the only translation point to business fields. - Repository/service pattern —
DataverseServiceowns all HTTP; tools never build requests.executeQuery()is the single list primitive (with pagination); finders (findLoanByReference,findLoansByPhone, …) compose it. - Open/closed tools — each tool is a declarative
defineTool({...}); the wrapper adds timing, logging and error mapping. Add a tool by listing it intools/registry.ts. - Dependency injection —
createServer()wires everything and accepts overrides for testing. - Typed errors —
AppErrorsubclasses (AuthenticationError,DataverseError,ValidationError,LoanNotFoundError) → clean MCP results. - stdout is sacred — it carries only JSON-RPC; all logs go to stderr.
Project structure
src/
├── index.ts # Entrypoint: stdio transport + lifecycle
├── http.ts # Entrypoint: Streamable HTTP transport + sessions
├── server.ts # Composition root (DI) — shared by both entrypoints
├── auth/
│ └── auth.ts # EntraAuthProvider (MSAL, cached tokens)
├── config/
│ ├── index.ts # Centralized appConfig (validated once)
│ ├── env.ts # Env schema + parsing (the only process.env reader)
│ ├── dataverse.ts # Table/column logical names, statuses, select set
│ ├── tools.ts # Canonical tool names
│ └── documents.ts # Loan-type → required documents mapping
├── errors/
│ └── index.ts # AppError hierarchy + codes
├── models/
│ └── loan.ts # Loan + projections, mappers, domain functions
├── services/
│ └── dataverseService.ts # Reusable Web API client
├── tools/
│ ├── shared.ts # defineTool wrapper + result helpers
│ ├── schemas.ts # Reusable Zod input fields
│ ├── registry.ts # List of all tool registrars
│ └── *.ts # 12 tool modules
└── utils/
└── logger.ts # pino logger (stderr only)
Authentication flow
EntraAuthProvider.getAccessToken()
│
├─ valid cached token (not within 60s of expiry)? ── yes ─► return it
│
└─ no ─► MSAL acquireTokenByClientCredential(scope = <DATAVERSE_URL>/.default)
│ (concurrent callers coalesce onto one in-flight request)
└─► cache token + expiry ─► return it
Every Dataverse request: Axios request interceptor attaches "Authorization: Bearer <token>".
On HTTP 401: response interceptor forces one refresh + retries the request once.
- Uses the OAuth 2.0 client-credentials grant (app-only, no user).
- Tokens are cached in-memory and reused until ~1 minute before expiry.
- Secrets are read from the environment only and are redacted from logs.
Dataverse integration
- Talks to the Web API directly:
https://<org>.crm.dynamics.com/api/data/v9.2/. - Reads use
$select(business columns),$expand(officer lookup → name),$filter(escaped OData literals) and follow@odata.nextLinkpagination. - Choice/Picklist columns (
status,eligibilityStatus,loanType) return integers; labels come from the formatted-value annotation. Status filtering resolves a label to its option value via cached option-set metadata. - The officer lookup is resolved to a display name via
$expandof the navigation property (defaults tosystemuser→fullname).
Naming note (schema vs logical): the names in the maker portal (
cr174_ReferenceNumber) are schema names. The Web API uses logical names, which are always lowercase (cr174_referencenumber). All logical names live inconfig/dataverse.ts.
Prerequisites
- Node.js 22+ and npm.
- A Microsoft Dataverse environment (e.g.
https://yourorg.crm.dynamics.com). - A loan applications table (
cr174_loanapplics) with the documented columns. - Permission to create an Entra ID App Registration and a Dataverse Application User.
Setup
1. Azure App Registration
- Entra ID portal → App registrations → New registration
(
loan-mcp-server, single tenant). - Copy the Application (client) ID and Directory (tenant) ID.
- Certificates & secrets → New client secret → copy the secret value.
Client-credentials needs no redirect URI or delegated permissions. Access is granted via the Dataverse Application User below.
2. Application User in Dataverse
- Power Platform Admin Center → environment → Settings → Users + permissions → Application users → New app user.
- Add the app by its Application (client) ID.
- Assign a security role with Read on the loan table (and read on the officer lookup's target table, e.g. User, so the officer name resolves).
3. Required API permissions
The token is scoped to https://yourorg.crm.dynamics.com/.default. No
admin-consented Graph permissions are required — the Application User +
security role authorize the app. Ensure the role grants at least:
- Read on
cr174_loanapplics - Read on
systemuser(to resolve officer names) - Read access to entity metadata (default for authenticated app users) so status option-set values can be resolved.
4. Environment variables
Copy .env.example to .env and fill in the four required values.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
TENANT_ID |
✅ | – | Directory (tenant) ID |
CLIENT_ID |
✅ | – | Application (client) ID |
CLIENT_SECRET |
✅ | – | Client secret value |
DATAVERSE_URL |
✅ | – | https://yourorg.crm.dynamics.com (no trailing /) |
DATAVERSE_API_VERSION |
– | v9.2 |
Web API version |
DATAVERSE_LOAN_TABLE |
– | cr174_loanapplics |
Loan entity set name (data paths) |
DATAVERSE_LOAN_ENTITY |
– | cr174_loanapplic |
Loan entity logical name (metadata paths) |
OFFICER_NAV_PROPERTY |
– | cr174_OfficerAssigned |
Officer lookup navigation property to $expand |
OFFICER_NAME_FIELD |
– | fullname |
Name column on the lookup's target table |
OFFICER_ENTITY_SET |
– | systemusers |
Officer target entity set (for getOfficer) |
OFFICER_EMAIL_FIELD |
– | internalemailaddress |
Officer email column (for getOfficer) |
HTTP_TIMEOUT_MS |
– | 30000 |
Dataverse request timeout (ms) |
LOG_LEVEL |
– | info |
trace…fatal/silent |
HTTP_PORT |
– | 3000 |
HTTP transport port (start:http) |
HTTP_HOST |
– | 127.0.0.1 |
HTTP bind host (0.0.0.0 when hosted behind a proxy) |
MCP_HTTP_PATH |
– | /mcp |
HTTP MCP endpoint path |
MCP_API_KEY |
– | (unset) | Shared secret for the HTTP endpoint (strongly advised) |
MCP_CORS_ORIGIN |
– | * |
CORS allow-origin for the HTTP transport |
Configuration is validated with Zod at startup; invalid/missing values fail fast with an actionable message.
Running locally
npm install # install dependencies
npm run build # compile TypeScript to dist/
# Streamable HTTP transport (default `start`; for Copilot Studio / remote / Azure)
npm start # node dist/http.js -> listens on HTTP_PORT (default 3000)
npm run dev:http # from source with reload
# stdio transport (Claude Desktop launches this as a child process)
npm run start:stdio # node dist/index.js
npm run dev # from source with reload
npm run typecheck # type-check without emitting
npm startruns the HTTP server so cloud hosts (Azure App Service) that invokenpm startwork with no custom startup command. Claude Desktop launches the stdio entrypoint (dist/index.js) directly, so this doesn't affect it.
Transports
| Transport | Entrypoint | Command | Use with |
|---|---|---|---|
| Streamable HTTP | src/http.ts |
npm start |
Copilot Studio / remote / Azure |
| stdio | src/index.ts |
npm run start:stdio |
Claude Desktop (local) |
Both expose the identical server, auth, service and 12 tools — only the
transport differs. The stdio server has no port (an MCP client launches it
and talks over stdin/stdout; stdout is reserved for JSON-RPC). The HTTP
server listens on HTTP_PORT and serves:
POST {MCP_HTTP_PATH}— client→server messages (initializestarts a session)GET {MCP_HTTP_PATH}— server→client SSE stream for a sessionDELETE {MCP_HTTP_PATH}— terminate a sessionGET /healthz— liveness probe (unauthenticated)
Sessions are tracked via the mcp-session-id header; the token cache and Axios
instance are shared across sessions. Protect the HTTP endpoint with
MCP_API_KEY (sent as x-api-key or Authorization: Bearer) — if unset, the
endpoint is unauthenticated and a warning is logged.
PowerShell note (Windows): if
npmis blocked by execution policy, runSet-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned, or callnpm.cmd.
Testing
An automated MCP client drives every tool against your real environment (reads
.env):
npm run build
npm test # or: npm run test:client
It performs the JSON-RPC handshake, asserts all 12 tools are advertised, calls
each one and checks response shapes, and verifies the error paths
(LOAN_NOT_FOUND, VALIDATION_ERROR, invalid input). A batch reporter is also
provided:
npm run test:batch # summary table for a set of references
node scripts/batch-test.mjs LN-XXXX LN-YYYY
Sample inputs can be overridden via env: TEST_REF, TEST_PHONE, TEST_EMAIL,
TEST_OFFICER, TEST_STATUS, TEST_LOAN_TYPE.
Connect to Claude Desktop
Edit the config file:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"loan-management": {
"command": "node",
"args": ["C:\\Changes\\Innorve\\loan-mcp-server\\dist\\index.js"]
}
}
}
Run npm run build first, then fully restart Claude Desktop. The server reads
.env from its own folder; alternatively pass secrets via an "env": { … }
block in the config. All 12 tools then appear in the tools menu.
Connect to Copilot Studio
Copilot Studio is cloud-hosted and connects to MCP servers over HTTP (not stdio), so use the Streamable HTTP transport and a reachable URL.
- Run the HTTP transport with an API key:
npm run build # set MCP_API_KEY (and HTTP_HOST=0.0.0.0 if behind a proxy) in .env, then: npm run start:http - Expose it with a public HTTPS URL so Copilot Studio's cloud can reach it:
- Testing: a tunnel to
http://localhost:3000(VS Code dev tunnels, ngrok, …). - Production: host on Azure App Service / Container Apps and use its HTTPS URL.
- Testing: a tunnel to
- Register it in Copilot Studio: your agent → Tools → Add tool →
Model Context Protocol, and point it at
https://<your-host>/mcp. Supply theMCP_API_KEYas thex-api-keyheader (orAuthorization: Bearer). - The server advertises all tools via
tools/list, so Copilot Studio discovers them automatically. The tool contract is identical to the Claude Desktop integration.
Security: the HTTP endpoint calls your loan data. Always set
MCP_API_KEY, serve over HTTPS (via the tunnel/host), and restrictMCP_CORS_ORIGINin production.
Verify the HTTP transport locally at any time:
npm run start:http # in one terminal
MCP_API_KEY=... npm run test:http # in another (drives it as a real MCP client)
Example MCP requests & responses
Call GetLoanSummary
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "GetLoanSummary",
"arguments": { "referenceNumber": "LN-20260708090758" }
}
}
Response (success) — structuredContent plus a text mirror:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{ ...json... }" }],
"structuredContent": {
"referenceNumber": "LN-20260708090758",
"applicantName": "Yogeshwara B",
"applicantEmail": "[email protected]",
"phoneNumber": "8197792301",
"loanAmount": 15000000,
"propertyValue": 0,
"loanType": "Personal Loan",
"status": "Under Review",
"eligibilityStatus": "Needs Manual Review",
"eligibilityRemarks": "Loan amount exceeds automatic approval threshold.",
"reviewRequired": true,
"assignedOfficer": "Akshitha S",
"createdDate": "2026-07-08T09:07:57Z",
"documentsUploaded": false
}
}
}
GetLoanAnalytics (no arguments) →
{
"totalApplications": 71,
"pending": 10, "approved": 10, "rejected": 4,
"underReview": 14, "received": 29,
"averageLoanAmount": 9625394.68,
"highestLoanAmount": 53214334,
"lowestLoanAmount": 33
}
GetOfficerWorkload { "officerName": "Akshitha S" } →
{
"officerName": "Akshitha S",
"totalWorkload": 3,
"pendingReviews": 3,
"byStatus": { "received": 0, "pending": 0, "underReview": 3, "approved": 0, "rejected": 0, "other": 0 },
"loans": [ { "referenceNumber": "LN-...", "status": "Under Review", "loanAmount": 50000000, "reviewRequired": true } ]
}
Error result (loan not found) —
{
"result": {
"isError": true,
"content": [{ "type": "text", "text": "{ ...json... }" }],
"structuredContent": {
"error": "No loan found with reference number 'LN-000'.",
"code": "LOAN_NOT_FOUND",
"retryable": false,
"httpStatus": 404
}
}
}
Error handling
Every failure is a typed AppError mapped to a structured MCP result:
| Code | Thrown as | Meaning |
|---|---|---|
VALIDATION_ERROR |
ValidationError |
Bad input / unknown status label |
LOAN_NOT_FOUND |
LoanNotFoundError |
No record matched |
AUTHENTICATION_FAILED |
AuthenticationError |
Could not acquire an Entra ID token |
UNAUTHORIZED |
AuthenticationError |
Dataverse rejected the token (401) |
FORBIDDEN |
DataverseError |
Insufficient Dataverse privileges (403) |
BAD_REQUEST |
DataverseError |
Malformed query (400) — check table/column config |
DATAVERSE_UNAVAILABLE |
DataverseError |
Dataverse 5xx / unavailable (retryable) |
NETWORK_ERROR |
DataverseError |
Timeout / network failure (retryable) |
UNKNOWN |
DataverseError |
Unclassified error |
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
BAD_REQUEST / "Could not find a property named …" |
A column logical name is wrong. Logical names are lowercase; verify against config/dataverse.ts. |
AUTHENTICATION_FAILED |
Check TENANT_ID, CLIENT_ID, CLIENT_SECRET (secret value, not ID). |
UNAUTHORIZED (401) after auth succeeded |
Token rejected — the Application User may be missing or disabled in Dataverse. |
FORBIDDEN (403) |
The Application User's security role lacks Read on the table (or on systemuser for officer names). |
| Officer shows blank | No officer assigned, or OFFICER_NAV_PROPERTY / OFFICER_NAME_FIELD don't match the lookup's target. |
VALIDATION_ERROR from SearchLoansByStatus |
The status label doesn't exist; the error lists valid labels (from option-set metadata). |
npm blocked on Windows (running scripts is disabled) |
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned, or use npm.cmd. |
| Client sees no output / server "hangs" | Expected — it's a stdio server waiting for JSON-RPC. Launch it from an MCP client, not a bare terminal. |
License
MIT
Установить Loan Server в Claude Desktop, Claude Code, Cursor
unyly install loan-mcp-serverСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add loan-mcp-server -- npx -y github:Yogeshwara7/loan-mcp-serverFAQ
Loan Server MCP бесплатный?
Да, Loan Server MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Loan Server?
Нет, Loan Server работает без API-ключей и переменных окружения.
Loan Server — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Loan Server в Claude Desktop, Claude Code или Cursor?
Открой Loan Server на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectCompare Loan Server with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
