Skip to main content
Glama
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 -->