Skip to main content
Glama
moha-55

Continent Economics

by moha-55
README.md
# Continent Economics - MCP Server

An MCP server that answers quantitative questions about the economies of any
continent, reconciled **live** from three public APIs that do not agree with
each other.

```
Countries GraphQL ──┐
                    ├──► reconcile (join on ISO codes, flag what fails) ──► MCP tools
World Bank ─────────┤
Frankfurter FX ─────┘
```

No API keys. No accounts. No hosted dependency. Everything is fetched at query
time from the live services - there are no cached or hardcoded figures in this
repository.

---

## Quickstart

Requires **Python 3.10+** (developed on 3.12) and an internet connection.

```bash
python -m venv .venv
```

Activate it:

```bash
# macOS / Linux
source .venv/bin/activate
```

```powershell
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
```

Install and verify:

```bash
pip install -r requirements.txt
```

```bash
python -m scripts.smoke_test
```

`smoke_test` calls all three live APIs and checks known GDP, FX and
per-capita values plus the reconciliation behaviour (country count, weekend FX
snapping, unsupported-currency refusal, name-mismatch join). If every check
passes - it prints `N/N checks passed` with no `FAIL` lines and exits 0 - the
server will work.

Optionally, run the offline tests (no network needed; covers parameter
validation, the HTTP retry policy, and ranking provenance):

```bash
python -m unittest discover -s tests
```

Start the server:

```bash
python -m src.server
```

It speaks MCP over **stdio** and will appear to hang - that is correct. It is
waiting for a client on stdin. Connect one of the clients below.

---

## Connect a client

### Option A - MCP Inspector (fastest, no config file)

In a separate terminal, from the project root. **Point it at the venv's Python
explicitly** - `npx` does not inherit an activated venv reliably, and a bare
`python` will start an interpreter that has no `mcp` installed, so the server
exits immediately and Connect fails:

```bash
npx @modelcontextprotocol/inspector .venv/Scripts/python.exe -m src.server
```

```bash
# macOS / Linux
npx @modelcontextprotocol/inspector .venv/bin/python -m src.server
```

Open the URL it prints (usually `http://localhost:6274`), click **Connect**,
then **List Tools**. Pick `country_metric`, set `country=France`, `year=2022`,
`currency=USD`, and click **Run Tool**.

### Option B - Claude Desktop

Edit the config file:

- **macOS** - `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows** - `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "continent-economics": {
      "command": "/ABSOLUTE/PATH/TO/project/.venv/bin/python",
      "args": ["-m", "src.server"],
      "cwd": "/ABSOLUTE/PATH/TO/project"
    }
  }
}
```

On Windows use `"command": "C:\\ABSOLUTE\\PATH\\TO\\project\\.venv\\Scripts\\python.exe"`
with escaped backslashes.

Replace both paths with the real location of this folder, then **fully quit and
reopen Claude Desktop**. The tools appear under the tools icon in the chat box.

> Both paths must be absolute, and `cwd` must be the project root so that
> `-m src.server` resolves. There are no secrets in this config.

### Option C - Claude Code

```bash
claude mcp add continent-economics -- /ABSOLUTE/PATH/TO/project/.venv/bin/python -m src.server
```

### Option D - any other client

Command `python`, args `["-m", "src.server"]`, working directory = project root,
transport **stdio**.

---

## Questions it answers

Ask a connected AI client these in plain language:

**Exactly one correct answer**

- *"What was France's GDP in 2022 in US dollars?"* → `2,794,788,137,066.94`
- *"Convert that to euros using the 30 December 2022 rate."* → `2,620,281,565,788.48` (rate `0.93756`)
- *"What was France's GDP per capita in 2022?"* → `40,988.64` USD
- *"How many countries does the source list for Europe?"* → `44` sovereign, `48` including territories, `58` under the World Bank region
- *"What was Czechia's population in 2022?"* → `10,672,118` (the sources call it *Czechia* and *Czech Republic*; the join uses ISO codes)

**Matters of judgment** - the server answers with its assumption attached

- *"What is Europe's total GDP in euros?"* → **€22.88tn** by default (44 sovereign states, FX 2022‑12‑31→2022‑12‑30), and it will tell you that `all_listed` gives €22.90tn and `worldbank_region` gives €24.24tn.
- *"Which 5 European countries have the highest GDP per capita?"*
- *"Top 10 Asian economies by GDP in euros."*
- *"What did you exclude, and why?"*

Full live output is in [`samples/sample_run.md`](samples/sample_run.md).

---

## Tools

| Tool | Purpose |
|---|---|
| `list_continents` | Continent codes and the meaning of every policy parameter |
| `list_countries` | The countries counted for a continent, and how many |
| `country_metric` | GDP / population / GDP per capita for one country |
| `region_total` | Continent-wide GDP or population total, any currency |
| `rank_countries` | Top-N or bottom-N by a chosen metric |
| `fx_convert` | Currency conversion at a pinned historical date |
| `fx_supported_currencies` | What the FX source actually covers |
| `list_reconciliation_issues` | Everything excluded or flagged, with reasons |
| `describe_methodology` | Sources, join key, and every assumption |

Every judgment call is a parameter, not a hardcoded choice:

| Parameter | Default | Options |
|---|---|---|
| `include` | `sovereign` | `sovereign` (UN members) · `all_listed` (+ territories) · `worldbank_region` (World Bank grouping) |
| `missing_policy` | `skip` | `skip` (exclude and report) · `walk_back` (nearest earlier year) · `error` |
| `year` | `2022` | any year, or `"latest"` |
| `currency` | `USD` | ~30 ECB currencies |
| `fx_date` | 31 Dec of `year` (latest rate for `"latest"` or a year not yet over) | any `YYYY-MM-DD` from 1999-01-04 |

Works for **any** continent: `EU`, `AS`, `AF`, `NA`, `SA`, `OC`, `AN`.

---

## Assumptions in brief

- **Join on ISO 3166-1 alpha-2**, never on names - the sources disagree on
  *Czechia*/*Czech Republic*, *Russia*/*Russian Federation*,
  *Slovakia*/*Slovak Republic*, and a name join would silently drop Russia from
  every European total.
- **78 World Bank "Aggregates" rows are always excluded** - *World*, *Euro
  area*, *High income* are not countries. Counting them inflates a world total
  roughly **7.5×**.
- **"Europe" defaults to the 44 UN member states.** Two other definitions are
  one parameter away.
- **Missing years are never zero-filled** - the country is dropped from the
  total and named in the response.
- **FX defaults to 31 December of the requested year**, snapping back to the
  last business day - the same rule in every tool. `latest`, or a year that
  has not ended, uses the most recent published rate instead of a future date.
  The response always reports the date actually used and why.

Full reasoning, including production concerns and how this would point at real
internal systems, is in **[WRITEUP.md](WRITEUP.md)**.

---

## Repository layout

```
src/sources.py      three API clients: timeouts, retries, caching
src/reconcile.py    the join and the issue list   ← the core of the project
src/analytics.py    all arithmetic; provenance on every result
src/validation.py   parameter checks with errors that name the valid options
src/server.py       MCP tool definitions
scripts/smoke_test.py         live end-to-end checks
scripts/generate_samples.py   regenerates samples/ from a live run
tests/test_offline.py         offline tests: validation, retries, provenance
samples/            reference output from an actual run
```

## Regenerating the samples

```bash
python -m scripts.generate_samples
```

## Troubleshooting

| Symptom | Cause |
|---|---|
| Server "hangs" on start | Correct - stdio servers wait for a client. |
| **Inspector "Connect" fails** | The command must point at the **venv's** Python (`.venv/Scripts/python.exe` on Windows, `.venv/bin/python` elsewhere). A bare `python` starts an interpreter without `mcp`, so the server exits before the client can connect. Check the Inspector's stderr pane for `ModuleNotFoundError: No module named 'mcp'`. |
| Claude Desktop shows no tools | Paths must be absolute; `cwd` must be the project root; fully quit and reopen the app. |
| `ModuleNotFoundError: src` | Run from the project root, or set `cwd` in the client config. |
| `ModuleNotFoundError: mcp` | You are running the system Python, not the venv's. See above. |
| `SourceError: ... failed after 3 attempts` | An upstream API is down. Each call is attempted three times (backoff, or the server's `Retry-After` on a 429); the tools return the error as data rather than crashing. |
| `SourceError: ... is rate-limiting requests` | The API asked for a wait longer than 30s. The server fails fast rather than blocking the client; try again later. |
| `ValidationError: ...` | A parameter was invalid (e.g. `top_n=0`, unknown `metric`). The message lists the valid options. |
| Slow first call | Cold cache - the World Bank country table (295 rows) is fetched once, then cached for 24h. |