Your AI can check the weather, fetch news, and book meetings — if you teach it how. This workshop gives you the blueprint.
Let's be honest: ChatGPT is impressive, but it can't check your calendar, pull live data, or order you a pizza. Yet.
You've seen the demos. You've read the docs. You've stared at JSON-RPC specs until your eyes glazed over. You've copy-pasted code from tutorials that mysteriously stopped working three months ago.
We've done the suffering so you don't have to. Three hours. One working AI agent. Tools you actually built. No hand-waving.
A complete agent system running in Docker.
Chat UI where you interact with your agent.
OpenAI GPT-4 based orchestration layer.
Where your custom Python tools live.
No death-by-PowerPoint. We cover Model Context Protocol (MCP) and JSON-RPC 2.0 without the fluff.
You'll understand:
Zero-install via GitHub Codespaces. Fork, keys, docker up.
.env.example to .envdocker compose up -d# Verify the agent is alive
curl -X POST "http://localhost:8001/query" \
-H "Content-Type: application/json" \
-d '{"query": "What is the weather in Oslo?"}' Before we build, we understand. Let's trace how a request flows through the system.
The MCP Server exposes tools via JSON-RPC:
curl -X POST "http://localhost:8000/message" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' Response — your available tools:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [{
"name": "get_weather_forecast",
"description": "Fetch weather forecast for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string" }
},
"required": ["location"]
}
}]
}
} The agent reads this at startup and knows what it can do. Add a tool to the server, restart, and the agent learns it automatically.
Time to get dangerous. You'll add a random fact generator to the MCP server.
Step 1: The function
async def get_random_fact(category: str = "general") -> Dict[str, Any]: """Because every AI needs useless trivia.""" facts = { "general": [ "Honey never spoils. 3000-year-old honey found in Egyptian tombs — still edible.", "Octopuses have three hearts." ], "space": [ "A day on Venus is longer than its year.", "Neutron stars are so dense a teaspoon weighs 6 billion tons." ] } import random return { "category": category, "fact": random.choice(facts.get(category, facts["general"])), "timestamp": datetime.now().isoformat() }
Step 2: Register it in the manifest
{
"name": "get_random_fact",
"description": "Get a random interesting fact",
"inputSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["general", "space"],
"description": "Category of fact"
}
},
"required": ["category"]
}
} Step 3: Handle the call
elif tool_name == "get_random_fact": result = await get_random_fact(arguments.get("category", "general")) return { "content": [{"type": "text", "text": json.dumps(result)}], "isError": False }
Step 4: Test it
# Rebuild and restart docker compose build mcp-server && docker compose up -d # Call it directly curl -X POST "http://localhost:8000/message" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_random_fact", "arguments": {"category": "space"} } }' # Or through the agent curl -X POST "http://localhost:8001/query" \ -H "Content-Type: application/json" \ -d '{"query": "Tell me something interesting about space"}'
Your agent now knows trivia. You've corrupted it. Well done.
Enough toy examples. Let's connect to a real API. We've also deployed a live MCP news server
at news.hlutur.com
(IP: 128.199.62.215) that you can use in the
bonus challenges.
async def get_news(topic: str, language: str = "en") -> Dict[str, Any]: """Fetch real news from NewsAPI.""" api_key = os.getenv("NEWS_API_KEY") if not api_key: return {"isError": True, "content": [{"type": "text", "text": "NEWS_API_KEY not set"}]} async with httpx.AsyncClient() as client: response = await client.get( "https://newsapi.org/v2/everything", params={"q": topic, "language": language, "apiKey": api_key}, timeout=10.0 ) articles = response.json().get("articles", [])[:3] formatted = "\n".join([f"• {a['title']}\n {a['url']}" for a in articles]) return { "isError": False, "content": [{"type": "text", "text": f"Latest on '{topic}':\n\n{formatted}"}] }
Test the combo:
curl -X POST "http://localhost:8001/query" \
-H "Content-Type: application/json" \
-d '{"query": "What is the weather in Berlin and what are the latest news about AI?"}' The agent calls BOTH tools in one request. It's not magic — it's good orchestration.
Things will break. Here's how to fix them.
Health checks:
curl http://localhost:8000/health # MCP Server curl http://localhost:8001/health # Agent curl http://localhost:8080/health # Web # Check if the agent loaded your tools docker compose logs travel-agent | grep "tools"
Common disasters and fixes:
| Symptom | Cause | Fix |
|---|---|---|
| "API key not configured" | Missing .env values | Check env vars in container |
| Tool not showing up | Manifest not updated | Restart agent after rebuilding MCP server |
| "Location not found" | Vague location name | Be specific: "Oslo, Norway" |
| Container won't start | Port conflict | docker compose down then up |
What you can build next.
Stateful tools:
# Remember user preferences user_prefs = {} async def set_preference(user_id: str, key: str, value: str): user_prefs.setdefault(user_id, {})[key] = value return {"status": "saved", "key": key, "value": value}
Multi-step workflows:
# Chain tools: weather → attractions → booking async def plan_trip(destination: str): weather = await get_weather_forecast(destination) if weather["current"]["temp"] > 20: attractions = await get_outdoor_attractions(destination) else: attractions = await get_indoor_attractions(destination) return {"weather": weather, "suggestions": attractions}
Async operations:
# Long-running tasks with status polling async def generate_report(topic: str) -> str: job_id = str(uuid.uuid4()) background_tasks.add_task(run_report, job_id, topic) return {"job_id": job_id, "status": "processing"}
Finished the workshop early? These three challenges will deepen your understanding of agents and MCP servers. Each one builds on what you've already done — no extra setup required.
Add system prompts, conversation memory, and multi-turn context so your agent actually remembers what you said.
Wire your agent to discover tools from both your local server and the remote news server at news.hlutur.com.
Create a brand new MCP server from scratch with caching, structured content, and production-ready error handling.
We've deployed a compatible MCP news server at news.hlutur.com
(IP: 128.199.62.215) that you can use in the bonus challenges.
It exposes get_news, get_headlines,
and list_sources tools via JSON-RPC 2.0 — no API key required.
DNS may not be available yet, so use the IP address if needed.
Complete system running in Docker — take it home
Weather, facts, news — and whatever you built
You understand the protocol, not just the code
Security, error handling, scaling strategies
Fork it, extend it, break it, fix it
Life is too short for Python 2
Async REST without boilerplate
The brain behind the op
One command to rule them all
The MCP lingua franca
Modern async HTTP client
"Has read the MCP spec so you don't have to."
"Believes every AI deserves access to weather data."