From 315cb2c1903a2a488b695c1e63887f27a4415355 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 22 Jun 2026 17:21:16 -0400 Subject: [PATCH] Container/runner/MCP lab polish (#41,#42,#45,#46) (#65) Co-authored-by: claude Co-committed-by: claude --- .../README.md | 16 +++++++- .../README.md | 5 ++- .../README.md | 4 +- .../lab/whoami-runner.yml | 4 ++ .../README.md | 37 +++++++++++++------ 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/modules/16-containers-and-reproducible-environments/README.md b/modules/16-containers-and-reproducible-environments/README.md index 89f62e1..1c1a2b8 100644 --- a/modules/16-containers-and-reproducible-environments/README.md +++ b/modules/16-containers-and-reproducible-environments/README.md @@ -173,7 +173,11 @@ containerize and run the app you already have. - 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 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 [`dockerignore-starter`](lab/dockerignore-starter). - 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` > 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 way on any machine with the engine — your laptop's local Python version is now irrelevant. diff --git a/modules/18-continuous-delivery-and-deployment/README.md b/modules/18-continuous-delivery-and-deployment/README.md index b669919..4ab7e37 100644 --- a/modules/18-continuous-delivery-and-deployment/README.md +++ b/modules/18-continuous-delivery-and-deployment/README.md @@ -208,7 +208,10 @@ account. The five deploy steps are real; only the *target* is your laptop instea **You'll need:** - 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 1–2, now a Git repo. - `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). diff --git a/modules/19-runners-the-compute-behind-automation/README.md b/modules/19-runners-the-compute-behind-automation/README.md index 82b3d02..31e70c1 100644 --- a/modules/19-runners-the-compute-behind-automation/README.md +++ b/modules/19-runners-the-compute-behind-automation/README.md @@ -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 `.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, - 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. You're now able to answer, for a real job, the question this module opened with: *whose computer diff --git a/modules/19-runners-the-compute-behind-automation/lab/whoami-runner.yml b/modules/19-runners-the-compute-behind-automation/lab/whoami-runner.yml index cc823ec..434a685 100644 --- a/modules/19-runners-the-compute-behind-automation/lab/whoami-runner.yml +++ b/modules/19-runners-the-compute-behind-automation/lab/whoami-runner.yml @@ -44,7 +44,11 @@ jobs: run: pytest -q # 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? + if: always() shell: bash run: | echo "=== runner identity ===" diff --git a/modules/20-mcp-servers-giving-the-ai-hands/README.md b/modules/20-mcp-servers-giving-the-ai-hands/README.md index b1dec3f..73066cb 100644 --- a/modules/20-mcp-servers-giving-the-ai-hands/README.md +++ b/modules/20-mcp-servers-giving-the-ai-hands/README.md @@ -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 client (your agentic tool) does, and why "it's a protocol, not a vendor feature" is the whole 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 it into your tool. 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`. - The starter files in this module's `lab/` folder: `tasks_mcp_server.py` and `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 > 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')" > ``` -### 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 -wrote. The MCP ecosystem ships a set of **reference servers** (filesystem, fetch, git, and more) — -pick a simple, read-only one your tool's docs point you at (a "filesystem" or "fetch" server is a -good first choice). +This part is **optional**: it proves the plumbing works by connecting a server someone else already +wrote, but it's a warm-up, not the load-bearing concept — Part B/C land that on the Python SDK you +already installed. The catch is the runtime: most **reference servers** (filesystem, fetch, git, and +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 - 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 **connected** and lists its tools. 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 connected an **existing** MCP server to your agentic tool and watched the AI call one of its - tools (Part A). +- (Optional, Part A) If you ran the warm-up, you connected an **existing** reference MCP server to + 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 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 @@ -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 servers, and that the `command`/`args` fields are current. Keep the lesson tool-agnostic about *where* the config file lives. -- [ ] **Reference servers (Part A).** Verify which first-party reference servers exist and how - they're launched today; the catalogue and launch commands change. Don't name a specific server - that may have moved or been retired without checking. +- [ ] **Reference servers (optional Part A).** Verify which first-party reference servers exist and + how they're launched today; the catalogue and launch commands change. Don't name a specific + 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 model" claim is still accurate and still vendor-neutral; update if the ecosystem has shifted.