3 Hours | Zero Fluff | Maximum Hands-on

Stop Talking to AI.
Start Building With It.

Your AI can check the weather, fetch news, and book meetings — if you teach it how. This workshop gives you the blueprint.

The Problem

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.

What You'll Actually Build

A complete agent system running in Docker.

Port 8080

Web Interface

Chat UI where you interact with your agent.

Port 8001

AI Agent

OpenAI GPT-4 based orchestration layer.

Port 8000

MCP Server

Where your custom Python tools live.

Fetch live weather data
Pull latest news via live MCP server
Serve random facts
Any tool YOU decide to add

The Timeline

30 min

What Are We Even Building?

No death-by-PowerPoint. We cover Model Context Protocol (MCP) and JSON-RPC 2.0 without the fluff.

You'll understand:

  • What an AI agent actually is (hint: it's not magic)
  • How Model Context Protocol connects AI to the real world
  • Why JSON-RPC 2.0 is the lingua franca of tool communication
User → Web → Agent → OpenAI → Tools (MCP) → Answer
20 min

Get Your Hands Dirty

Zero-install via GitHub Codespaces. Fork, keys, docker up.

  1. Fork the repo
  2. Open in Codespaces
  3. Copy .env.example to .env
  4. Add your API keys
  5. Run docker compose up -d
  6. You now have a working AI agent. That was fast.
terminal
# 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?"}'
20 min

Poke Around

Before we build, we understand. Let's trace how a request flows through the system.

The MCP Server exposes tools via JSON-RPC:

terminal
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.

30 min

Build Your First Tool

Time to get dangerous. You'll add a random fact generator to the MCP server.

Step 1: The function

services/mcp-server/app.py
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

terminal
# 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.

30 min

Real APIs — News Integration

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.

services/mcp-server/app.py
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:

terminal
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.

20 min

Make It Bulletproof

Things will break. Here's how to fix them.

Health checks:

terminal
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
20 min

Beyond the Workshop

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"}
For Speedy Developers

Bonus Challenges

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.

Live News Server Available

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.

What You Walk Away With

Working AI Agent

Complete system running in Docker — take it home

3+ Custom Tools

Weather, facts, news — and whatever you built

MCP Expertise

You understand the protocol, not just the code

Production Patterns

Security, error handling, scaling strategies

Full Source Code

Fork it, extend it, break it, fix it

What You Won't Get

Death by PowerPoint
"Left as an exercise for the reader"
Vague hand-waving about "the future of AI"
A tutorial that worked six months ago but doesn't now

The Stack

Python 3.11+

Life is too short for Python 2

FastAPI

Async REST without boilerplate

OpenAI GPT-4o-mini

The brain behind the op

Docker Compose

One command to rule them all

JSON-RPC 2.0

The MCP lingua franca

httpx

Modern async HTTP client

Prerequisites

Required

  • GitHub account (for Codespaces)
  • OpenAI API key (free tier works)
  • OpenWeatherMap API key (free)
  • Basic Python knowledge (if you know what a function is, you're good)

Optional but helpful

  • Docker experience (we'll explain as we go)
  • REST API familiarity (GET, POST, JSON)
  • A sense of humor about AI hype

Who Should Join

Developers who learn by doing
People who'd rather build in 3 hours than read docs for 3 weeks
Anyone who's thought "why can't my AI just do the thing?"
Backend devs curious about AI integration
Frontend devs who want to understand what they're connecting to
That person at work who keeps asking "but can we add AI to this?"

Instructors

LF

Leif Terje Fonnes

leffen@gmail.com github.com/leffen

"Has read the MCP spec so you don't have to."

LS

Lars Søraas

lsoraas@gmail.com github.com/zral

"Believes every AI deserves access to weather data."