Container/runner/MCP lab polish (#41,#42,#45,#46) (#65)

Co-authored-by: claude <claude@jpaul.io>
Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #65.
This commit is contained in:
2026-06-22 17:21:16 -04:00
committed by Claude (agent)
parent 6691755060
commit 315cb2c190
5 changed files with 51 additions and 15 deletions
@@ -173,7 +173,11 @@ containerize and run the app you already have.
- The `tasks-app` folder from Module 1 (`tasks.py`, `cli.py`). - The `tasks-app` folder from Module 1 (`tasks.py`, `cli.py`).
- A container engine. **Docker Desktop** (macOS/Windows) or **Docker Engine** (Linux) is the common - A container engine. **Docker Desktop** (macOS/Windows) or **Docker Engine** (Linux) is the common
choice; **Podman** works too and the commands below map 1:1 (`podman` for `docker`). Verify with choice; **Podman** works too and the commands below map 1:1 (`podman` for `docker`). Verify with
`docker --version` (or `podman --version`). `docker --version` (or `podman --version`). **The engine must be *running* before you build:**
`docker --version` reports the client version even when the engine is stopped, so it's false
reassurance — `docker build` then fails with "Cannot connect to the Docker daemon." On
macOS/Windows start it first (launch Docker Desktop, or `podman machine start`); confirm the daemon
is up with `docker info` (or `podman info`), which only succeeds when the engine is actually live.
- The starter files from this module's `lab/`: [`Dockerfile`](lab/Dockerfile) and - The starter files from this module's `lab/`: [`Dockerfile`](lab/Dockerfile) and
[`dockerignore-starter`](lab/dockerignore-starter). [`dockerignore-starter`](lab/dockerignore-starter).
- Your AI assistant. - Your AI assistant.
@@ -234,6 +238,16 @@ containerize and run the app you already have.
> Git Bash), or from PowerShell — `${PWD}` resolves correctly in each. The other `docker run` > Git Bash), or from PowerShell — `${PWD}` resolves correctly in each. The other `docker run`
> commands mount nothing of yours and are identical everywhere. > commands mount nothing of yours and are identical everywhere.
> **On native Linux:** the container runs as root by default, and the bind mount maps that straight
> onto your real project folder — so the `__pycache__` directories Python writes during the test
> run land in your repo owned by `root:root`, and you can't delete them without `sudo rm -rf`.
> Prevent it by telling Python not to write bytecode in the container: add
> `-e PYTHONDONTWRITEBYTECODE=1` to the `docker run` line (with pytest you'd also pass
> `pytest -p no:cacheprovider` to suppress `.pytest_cache`). A `.gitignore` won't help — it hides
> the files from Git but they're still on disk and still sudo-only to remove. Avoid `--user
> $(id -u):$(id -g)` here: it fixes ownership but breaks any in-container `pip install` into the
> image's root-owned site-packages.
This is, in miniature, exactly what containerized CI does. If it passes here, it passes the same This is, in miniature, exactly what containerized CI does. If it passes here, it passes the same
way on any machine with the engine — your laptop's local Python version is now irrelevant. way on any machine with the engine — your laptop's local Python version is now irrelevant.
@@ -208,7 +208,10 @@ account. The five deploy steps are real; only the *target* is your laptop instea
**You'll need:** **You'll need:**
- A container runtime from Module 16 — Docker or Podman. (Commands below use `docker`; if you run - A container runtime from Module 16 — Docker or Podman. (Commands below use `docker`; if you run
Podman, `alias docker=podman` or substitute.) Podman, `alias docker=podman` or substitute.) As in Module 16, the engine must be **running**
before you build or deploy — on macOS/Windows start Docker Desktop (or `podman machine start`);
`docker --version` succeeds even when the engine is stopped, so confirm it's live with
`docker info` first, or `deploy.sh`'s build step fails with "Cannot connect to the Docker daemon."
- The `tasks-app` from Modules 12, now a Git repo. - The `tasks-app` from Modules 12, now a Git repo.
- `curl` (for the health check) and a bash-capable shell. On Windows, use WSL or Git Bash. - `curl` (for the health check) and a bash-capable shell. On Windows, use WSL or Git Bash.
- Your AI assistant — by now, ideally editor-integrated (Module 4). - Your AI assistant — by now, ideally editor-integrated (Module 4).
@@ -222,7 +222,9 @@ a repo also works). If a real runner is too heavy right now, Track A alone satis
(the same place your Module 14 `ci.yml` lives — for Actions-style forges that's (the same place your Module 14 `ci.yml` lives — for Actions-style forges that's
`.github/`/`.forgejo/`/`.gitea/` under `workflows/`; the file comments tell you where). Commit and `.github/`/`.forgejo/`/`.gitea/` under `workflows/`; the file comments tell you where). Commit and
push. It runs the same lint-and-test as Module 14, then prints the runner's hostname, OS, user, push. It runs the same lint-and-test as Module 14, then prints the runner's hostname, OS, user,
whether it looks ephemeral, and whether it can reach the public internet. whether it looks ephemeral, and whether it can reach the public internet. The receipt step carries
`if: always()` so it still prints even when lint or test fail — a diagnostic shouldn't disappear on
a red build (the job still reports red). On GitLab CI the same idea is `when: always` on the job.
2. **Read the receipt.** Open the job logs on your forge and read the `Where did this run?` step. 2. **Read the receipt.** Open the job logs on your forge and read the `Where did this run?` step.
You're now able to answer, for a real job, the question this module opened with: *whose computer You're now able to answer, for a real job, the question this module opened with: *whose computer
@@ -44,7 +44,11 @@ jobs:
run: pytest -q run: pytest -q
# The point of THIS workflow: make the runner identify itself. # The point of THIS workflow: make the runner identify itself.
# if: always() so the receipt prints even when Lint/Test fail above — a diagnostic step
# shouldn't vanish on a red build. The job still reports red; only this step is unconditional.
# (On GitLab CI the same idea is `when: always` on the job/step.)
- name: Where did this run? - name: Where did this run?
if: always()
shell: bash shell: bash
run: | run: |
echo "=== runner identity ===" echo "=== runner identity ==="
@@ -35,7 +35,8 @@ By the end of this module you can:
1. Explain the MCP client/server model — what a server exposes (tools, resources, prompts), what the 1. Explain the MCP client/server model — what a server exposes (tools, resources, prompts), what the
client (your agentic tool) does, and why "it's a protocol, not a vendor feature" is the whole client (your agentic tool) does, and why "it's a protocol, not a vendor feature" is the whole
point. point.
2. Connect an existing MCP server to your agentic tool and confirm the AI can call its tools. 2. Connect an MCP server to your agentic tool and confirm the AI can call its tools — an existing
reference server (the optional Part A warm-up) or the one you build in Part B/C.
3. Build a tiny MCP server in Python that exposes one real capability over the `tasks-app`, and wire 3. Build a tiny MCP server in Python that exposes one real capability over the `tasks-app`, and wire
it into your tool. it into your tool.
4. Watch the AI *use* that server — read and change real state through a tool call — and verify the 4. Watch the AI *use* that server — read and change real state through a tool call — and verify the
@@ -243,6 +244,9 @@ is the one that lands the concept.
**Python packages and which `python`** note just below *before* you run `pip`. **Python packages and which `python`** note just below *before* you run `pip`.
- The starter files in this module's `lab/` folder: `tasks_mcp_server.py` and - The starter files in this module's `lab/` folder: `tasks_mcp_server.py` and
`mcp-config-example.json`. `mcp-config-example.json`.
- **Only for the optional Part A warm-up:** the reference server your tool points you at typically
runs via `npx` (needs Node) or `uvx` (needs uv) — install whichever its documented `command`
needs. Part B/C, the load-bearing path, need only the Python SDK above, so you can skip this.
> **Python packages and which `python`.** This lab's one dependency is the MCP SDK, and *how* you > **Python packages and which `python`.** This lab's one dependency is the MCP SDK, and *how* you
> install it decides whether the server ever connects. Two things bite people: > install it decides whether the server ever connects. Two things bite people:
@@ -273,15 +277,22 @@ is the one that lands the concept.
> ~/workflow-course/tasks-app/.venv/bin/python -c "import mcp; print('mcp ok')" > ~/workflow-course/tasks-app/.venv/bin/python -c "import mcp; print('mcp ok')"
> ``` > ```
### Part A — Connect an existing server (warm-up, ~10 min) ### Part A — Connect an existing server (optional warm-up, ~10 min)
Before building anything, prove the plumbing works by connecting a server someone else already This part is **optional**: it proves the plumbing works by connecting a server someone else already
wrote. The MCP ecosystem ships a set of **reference servers** (filesystem, fetch, git, and more) — wrote, but it's a warm-up, not the load-bearing concept — Part B/C land that on the Python SDK you
pick a simple, read-only one your tool's docs point you at (a "filesystem" or "fetch" server is a already installed. The catch is the runtime: most **reference servers** (filesystem, fetch, git, and
good first choice). more) are distributed for `npx` (Node) or `uvx` (uv), *not* Python, so this warm-up needs whichever
runtime its documented command uses. If you don't already have Node or uv and don't want to install
one for a 10-minute warm-up, **skip straight to Part B** — you lose nothing the rest of the lab needs.
To do it: pick a simple, read-only reference server your tool's docs point you at (a "filesystem" or
"fetch" server is a good first choice), and install the runtime its command needs (Node for `npx`, uv
for `uvx`).
1. Add the server to your tool's MCP config, following the tool's docs. Most reference servers are 1. Add the server to your tool's MCP config, following the tool's docs. Most reference servers are
launched the same stdio way as the JSON shape shown in *Key concepts* — a `command` and `args`. launched the same stdio way as the JSON shape shown in *Key concepts* — a `command` (e.g. `npx` or
`uvx`) and `args`.
2. Restart or reload your agentic tool so it picks up the config. Confirm it reports the server as 2. Restart or reload your agentic tool so it picks up the config. Confirm it reports the server as
**connected** and lists its tools. **connected** and lists its tools.
3. Ask the AI to do something only that server enables — e.g. with a fetch server, *"fetch 3. Ask the AI to do something only that server enables — e.g. with a fetch server, *"fetch
@@ -429,8 +440,9 @@ The honest caveats — and one of them is large enough that it gets its own modu
**You're done when:** **You're done when:**
- You connected an **existing** MCP server to your agentic tool and watched the AI call one of its - (Optional, Part A) If you ran the warm-up, you connected an **existing** reference MCP server to
tools (Part A). your agentic tool and watched the AI call one of its tools. Skipping it costs nothing — Part C
connects the server you build and shows the same tool call.
- You built `tasks_mcp_server.py`, wired it into your tool, and saw the `tasks` server report as - You built `tasks_mcp_server.py`, wired it into your tool, and saw the `tasks` server report as
connected with `list_tasks` and `add_task` available. connected with `list_tasks` and `add_task` available.
- You asked the AI a question and it answered by **calling a tool** against the live system, and you - You asked the AI a question and it answered by **calling a tool** against the live system, and you
@@ -461,8 +473,9 @@ MCP is moving fast; re-check these at build/publish time rather than trusting th
- [ ] **The `mcpServers` config shape.** Confirm it's still the widely-shared convention for stdio - [ ] **The `mcpServers` config shape.** Confirm it's still the widely-shared convention for stdio
servers, and that the `command`/`args` fields are current. Keep the lesson tool-agnostic about servers, and that the `command`/`args` fields are current. Keep the lesson tool-agnostic about
*where* the config file lives. *where* the config file lives.
- [ ] **Reference servers (Part A).** Verify which first-party reference servers exist and how - [ ] **Reference servers (optional Part A).** Verify which first-party reference servers exist and
they're launched today; the catalogue and launch commands change. Don't name a specific server how they're launched today; the catalogue and launch commands change. Don't name a specific
that may have moved or been retired without checking. server that may have moved or been retired without checking. Confirm the named runtimes (`npx`
via Node, `uvx` via uv) are still how the common reference servers are distributed.
- [ ] **Adoption framing.** Re-confirm the "open standard, adopted across vendors regardless of - [ ] **Adoption framing.** Re-confirm the "open standard, adopted across vendors regardless of
model" claim is still accurate and still vendor-neutral; update if the ecosystem has shifted. model" claim is still accurate and still vendor-neutral; update if the ecosystem has shifted.