mcp-apps-lab
by hazurafif
README.md
# mcp-apps-lab
One **FastMCP app server** (Python) β a single MCP server that hosts four
interactive Prefab apps (quiz, weather, news, English Duo) plus MCP
resources and prompts.
Each app tool returns a Prefab UI (buttons, cards, tabs, progress) instead of
raw JSON; any MCP host renders them, and the LLM sees a text summary.
## Apps
| App | UI tool | Backend tool | What it demonstrates |
| --- | --- | --- | --- |
| Quiz | `take_quiz` | `submit_answer` | Multi-turn state: the LLM generates questions, the user answers via buttons, each click grades through a backend tool, the final score is sent back to the conversation |
| Weather | `weather_app` | `get_weather` | Live forecast from the Open-Meteo API: free-text location input geocodes ANY city name (no fixed table β βbekasiβ works), shows current conditions with the city's local time (π 20:31 WIB), region/country, and a 5-day forecast; unknown names fall back to Jakarta, sample data offline (LIVE/SAMPEL badge); lookups go through the host's tools/call proxy (hashed tool names β the proxy never sees the mapping) |
| News Curator | `news_curator` | `get_feed` | Live RSS feeds (Bloomberg Markets, CNBC, The Guardian Business, BBC Business) fetched through the backend tool on tab click / refresh β parsed with the stdlib, sample-data fallback when offline (LIVE/SAMPLE badge); compiles a markdown briefing and sends it back to the conversation |
| English Duo | `duo_english`, `duo_flashcards` | `grade_answer`, `get_profile`, `add_word` | A Duolingo-style English learning app: CEFR-graded vocabulary (A1-B2), FSRS-6 spaced-repetition cards per word, and game mechanics β XP + combo bonus, 5 hearts (mistakes cost one, daily refill), daily streak π₯, and a BronzeβDiamond level ladder. Due reviews + new words drive each lesson; answers are graded by the backend tool which reschedules the word's card and updates the profile in SQLite. `duo_english` cycles five exercise types (MC, fill, typing, sentence builder, flashcards); `duo_flashcards` is a pure flip-card review session (Again/Hard/Good/Easy β FSRS) with no quiz questions |
Also exposed server-side: live resources (`news://{source}/feed`,
`news://{source}/briefing`, `weather://{city}/current`, `duo://profile`,
`duo://due`, `duo://words/{level}`, `duo://guide/{level}`, `duo://levels`)
and prompts (`morning-briefing`, `daily-english`).
## Layout
```
src/mcp_apps_lab/
βββ server.py # the ONE FastMCP server β wires apps, tools, resources, prompts
βββ apps/ # the FastMCPApp UIs (LLM-facing entry points)
β βββ quiz.py # take_quiz UI
β βββ weather.py # weather_app UI
β βββ news.py # news_curator UI
β βββ duo.py # duo_english UI (English Duo)
βββ tools/ # backend tool functions the UIs call via the tool proxy
β βββ quiz.py # submit_answer
β βββ weather.py # get_weather
β βββ news.py # get_feed (live RSS fetch + offline fallback)
β βββ duo.py # grade_answer, get_profile, add_word
βββ duo/ # the English Duo engine
β βββ store.py # SQLite persistence (~/.mcp-apps-lab/duo.db)
β βββ scheduler.py # FSRS-6 spaced-repetition wrapper
β βββ game.py # XP/combo, hearts, streak, level ladder
β βββ engine.py # lesson building + grading orchestration
βββ resources/ # MCP resources (news://, weather://, duo://profile, duo://due)
βββ prompts/ # MCP prompt templates (morning-briefing, daily-english)
βββ data/ # feed definitions, offline fallback data, word bank
```
## Setup
```bash
uv sync # installs the package (editable) + fastmcp[apps] + dev tools
```
## Configuring which tools are enabled
`config.json` at the repo root decides which UI apps (tools) the server
advertises to the LLM. Disabled apps are not registered at all β their
backend tools stay hidden too.
```json
{
"tools": {
"take_quiz": false,
"weather_app": false,
"news_curator": true,
"duo_english": true
}
}
```
- Keys: `take_quiz`, `weather_app`, `news_curator`, `duo_english`.
- Missing keys default to **enabled**; unknown keys are ignored.
- Lookup order: `MCP_APPS_LAB_CONFIG` env var β `./config.json` β
`~/.mcp-apps-lab/config.json`.
- The checked-in config currently runs with **quiz and weather disabled**
(English Duo + News Curator active); flip the booleans to re-enable.
## Running
### Plain streamable-HTTP server
```bash
uv run python -m mcp_apps_lab # streamable HTTP at http://127.0.0.1:8090/mcp
# (MCP_APPS_LAB_PORT to override)
```
### Browser dev UI (`fastmcp dev apps`)
```bash
uv run fastmcp dev apps src/mcp_apps_lab/server.py --mcp-port 8090
```
- MCP server: `http://127.0.0.1:8090/mcp` (auto-reload on save)
- Dev UI: `http://localhost:8080` β pick `take_quiz`, `weather_app`,
`news_curator`, or `duo_english`, fill in arguments, and play the
rendered app in a new tab
- The left inspector panel shows the JSON-RPC traffic (including the hashed
backend-tool calls the UIs make)
## English Duo details
State lives in a SQLite database (`~/.mcp-apps-lab/duo.db`, override with
`DUO_DB_PATH`):
- **AI-generated content** β the coach generates fresh, CEFR-appropriate
vocabulary itself (personalized to the user's level/interests) and
passes it via the `words` argument of `duo_english` / `duo_flashcards`;
sessions built this way show an β¨ AI-GENERATED badge. Every generated
word is saved to the bank with its own FSRS card, so it comes back in
later review sessions. The built-in 120-word bank remains the fallback
(and the source for due-review-only sessions).
- **Duolingo brand UI** β Nunito typeface, Duolingo palette (green `#58CC02`,
blue `#1CB0F6`, yellow `#FFC800`, red `#FF4B4B`), rounded cards, 3D-press
buttons, and green/red feedback banners β distinct from the generic quiz UI.
- **Spaced repetition** β one FSRS-6 card per word (`fsrs` package, the
algorithm modern Anki uses). Correct β *Good*, wrong β *Again*; due
reviews are served first in every lesson, new words fill the rest.
An explicit level (e.g. `duo_flashcards(level="b1")`) is honored
strictly: only that level's due reviews and new words are served,
never other levels. The masthead shows the active level.
- **Game mechanics** β 10 XP per correct answer + combo bonus (capped),
β€οΈ 5 hearts (a mistake costs one; refill daily), π₯ streak (once per day
per completed lesson), levels 1-10 with BronzeβDiamond leagues.
- **Exercise types** β five, cycling through each lesson so it never
feels like one quiz:
- `mc` β βWhat does X mean?β multiple choice
- `fill` β pick the word that fits a sentence blank
- `type` β TYPE the missing word (no choices at all)
- `order` β build the sentence by tapping scrambled word tiles
(with a clear/undo button)
- `flip` β flashcard: flip the card, then self-rate Again / Hard /
Good / Easy β mapped straight onto the FSRS ratings, so reviews get
a real difficulty signal and XP (0/8/10/12) instead of just right/wrong
- For a **pure flashcards session** (no quiz questions), the
`duo_flashcards` UI serves flip cards only β flip to reveal the
definition, self-rate, and the FSRS schedule updates per card
- **Resources/prompts** β `duo://profile` and `duo://due` give the
assistant live stats. The AI-generation reference resources:
`duo://guide/{level}` (the per-level generation prompt β learner
profile, word scope, definition/example rules, output format),
`duo://words/{level}` (existing words at a level, to match style and
avoid duplicates), and `duo://levels` (overview + word counts). The
`daily-english` prompt wires everything into a daily routine.
## Wiring into the ai-backend-lab agent
One server, one entry β the agent sees all three UI tools plus the resources
and prompt:
```json
{
"mcp-apps-lab": {
"url": "http://127.0.0.1:8090/mcp",
"transport": "streamable_http"
}
}
```
Then ask the agent something like *"give me a quiz about Python"*, *"show me
the weather in Tokyo"*, *"curate today's financial news"*, or *"let's do my
daily English practice"* β it calls the UI tool, and the agent's reply
streams a structured tool event the frontend renders as the interactive app.
## How it's structured
```python
# server.py β one server, apps filtered by config.json, plus resources & prompts
mcp = build_server() # providers = apps enabled in config.json
register_resources(mcp) # news://{source}/feed, weather://{city}/current, duo://profile, ...
register_prompts(mcp) # morning-briefing, daily-english
# apps/weather.py β the UI app owns its backend tool
app = FastMCPApp("Weather")
app.add_tool(get_weather) # from mcp_apps_lab.tools β hashed to this app
@app.ui() # LLM-facing entry point β returns a PrefabApp
def weather_app(city: str) -> PrefabApp: ...
```
- `@app.ui()` tools are the only ones advertised to the LLM; their result is a
Prefab UI the host renders (the model sees a text summary).
- Backend tools (`tools/`) are plain functions registered with
`app.add_tool(...)`: the renderer calls them over the MCP server under a
hashed name (`<sha256(app+tool)>_<tool>`), so the UI can grade, look up, or
compile without the LLM being in the loop β and the tool proxy never sees
the mapping.
- `providers=[...]` lets one server host several apps; resources and prompts
are registered server-side and shared.
## Adding to the lab
1. **App**: create `apps/<name>.py` (a `FastMCPApp` with an `@app.ui()`
entry point), register its backend tool(s) from `tools/`, and add the app
to `providers=[...]` in `server.py`.
2. **Backend tool**: add a plain function to `tools/<name>.py`; register it
with `app.add_tool(...)` in the owning app.
3. **Resource / prompt**: add a `register(mcp)` function in
`resources/<name>.py` / `prompts/<name>.py` and call it from the
corresponding `register_*` in `server.py`.
4. Keep it lint-clean and tested: `uv run ruff check .` and
`uv run pytest` (smoke tests live in `tests/test_server.py`).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues