mcp-annuities-server
by btholath
README.md
# MCP Annuities Server
**Consolidated & fixed from `MCP-Example-main` + `mcp-prompt-templates-main`**
For: VS Code + Claude Code on WSL/Ubuntu
> š Open `README.html` in a browser for the full documentation with embedded architecture diagrams.
---
## What Was Fixed
| Issue in original repos | Fix applied |
| --------------------------------------------- | ----------------------------------------------------- |
| Hardcoded `C:\Users\Bijut\Desktop\typesdk.md` | `Path(__file__).parent` ā cross-platform |
| Windows paths + `uv` in desktop config | WSL-friendly `.venv/bin/python` absolute paths |
| Two separate repos | Merged into one: 5 tools + 1 resource + 3 prompts |
| Low-level `Server` class (verbose) | FastMCP `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` |
| No automated test | `test_client.py` ā full end-to-end, no IDE needed |
| `realpath` resolves symlink to system Python | Use `$(pwd)/.venv/bin/python` in `claude mcp add` |
**Key rule applied:** tool count kept to **5**. Too many tools breaks LLM tool-selection reliability.
---
## Project Structure
```
mcp-annuities-server/
āāā CLAUDE.md
āāā README.md ā this file
āāā README.html ā full docs with diagrams (open in browser)
āāā requirements.txt
āāā server.py ā MCP server (5 tools + 1 resource + 3 prompts)
āāā test_client.py ā standalone end-to-end test
āāā claude_desktop_config.example.json
āāā .vscode/
ā āāā mcp.json
āāā data/
ā āāā annuities.csv ā 500-row dataset
ā āāā generate_data.py
āāā templates/
āāā annuity_review/ ā config.yaml + template.md
āāā client_summary/
āāā portfolio_report/
```
---
## Setup
```bash
cd ~/mcp-annuities-server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python data/generate_data.py
```
---
## Full Lifecycle ā How a Request Flows
```
[1] User terminal (VS Code / WSL)
ā Natural-language request typed
ā¼
[2] Claude Code reasoning (Sonnet / Opus)
ā Matches request to tool schema
ā Produces tool_use block: {name, arguments}
ā¼
[3] MCP stdio handshake
ā JSON-RPC 2.0 sent over child process stdin
ā Server lives in .venv/bin/python subprocess
ā¼
[4] FastMCP dispatch + validation
ā Validates args against JSON Schema (from type hints)
ā Routes to the matching @mcp.tool() function
ā¼
[5] Tool execution
ā get_client: reads annuities.csv, returns matching row
ā calculate_payout: amortization math, no data lookup
ā¼
[6] data/annuities.csv
ā 500 rows, 15 columns, all values stored as strings
ā
āāāāŗ Result serialized ā stdout ā layers 4ā3ā2ā1 ā user
```
### Tool chaining (your session)
When you said "calculate the payout for this client", Claude Code
chained two tools without any orchestrator script:
```
"Calculate the payout..."
ā Call 1: get_client("CLIENT_0001") [retrieves record]
ā Model extracts current_account_value + rate [type-converts strings]
ā Call 2: calculate_payout(principal, rate, term) [math]
ā Final answer combining both results
```
This is the live minimal version of Domain 1's agentic loop.
---
## Layer-by-Layer Explanations
### Layer 1 ā User terminal
The terminal is just the I/O surface. All orchestration (deciding which
tool to call, building JSON-RPC, managing the subprocess) happens inside
the `claude` process, not in bash. Auth: use Claude Pro login (not API
key) to avoid extra billing. The ANTHROPIC_API_KEY env var set for your
Python lab scripts causes the "both auth methods" warning in Claude Code.
### Layer 2 ā Claude Code reasoning
Tool selection is pure pattern matching against schema descriptions.
"look up CLIENT_0001" matches `get_client` because its description says
"Retrieve full annuity contract details for one client by ID". A vague
description causes wrong picks ā this is the "tool boundary design"
Domain 2 exam concept. The model emits a `tool_use` block, Claude Code
intercepts it, sends to layer 3, and waits for the result before writing
the final answer.
### Layer 3 ā MCP stdio handshake
Server.py is spawned ONCE as a child process at session start (not per
call). Three pipes wired: stdin (requests in), stdout (results out),
stderr (logs ā never corrupts the protocol). JSON-RPC 2.0, newline-
delimited. The `ā Failed to connect` happened because `realpath` resolved
the venv symlink to `/usr/bin/python3.12` (no mcp package) ā `$(pwd)/
.venv/bin/python` keeps the venv path and the handshake succeeds.
### Layer 4 ā FastMCP dispatch + validation
FastMCP's event loop reads each JSON-RPC line, dispatches on `method:
tools/call`, validates `arguments` against the auto-generated JSON Schema
(from type hints) before calling Python, serializes the return value into
a text content block, and writes the response to stdout. No print()
allowed in server.py ā it would corrupt the stream.
### Layer 5 ā Tool execution
`get_client`: linear scan of CSV, returns matching dict (all strings).
`calculate_payout`: amortization formula with zero-rate edge-case handler
and explicit input validation (returns structured error, not exception).
Both are pure functions with no side effects ā safe for an LLM to call.
### Layer 6 ā data/annuities.csv
500 rows, 15 columns (client_id, dob, age, product_type, premium_amount,
crediting_rate_pct, term_years, monthly_payment, surrender_charge_pct,
rider, risk_profile, state, payment_frequency, contract_start_date,
current_account_value). IMPORTANT: csv.DictReader returns ALL values as
strings ā tools doing math must cast with float()/int(). Pydantic models
with typed fields do this automatically (Domain 4 connection).
---
## Step-by-Step: Wire into Claude Code
```bash
# 1. Standalone test (proves server logic, no AI)
.venv/bin/python test_client.py
# 2. MCP Inspector (visual debugging)
npx @modelcontextprotocol/inspector .venv/bin/python server.py
# 3. Register with Claude Code
claude mcp remove annuities-server # if exists
claude mcp add annuities-server -- "$(pwd)/.venv/bin/python" server.py
claude mcp list
# ā annuities-server: .../.venv/bin/python server.py - ā Connected
# 4. Fix auth (if showing Opus / API billing)
claude /logout
# ā choose claude.ai YES, API key NO
# 5. Launch and test
claude
/mcp
"Use the annuities-server to look up CLIENT_0001"
```
---
## Tool / Resource / Prompt Reference
### Tools (5)
| Tool | Purpose | Key args |
| ---------------------- | ------------------ | ------------------------------------------------ |
| `get_client` | Look up one client | `client_id` |
| `search_clients` | Filter portfolio | `product_type`, `risk_profile`, `state`, `limit` |
| `portfolio_summary` | Aggregate stats | `group_by` |
| `calculate_payout` | Amortized payout | `principal`, `annual_rate_pct`, `term_years` |
| `calculate_percentage` | Utility math | `value`, `percentage` |
### Resource
| URI | Content |
| --------------------- | ----------------------- |
| `annuities://dataset` | Raw CSV of all 500 rows |
### Prompts
| Prompt | Purpose | Args |
| ------------------ | ---------------------- | ----------- |
| `annuity_review` | Suitability assessment | `client_id` |
| `client_summary` | Client letter | `client_id` |
| `portfolio_report` | Executive report | `group_by` |
---
## CCAF Exam Domain Mapping
| What you built | Exam concept | Domain |
| -------------------------------------- | ---------------------------------- | ------ |
| `@mcp.tool()` with type hints | Tool schema design | D2 |
| stdio subprocess transport | Local MCP deployment | D2 |
| `claude mcp add` with venv path | MCP client configuration | D2 |
| 5-tool limit | Tool boundary / reasoning overload | D2 |
| `@mcp.resource()` | Resources vs tools distinction | D2 |
| YAML + `@mcp.prompt()` | MCP prompt templates | D2 |
| get_client ā calculate_payout chaining | Implicit task decomposition | D1 |
| CLAUDE.md at project root | CLAUDE.md hierarchy | D3 |
| String CSV ā Pydantic coercion | Structured output enforcement | D4 |
| JSON-RPC id correlation | Context / multi-turn state | D5 |
---
## Quick Reference
```bash
# Golden path
cd ~/mcp-annuities-server && source .venv/bin/activate
python test_client.py # sanity check
claude mcp list # ā Connected
claude # launch
/mcp # verify
"Use annuities-server to look up CLIENT_0001"
# Fix ā Failed to connect
claude mcp remove annuities-server
claude mcp add annuities-server -- "$(pwd)/.venv/bin/python" server.py
# Fix Opus / API billing instead of Pro
claude /logout ā claude.ai YES ā API key NO
```
<-- See all tables with row Last updated: 2026-06-13 -->
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues