loading…
Search for a command to run...
loading…
Exposes World Bank development data to AI agents via local CSV resources and live API tools, enabling queries about GDP, population, and other indicators.
Exposes World Bank development data to AI agents via local CSV resources and live API tools, enabling queries about GDP, population, and other indicators.
Build an MCP (Model Context Protocol) server that exposes World Bank development data to AI agents like Claude.
By completing this lab, you will:
You will build an MCP server called world-bank-server that exposes:
| Part | Type | Description |
|---|---|---|
| Part 1 | Resources | Read local World Bank indicator data from a CSV file |
| Part 2 | Tools | Fetch live data from REST Countries and World Bank APIs |
When complete, an AI agent can ask questions like:
This lab intentionally uses both local data (Resources) and API calls (Tools) to teach an important MCP design pattern:
| Resources (Local CSV) | Tools (API Calls) | |
|---|---|---|
| Data type | Historical indicators (2000-2023) | Current country metadata |
| Example | "GDP of USA in 2015" | "What's the capital of USA?" |
Why not just call the API every time?
Historical data doesn't change - USA's GDP in 2015 is fixed forever. There's no reason to fetch it from an API repeatedly.
Performance - Local file reads are ~1ms. API calls are ~200-500ms. For bulk queries across 200 countries, that's the difference between instant and waiting 2 minutes.
Reliability - APIs can fail, have rate limits, or go down. Local data is always available.
Cost at scale - Many APIs charge per request or have rate limits. In production, you'd pay for every unnecessary API call.
Offline capability - Local resources work without internet. Your agent can still answer historical questions on an airplane.
Data consistency - You control the exact dataset version. APIs might update data or change formats unexpectedly.
The real-world pattern:
Cache what you can (Resources), fetch what you must (Tools)
In production MCP servers, you typically:
This is why we split the lab this way - it mirrors how you'd actually build a production MCP server.
uv package manager installedlab-week4-mcp-server/
├── data/
│ └── world_bank_indicators.csv # PROVIDED - World Bank data (do not modify)
├── server.py # STARTER CODE - implement the TODOs
├── test_client.py # PROVIDED - tests your server
├── pyproject.toml # PROVIDED - dependencies
├── .mcp.json # OPTIONAL - Claude Code config
└── README.md # YOU WRITE - document your implementation
Clone the repository (via GitHub Classroom)
Install dependencies
uv sync
Verify the data file exists
ls data/world_bank_indicators.csv
Resources expose read-only data from the local CSV file. Implement these three resources in server.py:
data://schemaReturn the column names and data types of the dataset.
@mcp.resource("data://schema")
def get_schema() -> str:
"""Return the schema of the World Bank dataset."""
# Already implemented as an example
data://countriesReturn a list of all unique countries in the dataset.
@mcp.resource("data://countries")
def get_countries() -> str:
"""List all unique countries in the dataset."""
df = _load_data()
# TODO: Return unique country codes and names as JSON string
# Hint: Use df.select() and df.unique() then df.write_json()
Expected output format:
[
{"countryiso3code": "USA", "country": {"value": "United States"}},
{"countryiso3code": "CHN", "country": {"value": "China"}},
...
]
data://indicators/{country_code}Return all indicators for a specific country from the local data.
@mcp.resource("data://indicators/{country_code}")
def get_country_indicators(country_code: str) -> str:
"""Get all indicators for a specific country from local data."""
df = _load_data()
# TODO: Filter by country_code and return as JSON string
# Hint: Use df.filter() with pl.col("countryiso3code") == country_code
Tools are executable functions that call external APIs. Implement these three tools:
get_country_info(country_code: str)Fetch country metadata from the REST Countries API.
@mcp.tool()
def get_country_info(country_code: str) -> dict:
"""Fetch detailed information about a country from REST Countries API."""
logger.info(f"Fetching country info for: {country_code}")
# TODO: Use _fetch_rest_countries() and extract relevant fields
# Return: name, capital, region, subregion, languages, currencies, population, flag
API Endpoint: https://restcountries.com/v3.1/alpha/{country_code}
Expected output:
{
"name": "United States of America",
"capital": "Washington, D.C.",
"region": "Americas",
"subregion": "North America",
"languages": ["English"],
"currencies": ["USD"],
"population": 331002651,
"flag": "🇺🇸"
}
get_live_indicator(country_code: str, indicator: str, year: int)Fetch a specific indicator value from the World Bank API.
@mcp.tool()
def get_live_indicator(country_code: str, indicator: str, year: int = 2022) -> dict:
"""Fetch a specific indicator value from the World Bank API."""
logger.info(f"Fetching {indicator} for {country_code} in {year}")
# TODO: Use _fetch_world_bank_indicator() and return the value
API Endpoint: https://api.worldbank.org/v2/country/{code}/indicator/{indicator}?format=json
Common Indicators:
| Indicator ID | Description |
|---|---|
NY.GDP.PCAP.CD |
GDP per capita (current US$) |
SP.POP.TOTL |
Total population |
SP.DYN.LE00.IN |
Life expectancy at birth |
SE.ADT.LITR.ZS |
Adult literacy rate |
compare_countries(country_codes: list[str], indicator: str)Compare an indicator across multiple countries.
@mcp.tool()
def compare_countries(country_codes: list[str], indicator: str, year: int = 2022) -> list[dict]:
"""Compare an indicator across multiple countries."""
logger.info(f"Comparing {indicator} for countries: {country_codes}")
# TODO: Call get_live_indicator for each country and collect results
Expected output:
[
{"country": "USA", "indicator": "SP.POP.TOTL", "value": 331002651, "year": 2022},
{"country": "CHN", "indicator": "SP.POP.TOTL", "value": 1412175000, "year": 2022},
{"country": "DEU", "indicator": "SP.POP.TOTL", "value": 83797985, "year": 2022}
]
Your implementation should handle these error cases gracefully:
Example error handling:
try:
data = _fetch_rest_countries(country_code)
except httpx.HTTPStatusError as e:
logger.error(f"API error for {country_code}: {e}")
return {"error": f"Country not found: {country_code}"}
Demonstrate that your server works by testing it with one of the following options:
Run the provided test client and capture the output to a log file:
# Terminal 1: Start your server
uv run python server.py
# Terminal 2: Run tests and save output to log file
uv run python test_client.py 2>&1 | tee test_results.log
Requirements:
test_results.log in your repositoryTest your server using the MCP Inspector visual UI:
# Terminal 1: Start your server
uv run python server.py
# Terminal 2: Start MCP Inspector
npx @modelcontextprotocol/inspector
Requirements:
screenshots/ folder and include your screenshotsinspector-connected.png, inspector-resource-test.png, inspector-tool-test.pngThe MCP Inspector provides a visual UI to test your server.
Terminal 1 - Start your server:
uv run python server.py
You should see:
Starting World Bank MCP Server on http://127.0.0.1:8765/mcp
Press Ctrl+C to stop
Terminal 2 - Start MCP Inspector:
npx @modelcontextprotocol/inspector
In the Inspector UI:
http://127.0.0.1:8765/mcpIf you're running on an EC2 instance, use SSH tunneling:
# From your LOCAL machine (not EC2), run:
ssh -L 8765:localhost:8765 -L 6274:localhost:6274 ubuntu@your-ec2-ip
# Then open in your local browser:
# Inspector: http://localhost:6274
# Your server: http://localhost:8765/mcp
We provide a test client that automatically tests all resources and tools.
Terminal 1 - Start your server:
uv run python server.py
Terminal 2 - Run the test client:
uv run python test_client.py
Expected output:
============================================================
TESTING RESOURCES
============================================================
Available resources: ['data://schema', 'data://countries', 'data://indicators/{country_code}']
Testing data://schema...
Schema: {'countryiso3code': 'String', 'country': 'String', ...}
Testing data://countries...
Countries: [{"countryiso3code": "USA", ...}]
Testing data://indicators/USA...
USA Indicators: [{"indicator": "NY.GDP.PCAP.CD", ...}]
============================================================
TESTING TOOLS
============================================================
Available tools: ['get_country_info', 'get_live_indicator', 'compare_countries']
Testing get_country_info('USA')...
Result: {"name": "United States of America", "capital": "Washington, D.C.", ...}
Testing get_live_indicator('USA', 'NY.GDP.PCAP.CD', 2022)...
Result: {"country": "USA", "indicator": "NY.GDP.PCAP.CD", "value": 76329.58, "year": 2022}
Testing compare_countries(['USA', 'CHN', 'DEU'], 'SP.POP.TOTL')...
Result: [{"country": "USA", ...}, {"country": "CHN", ...}, {"country": "DEU", ...}]
============================================================
ALL TESTS PASSED
============================================================
Once your server works, connect it to Claude Code:
Create .mcp.json in your project root:
{
"mcpServers": {
"world-bank": {
"type": "streamable-http",
"url": "http://127.0.0.1:8765/mcp"
}
}
}
Start your server, then restart Claude Code.
Try these prompts:
Endpoint: https://restcountries.com/v3.1/alpha/{code}
Example: https://restcountries.com/v3.1/alpha/USA
Response fields you need:
name.common - Country namecapital[0] - Capital cityregion - Geographic regionsubregion - Geographic subregionlanguages - Dictionary of languagescurrencies - Dictionary of currenciespopulation - Population countflag - Flag emojiEndpoint: https://api.worldbank.org/v2/country/{code}/indicator/{indicator}?format=json
Example: https://api.worldbank.org/v2/country/USA/indicator/NY.GDP.PCAP.CD?format=json&date=2022
Response structure:
[
{ "page": 1, "pages": 1, "total": 1 },
[
{
"indicator": {"id": "NY.GDP.PCAP.CD", "value": "GDP per capita"},
"country": {"id": "US", "value": "United States"},
"date": "2022",
"value": 76329.58
}
]
]
Note: Data is in the second element of the array (response[1]).
| Component | Points | Criteria |
|---|---|---|
| Resources (Part 1) | 25 | All 3 resources implemented and return correct data |
| Tools (Part 2) | 30 | All 3 tools implemented, API calls work correctly |
| Error Handling (Part 3) | 15 | Graceful handling of invalid inputs, API failures |
| Code Quality | 15 | Type hints, docstrings, follows course coding standards |
| Testing (Part 4) | 15 | Test log file OR screenshots showing server works |
Total: 100 points
Integrate your MCP server with an AI assistant and provide proof:
Option 1: Claude Code
.mcp.jsonscreenshots/ folderOption 2: Other AI Assistants
screenshots/ folderScreenshot requirements for bonus:
Before submitting, verify:
uv run python server.pyuv run ruff check .test_results.log file with passing tests, ORscreenshots/ folder with MCP Inspector screenshotsuv run python server.py)127.0.0.1 not 0.0.0.0lsof -i :8765uv sync to install dependenciesdf.write_json() returns a stringdf.to_dicts() returns a list of dictionariesdf.filter(pl.col("column") == value)from mcp.server.fastmcp import FastMCP
# Initialize server with host and port
mcp = FastMCP(
"my-server-name",
host="127.0.0.1",
port=8765,
)
# Define a resource (read-only data)
@mcp.resource("data://example")
def get_example_data() -> str:
return "Hello from resource!"
# Define a tool (executable function)
@mcp.tool()
def my_tool(param: str) -> dict:
return {"result": f"Processed: {param}"}
# Run with Streamable HTTP transport
if __name__ == "__main__":
mcp.run(transport="streamable-http")
| Decorator | Purpose | Return Type |
|---|---|---|
@mcp.resource("uri://path") |
Expose read-only data | str (usually JSON) |
@mcp.resource("uri://path/{param}") |
Parameterized resource | str |
@mcp.tool() |
Expose callable function | dict or any JSON-serializable |
mcp.run(transport="streamable-http")mcp.run(transport="stdio") - for subprocess-based clientsВыполни в терминале:
claude mcp add world-bank-server -- npx Безопасность
Низкий рискАвтоматическая эвристика по публичным данным — не гарантия безопасности.