country-analytics-mcp
# π country-analytics-mcp
**Ask an AI plain-language questions about world economics β answered live from three public APIs that disagree with each other.**
   
> *"What was France's GDP in 2022 in US dollars?"* Β· *"Convert that to euros at the Dec 30 2022 rate."* Β· *"What's Europe's total GDP β and who did you exclude, and why?"*
GDP, population, GDP per capita, regional totals in your currency, rankings β reconciled **live** (nothing hard-coded, no keys, no accounts) and fully **traceable**: every answer names its countries, years, FX rate, and exclusions.
---
## β‘ Step 1 β Connect it to your AI (60 seconds, nothing to install)
The server is already running on Azure. Paste this URL into your AI client:
```
https://country-analytics-mcp.grayriver-35df865c.eastus.azurecontainerapps.io/mcp
```
### π Claude (claude.ai or Claude Desktop)
1. **Settings β Connectors β Add custom connector**
2. Name: `country-analytics` Β· URL: the one above Β· no authentication
3. **Add**, then start a chat and ask away β Claude picks the tools itself.
### π’ ChatGPT
1. **Settings β Apps & Connectors β Advanced settings** β enable **Developer mode**
2. Back in **Connectors** β **Create**: name `country-analytics`, MCP server URL as above, Authentication: **None** β save
3. In a new chat, enable the connector (via the composer's tools / Developer mode menu) and ask your question.
*(Custom connectors require a paid plan on both platforms; exact menu names shift occasionally.)*
### π No paid plan? MCP Inspector works too
```bash
npx @modelcontextprotocol/inspector
```
Transport **Streamable HTTP** β paste the URL β **Connect** β call the 8 tools from a form UI.
> π€ The app scales to zero when idle β the *first* request after a quiet spell takes ~10 extra seconds to cold-start. Health check: [`/health`](https://country-analytics-mcp.grayriver-35df865c.eastus.azurecontainerapps.io/health)
## π¬ Step 2 β Things worth asking
| Ask thisβ¦ | β¦and notice |
|---|---|
| *How many countries does the source list for Europe?* | 53 β and 5 of them have no World Bank data (it says which) |
| *France's GDP in 2022, in USD?* | exact World Bank figure, year pinned |
| *Now in euros, at the Dec 30 2022 rate* | conversion pinned to the ECB rate, date echoed back |
| *Europe's total GDP in euros?* | the answer states its scope, FX date, and every excluded country |
| *Same, but using the World Bank's definition of Europe* | a **different set** (Central Asia in, Malta out β yes, really) |
| *Top 5 European countries by GDP per capita* | rankings with the year each value actually came from |
| *What couldn't you reconcile for Europe?* | the full flagged-issues ledger |
---
## π» Run it locally (clean checkout β connected client)
No hosted dependency required β the same server runs on your machine with Python 3.10+:
```bash
git clone https://github.com/Satyampatel779/country-analytics-mcp.git
cd country-analytics-mcp
python -m venv .venv
.venv\Scripts\pip install . # macOS/Linux: .venv/bin/pip install .
```
| Transport | Command |
|---|---|
| **stdio** (Claude Desktop, Inspector) | `.venv\Scripts\python -m country_analytics.server` |
| **HTTP** at `http://127.0.0.1:8000/mcp` | `set MCP_TRANSPORT=http` then the same command (`export` on macOS/Linux) |
| **Docker** (identical to the hosted image) | `docker build -t cam . && docker run -p 8000:8000 cam` |
**Claude Desktop (stdio):** merge [client-configs/claude_desktop_config.json](client-configs/claude_desktop_config.json) into your `claude_desktop_config.json` (Settings β Developer β Edit Config), fix the absolute path, restart:
```json
{
"mcpServers": {
"country-analytics": {
"command": "C:\\path\\to\\country-analytics-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "country_analytics.server"]
}
}
}
```
**MCP Inspector (stdio or HTTP):** step-by-step in [client-configs/mcp_inspector.md](client-configs/mcp_inspector.md).
### β
Prove it works
```bash
.venv\Scripts\pip install ".[dev]"
.venv\Scripts\python -m pytest -q # 11 live tests, both MCP transports
```
Expected values are cross-checked against **independent direct fetches** of the same live APIs β not constants.
---
## π§ What's under the hood
Three keyless public services, each speaking a different protocol, joined on **ISO2 codes** (never names β the sources spell them differently):
| Source | Protocol | Provides |
|---|---|---|
| [Countries GraphQL](https://countries.trevorblades.com/) | GraphQL | Which countries are on a continent (ISO2, name, currency) |
| [World Bank Indicators](https://api.worldbank.org/v2) | REST, `[metadata, data]` arrays | GDP (`NY.GDP.MKTP.CD`, current US$) & population (`SP.POP.TOTL`) by year |
| [Frankfurter](https://api.frankfurter.dev/v1) | REST | ECB FX rates, latest or pinned to a date |
### The 8 tools
| Tool | Answers |
|---|---|
| `list_continents()` | continent codes + country counts |
| `list_countries(continent)` | the full source list with join status per country |
| `get_country_metric(country, metric, year, currency, fx_date)` | one country, one number, fully traced |
| `region_total(continent, metric, year, currency, scope, fx_date, missing_policy)` | regional totals β every assumption a parameter |
| `rank_countries(continent, metric, year, top_n, currency, scope, order)` | top-N / bottom-N |
| `convert_currency(amount, from, to, date)` | pinned or latest FX conversion |
| `list_reconciliation_issues(continent)` | everything excluded, unmatched, or flagged β with reasons |
| `describe_methodology()` | join key, defaults, and which parameters change them |
### Judgment calls are parameters, not hard-coded
- **`scope`** β geographic continent (default) vs. the World Bank region: for Europe the WB set adds Central Asia and *drops Malta* (the World Bank files it under MENA β flagged, of course).
- **`year`** β pinned exactly, or `latest` walks back past null years *per country* and reports the year used.
- **`missing_policy`** β exclude-and-list (default) or walk back to the most recent earlier value.
- **`fx_date`** β defaults to the pinned year's last trading day (2022 β 2022-12-30); pin any date you like.
- **Refuses to guess** β currencies outside Frankfurter's 30 (Serbian dinar, hryvniaβ¦) return the supported list instead of an invented rate.
## π More
- **[WRITEUP.md](WRITEUP.md)** β sources & join choices, every disagreement found, every assumption, production notes (β€1 page, written for a non-technical stakeholder).
- **[samples/sample_run_output.md](samples/sample_run_output.md)** β a real timestamped run: the reconciled Europe table, computed metrics, and the flagged-issues list, kept as a reference in case a source is down during evaluation.
TDQS
Scored across 8 tools
Tools are mostly distinct: list_continents, list_countries, and get_country_metric target different granularities, while region_total and rank_countries differ by aggregation vs ranking. However, both region_total and rank_countries share identical scope/year/currency semantics and could be confused for aggregate queries, and get_country_metric is buried despite its importance.
All tool names follow a clear verb_noun pattern (list_continents, get_country_metric, region_total, convert_currency, describe_methodology). The only slight deviation is region_total and rank_countries which omit an explicit verb, but they remain readable and consistent within the set.
Eight tools is well-scoped for a country analytics server: listing entities, fetching metrics, aggregating, ranking, converting currency, and explaining reconciliation/methodology. Each tool earns its place without redundancy.
The surface covers the full lifecycle: discovery (continents, countries), retrieval (get_country_metric), aggregation (region_total), ranking (rank_countries), currency conversion, and transparency tools (list_reconciliation_issues, describe_methodology). No obvious gaps for the stated domain.