Skip to content

netlight/uzh_mcp_workshop

Repository files navigation

UZH MCP Workshop

Timeline

Time Section Details
0–5 min Get to Know Your Netlighter Ice breaker with consultant
5–15 min Motivation & MCP Demo (everyone) Slides + live demo of finished project
15–25 min Setup Docker
25–30 min Exercise Explanation (everyone) Code walkthrough
30–60 min Coding Exercises Buildings (5-10m) → Transport (10-15m) → Bonus
60–70 min Debrief + Solution (everyone) Show solution branch, Q&A

What You'll Build

An MCP (Model Context Protocol) server that gives your AI assistant real-time access to:

  • UZH building locations
  • Swiss public transport connections
  • UZH Mensa menus

Get to Know Your Netlighter (0–5 min)

Meet your Netlighter consultant! Quick round of introductions to kick off the workshop.


Motivation & MCP Demo (5–15 min)

Your presenter will cover:

  • What is MCP (Model Context Protocol)?
  • The problem: LLMs are isolated from real-world data
  • The solution: MCP tools give LLMs access to APIs
  • Live demo of the finished project — the AI assistant you'll build today

Setup (15–25 min)

1. Clone the repository

git clone https://github.com/netlight/uzh_mcp_workshop
cd uzh_mcp_workshop

2. Start with Docker

docker compose up --build

This starts:

3. Verify

Open http://localhost:3000 in your browser. You should see the Open WebUI interface.

4. Configure Open WebUI

  1. Open http://localhost:3000/admin/settings/integrations
  2. Find the tool named UZH MCP, toggle it off, then toggle it back on
  3. Open http://localhost:3000/admin/settings/models
  4. Select claude-sonnet-4-5 and click the Edit (pen) button
  5. In the System Prompt field, paste the contents of prompt_system.md
  6. Under Tools, tick UZH MCP to enable it
  7. Save

5. Test it

  1. Open http://localhost:3000 and start a New Chat
  2. Try asking: "List all UZH buildings"

The AI will automatically call your MCP tools to answer. After each code change, rebuild with docker compose up --build and refresh the page.


Exercise Explanation (25–30 min)

Your presenter will walk through the code structure:

  • src/server.py — FastMCP server instance
  • src/__main__.py — entry point where tool modules are registered
  • src/buildings.py, src/transportation.py, src/mensa.py — starter code you'll complete
  • How the @mcp.tool() decorator works
  • Overview of the three APIs you'll use

Coding Exercises (30–60 min)

Exercise 1: Buildings (5–10 min)

File: src/buildings.py API: https://ziplaene-api.uzh.ch/v1/map/buildings

The API returns JSON like:

{
  "buildings": [
    {"shortName": "KOL", "group": "Zentrum", "lat": 47.37, "lon": 8.55, ...},
    ...
  ]
}

Task 1: get_building_location(building_name)

  1. Make a GET request to UZH_BUILDINGS_URL using httpx.get()
  2. Call .raise_for_status() on the response to check for errors
  3. Parse the JSON response: resp.json()["buildings"]
  4. Loop through buildings, find the one matching building_name (convert to uppercase first)
  5. Return a dict with: shortName, lat, lon

Task 2: list_buildings()

  1. Make a GET request to the same URL
  2. Return a list of dicts with shortName and group for each building

Rebuild & test:

docker compose up --build

Exercise 2: Transportation (10–15 min)

File: src/transportation.py API: https://transport.opendata.ch/v1/

Task: get_connections(origin, destination, time, is_arrival_time, limit)

  1. Build a params dict with: from (= origin), to (= destination), limit
  2. If time is provided, add "time" to params
  3. If is_arrival_time is true, add "isArrivalTime": 1 to params
  4. GET request to TRANSPORT_URL_API + "connections"
  5. Parse resp.json()["connections"] and return a list of dicts with:
    • departure: conn["from"]["departure"]
    • arrival: conn["to"]["arrival"]
    • duration: conn["duration"]
    • transports: conn["products"]

Rebuild & test:

docker compose up --build

Bonus 1: Mensa

File: src/mensa.py API: https://api.zfv.ch/graphql (GraphQL)

The GraphQL queries, the _zfv_query() helper, and step-by-step comments are already provided in the stub. You need to implement a single get_mensa_menu(date) function that internally fetches all outlets, categories, and dishes.

Task: get_mensa_menu(date)

  1. Default date to today (datetime.date.today().isoformat()) if None
  2. Fetch all outlets: call _zfv_query() with the outlets query, navigate data["data"]["client"]["organisationPermissions"], loop through and collect perm["organisation"]["outlets"]
  3. For each outlet, fetch its menu categories: call _zfv_query() with the categories query, get data["data"]["outlet"]["menuCategories"]
  4. For each outlet + category, fetch dishes: call _zfv_query() with the dishes query, navigate to data["data"]["outlet"]["calendar"]["week"]["menuCategory"]["daily"], filter entries where date["dateLocal"] starts with the requested date, collect the dish from each menuItem
  5. Return structured result: {"date": "...", "outlets": [{"name": "...", "categories": [{"name": "...", "dishes": [...]}]}]}
  6. Only include outlets/categories that have dishes for the requested date

Bonus 2: Your Own Tool

Create your own MCP tool! Ideas:

  • Weather API
  • UZH events
  • Study room finder

Steps:

  1. Create a new file src/my_tool.py
  2. Import the mcp server: from .server import mcp
  3. Add @mcp.tool() decorated functions
  4. Import your module in src/__main__.py: from . import my_tool
  5. Rebuild: docker compose up --build

Debrief + Solution (60–70 min)

Your presenter will:

  • Walk through the solution branch and completed implementations
  • Open the floor for Q&A
  • Recap what you built: a real AI-powered campus assistant

Going Further: MCP Resources

So far we've only used tools — functions the AI actively calls with parameters. MCP defines a second primitive called resources: read-only data the AI can read by URI, like opening a file.

Tool Resource
Direction AI calls it with parameters AI reads it by URI
Example get_building_location("KOL") uzh://buildings/list
Use case Actions, lookups, side-effects Static or slowly-changing data

Here's what a resource looks like using our buildings example:

@mcp.resource("uzh://buildings/list")
def buildings_overview() -> str:
    """A static list of all UZH building codes."""
    return "KOL, BIN, SOC, RAA, ..."

Resources are useful when you want to expose reference data without requiring the AI to pass parameters — think configuration, documentation, or catalogues.


Quick Reference

Library Usage
httpx.get(url, params={...}) HTTP GET request with query parameters
httpx.post(url, json={...}) HTTP POST request with JSON body
resp.raise_for_status() Raise an error if the request failed
resp.json() Parse JSON response body
@mcp.tool() Register a function as an MCP tool
@mcp.resource("uri") Expose read-only data the AI can access by URI

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors