AI-powered Oktoberfest reservation monitor β watches 38 beer tent portals, alerts you when slots open, and helps you book before anyone else.
"Check all portals and tell me which tents have evening slots on September 25th."
"Go to the Hacker-Festzelt and fill out the reservation form for me."
"Keep monitoring all portals and send me a notification when new dates appear."
"What tents currently have open reservations? Show me a summary."
Three ways to use Wiesn-Agent:
Option A β Web Dashboard (wiesn-agent web / Docker)
graph LR
SCAN["π Scanner\nauto every 30 min"] -->|new slots| ALERT["π² Push + π Toast"]
SCAN -->|results| DASH["πΊ Dashboard"]
DASH -->|chat: fill form| FILL["π€ AI pre-fills"]
FILL -->|never submits| YOU["β You click submit"]
Option B β MCP Tools (VS Code Copilot, Claude Desktop, Cursor, β¦)
graph LR
AI["π¬ AI Client"] -->|MCP| TOOLS["π οΈ 15 tools\nscan Β· fill Β· notify"]
TOOLS -->|browser actions| PORTAL["π Portal"]
PORTAL -->|never auto-submits| YOU["β You confirm"]
Option C β CLI Workflow (wiesn-agent once/watch)
graph LR
MON["Monitor"] --> ANA["Analyzer"] --> NOT["Notifier\n+ approval prompt"]
NOT -->|approved| FILL["Filler"]
NOT -->|declined| END["End run"]
In
watchmode, the workflow repeats every N minutes.
All three options share the same scanner, tools, and config. Forms are never submitted automatically.
β οΈ Continuous monitoring requires a running process. For Option A, keepwiesn-agent webrunning (or usedocker compose up -dfor background). For Option C, usewiesn-agent watch. The scanner checks every 30 minutes and sends push notifications when new slots appear β but only while the process is alive.
git clone https://github.com/annawiewer/wiesn-agent.git && cd wiesn-agent
cp config.example.yaml config.yaml # edit with your details
cp .env.example .env # edit tokens if needed
docker compose up # β http://localhost:5000The dashboard includes a chat window β ask questions, trigger scans, or fill forms directly from the browser. Add a GITHUB_TOKEN to .env for LLM-powered chat (see Agent Workflow).
Prerequisites: Python 3.10+, Node.js 18+ (for building the frontend)
git clone https://github.com/annawiewer/wiesn-agent.git && cd wiesn-agent
python -m venv .venv
# Linux/macOS: source .venv/bin/activate
# Windows: .venv\Scripts\activate
pip install -e ".[web]"
playwright install chromium # Linux: add --with-deps if missing system libs
cp config.example.yaml config.yaml # edit with your details
cp .env.example .env # edit tokens if needed
cd web && npm install && npm run build && cd ..
wiesn-agent web # β http://localhost:5000Note: The backend (
wiesn-agent web) must be running before the frontend works. It serves both the API and the built React app on port 5000. For development, start the backend first (wiesn-agent web), then the frontend dev server (cd web && npm run dev) β the Vite dev server on :5173 proxies API calls to :5000.
Connect any AI assistant (Copilot, Claude, Cursor, etc.) to 15 MCP tools via the Model Context Protocol.
Prerequisites: Install the package and Playwright browser first:
cd wiesn-agent
pip install -e .
playwright install chromium
cp config.example.yaml config.yaml # edit with your detailsAdd to .vscode/mcp.json:
{
"servers": {
"wiesn-agent": {
"command": "python",
"args": ["-m", "wiesn_agent.mcp_server"],
"cwd": "${workspaceFolder}"
}
}
}Add to your Claude MCP config (set cwd to the repo directory where config.yaml lives):
{
"mcpServers": {
"wiesn-agent": {
"command": "python",
"args": ["-m", "wiesn_agent.mcp_server"],
"cwd": "/path/to/wiesn-agent"
}
}
}| Tool | Description | Input Parameters |
|---|---|---|
check_portal |
Navigate to a portal and check for changes | url (string), name (string, optional) |
check_all_portals |
Scan all configured portals at once | β |
detect_forms |
Detect form fields, selects, and buttons on the page | β |
fill_field |
Fill a text input field | selector (string), value (string) |
select_option |
Select a dropdown option (with Livewire support) | selector (string), value (string) |
click_element |
Click a button or element | selector (string), force (bool, optional) |
fill_reservation_form |
Auto-fill entire reservation form with user data | β |
switch_to_iframe |
Switch context to an iframe (for KΓ€fer, Tradition) | selector (string) |
run_js |
Execute JavaScript on the page | script (string) |
wait_for_element |
Wait for an element to appear | selector (string), timeout (int, optional), state (string, optional) |
navigate_to |
Navigate to a URL | url (string), wait_until (string, optional) |
take_screenshot |
Take a screenshot of the current page | name (string, optional) |
get_page_content |
Get the text content of the page | selector (string, optional) |
monitor_availability |
Scan all portals and compare with last snapshot | portal_name (string, optional), check_date (string, optional), notify (bool, optional) |
send_notification |
Send notification via 130+ services (ntfy, Telegram, etc.) | title (string), message (string), notify_type (string, optional) |
Note: The web dashboard chat exposes 14 of these tools β
run_jsis excluded from chat agents for safety. It remains available via direct MCP connection.
Resources: wiesn://config Β· wiesn://portale Β· wiesn://slots
Prompts: Check all portals Β· Monitor availability Β· Check single portal Β· FestZelt OS Wizard
Built on Microsoft's Microsoft Agent Framework β instead of one monolithic LLM prompt, the system uses 4 specialized agents that each have their own instructions and tools. A triage layer routes every message to the right agent automatically.
The TriageExecutor is a custom routing layer built on top of the Agent Framework's Executor base class. The framework provides the workflow graph infrastructure (executors, edges, context passing), and we implement the routing logic:
- No LLM call needed β uses fast keyword matching (e.g. "slot", "available" β Scanner; "fill", "form" β Form Agent)
- Context-aware β "Fill the form for Sep 25" routes to Form Agent (not Scanner), even though a date is present
- Bilingual β recognizes both German and English keywords
- Structured handoffs β agents signal follow-up intent via invisible markers, so "Ja" correctly routes to the offered action
- Cancel handling β "Nein", "Stop", "Cancel" resets pending actions and routes to Chat
- Zero latency β routing happens instantly, only the chosen agent calls the LLM
| Single Prompt | Multi-Agent (Wiesn-Agent) |
|---|---|
| One LLM sees all 15 tools | Each agent sees only its tools |
| Long, unfocused system prompt | Short, expert prompt per agent |
| Can accidentally call form tools while scanning | Scanner cannot access form tools |
| Hard to debug | Triage log shows exactly which agent ran |
User Message
β
ββββββββββββββββββββββββββ
β TriageExecutor β keyword classification (no LLM needed)
β "Evening slots?" β β detected: scan intent
ββββββββββββββ¬ββββββββββββ
β routes to one of 4 agents:
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Scanner β β Form β β Notifier β β Chat β
β Agent β β Agent β β Agent β β Agent β
β β β β β β β β
β Tools: β β Tools: β β Tools: β β No tools β
β β’ monitor β β β’ navigate β β β’ send β β (text only) β
β β’ check β β β’ fill β β notif. β β β
β β’ check_all β β β’ select β β β β β
β β β β’ click β β β β β
β β β β’ detect β β β β β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
| Component | What it does |
|---|---|
| Agent | LLM with own system prompt + own tools |
| TriageExecutor | Routes user messages by keyword (no LLM cost) |
| WorkflowBuilder | Defines the agent graph (who talks to whom) |
| WorkflowAgent | Wraps the graph as a session-aware agent API |
| InMemoryHistoryProvider | Conversation memory across turns |
| MCPStdioTool | Connects Playwright MCP tools to agents |
| OpenAIChatCompletionClient | LLM via GitHub Models (GPT-4o) |
Web Chat (wiesn-agent web) β star graph with triage:
graph LR
USER["π¬ User"] --> TRIAGE["π Triage"]
TRIAGE -->|scan| SCANNER["π Scanner"]
TRIAGE -->|form| FORM["π Form Agent"]
TRIAGE -->|notify| NOTIFY["π Notifier"]
TRIAGE -->|chat| CHAT["π Chat Agent"]
CLI Pipeline (wiesn-agent once/watch) β linear graph:
graph LR
MON["Monitor"] --> ANA["Analyzer"] --> NOT["Notifier\n+ approval"]
NOT -->|approved| FILL["Filler"]
NOT -->|declined| END["End run"]
In
watchmode, the pipeline repeats automatically every N minutes.
The triage layer uses structured handoff signals β agents append invisible markers to their responses that tell the triage what action was offered:
- Scanner found no matching slots β offers notification test
<!-- handoff:notify -->β user says "Ja" β routes to Notifier - Scanner found matching slots β offers form fill
<!-- handoff:form -->β user says "Ja" β routes to Form Agent - User says "Nein" or "Cancel" β resets pending action, routes to Chat
This works in both German and English, no LLM call needed for routing.
Setup & Usage
pip install -e ".[web]"
playwright install chromium
echo "GITHUB_TOKEN=ghp_..." > .env # github.com/settings/tokens (models:read)
wiesn-agent web # web dashboard + chat at :5000
wiesn-agent web --host 0.0.0.0 --port 8080 # LAN access on custom port
wiesn-agent once # single CLI run
wiesn-agent watch # continuous CLI monitoring (interactive β prompts for approval)Note:
watchmode is interactive β it prompts via stdin when a matching slot is found. It is not suitable as an unattended background daemon. For unattended monitoring, usewiesn-agent webwhich runs the scanner automatically and sends push/web notifications.
Without
GITHUB_TOKEN, the dashboard chat falls back to keyword-based responses (scan, status, matches, portals) β no LLM required.
wiesn-agent web β Dashboard (live results, scan trigger) Β· Portals (38 tents, filter) Β· Statistics (charts) Β· Settings (config editor)
Background scanner runs automatically with deep-scan on your preferred dates.
# config.yaml β keys use German field names (keep them unchanged)
user:
vorname: "Max"
nachname: "Mustermann"
email: "max@example.com"
telefon: "+49 170 1234567"
personen: 10
reservierung:
wunsch_tage: ["2026-09-19", "2026-09-25", "2026-09-26"]
slots:
morgens: { enabled: true, von: "10:00", bis: "12:00", prioritaet: 3 }
mittags: { enabled: true, von: "12:00", bis: "16:00", prioritaet: 2 }
abends: { enabled: true, von: "16:00", bis: "23:00", prioritaet: 1 }
notifications:
desktop: true
apprise_urls:
- "ntfy://wiesn-alert" # free phone push via ntfy.shAll 38 tents are pre-configured. Enable/disable in the portale section or via the Dashboard.
Auto-detected: Livewire (LΓΆwenbrΓ€u, Fischer-Vroniβ¦) Β· WordPress (Hacker, Schottenhamelβ¦) Β· iFrame (KΓ€fer, Tradition) Β· Standard (Augustiner, Paulanerβ¦)
Custom per-portal adapters can be registered for high-value tents that use non-standard UI patterns (see portal_adapters.py).
Apprise-powered β ntfy (recommended), Telegram, Slack, Email, 130+ more.
-
Install the ntfy app on your phone:
-
Open the app β tap "+" β enter your channel name (e.g.
wiesn-alert) -
Add the channel to your config (
config.yamlβ notifications β apprise_urls):notifications: apprise_urls: - "ntfy://wiesn-alert" # must match your app subscription
-
Done! You'll receive push notifications when new evening slots are found.
Tip: Your channel name is public on ntfy.sh. Use something unique like
wiesn-yourname-2026for privacy. Or self-host ntfy for full control.
No app? Open
https://ntfy.sh/your-channelin any browser to see notifications.
Add any Apprise URL to apprise_urls:
| Service | Config Example |
|---|---|
| Telegram | tgram://BOT_TOKEN/CHAT_ID |
| Slack | slack://TOKEN_A/TOKEN_B/TOKEN_C |
| Discord | discord://WEBHOOK_ID/WEBHOOK_TOKEN |
mailtos://user:pass@gmail.com |
|
whatsapp://TOKEN@PHONE/TARGET |
Notifications are suppressed during quiet hours (default: 22:00β08:00). Alerts found overnight are queued and delivered as a morning digest when quiet hours end. Web browser toasts are always shown immediately.
- API Auth: Set
WIESN_API_TOKENin.envto require bearer token auth on all/api/endpoints (except/api/health). When not set, the API is open (localhost-only use). - PII Redaction: Config endpoints and MCP resources automatically redact email, phone, notification secrets, and tokens. Personal names are kept (needed for form filling).
- Human-in-the-Loop: Forms are never auto-submitted. The CLI workflow prompts for approval via stdin before pre-filling. In web mode, form filling is chat-driven β the agent pre-fills, you review and submit manually.
- run_js Gating: The
run_jstool (arbitrary JS execution) is excluded from chat agent tools. It remains available via direct MCP connection for expert users. All executed scripts are logged. - Prompt Injection Defense: Agent instructions explicitly treat portal page content as untrusted and ignore any instructions found in page text.
- Audit Log: Scan events and notifications are logged to
data/audit.log(append-only, not subject to ring buffer limits). - Atomic Snapshots: Availability snapshots use temp-file + rename to prevent corruption from concurrent writes.
src/wiesn_agent/
βββ mcp_server.py # MCP Server (15 tools)
βββ api.py # FastAPI backend + background scanner
βββ scanner.py # Portal scanner (deep scan, snapshots)
βββ chat_agent.py # Multi-agent chat workflow (TriageExecutor)
βββ workflow.py # CLI agent workflow graph (once/watch)
βββ portal_adapters.py # Per-portal scanning adapters (extensible)
βββ agents/ # 4 AI agents (CLI workflow)
βββ tools/ # Browser + notification tools
web/src/ # React dashboard (Vite + Tailwind)
| Issue | Solution |
|---|---|
playwright install chromium fails |
Run with sudo on Linux, or use playwright install --with-deps chromium for missing system libraries |
| Livewire forms don't populate next selects | Increase wait time between select_option calls (Livewire needs 2β3s for server roundtrip) |
| CSS selector fails on FestZelt OS portals | IDs contain dots β use run_js with document.getElementById() instead |
| iframe portals (KΓ€fer, Tradition) show no forms | Call switch_to_iframe first to enter the iframe context |
GITHUB_TOKEN errors in chat |
Create a token at github.com/settings/tokens with models:read scope, add to .env |
| Scanner finds no dates | Most portals only open reservations ~3 months before Oktoberfest (mid-June 2026) |
| API returns 401 Unauthorized | Set Authorization: Bearer <token> header, or remove WIESN_API_TOKEN from .env |
MIT