fix(M7-27+capstone): apply AI-drives-git reframe, lesson=theory, de-slop course-wide
Phase 2 sweep — all modules are post-pivot, so the learner directs the AI agent
(Claude Code as the worked example) to do the git/setup work and verifies, instead
of typing commands by hand; no re-teaching basics. Lesson sections are theory with
example output; all execution lives in the labs. De-slopped ("prose" etc. gone
course-wide, em-dash density thinned). /path/to placeholders -> ~/ai-workflow-course.
Every deliberate teaching device verified intact: M10 ai-change.patch trap,
M12 bad-clear-snippet, M13/M27 planted pending_count bug, M15 secret+typosquat+MD5,
M18 BREAK=1, M21 absent-.gitignore, M22 poisoned skill, M24 no-op patch, M25 --simulate.
Labs compile/parse (py/sh/yaml/json); no junk.
Closes #83
Closes #86
Closes #89
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Module 20 — MCP Servers: Giving the AI Hands
|
||||
|
||||
> **Until now the AI could read and write files in your repo and nothing else. MCP lets it reach
|
||||
> your real tools, data, and systems — your task tracker, your database, your docs, your APIs —
|
||||
> your real tools, data, and systems (your task tracker, your database, your docs, your APIs)
|
||||
> through a standard interface instead of working blind.** And because MCP is an open protocol, not
|
||||
> a vendor feature, the connections you build outlive whichever model you're running.
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Module 1** — the `tasks-app` running example, an editor, and a terminal. The lab gives the AI
|
||||
hands on this exact app.
|
||||
- **Module 2** — you read a project's state from Git and you trust `git restore` to undo a mess.
|
||||
- **Module 1** gave you the `tasks-app` running example, an editor, and a terminal. The lab gives
|
||||
the AI hands on this exact app.
|
||||
- **Module 2** taught you to read a project's state from Git and trust `git restore` to undo a mess.
|
||||
That safety net matters more here than anywhere so far: you're about to let the AI *act on real
|
||||
systems*, not just edit files.
|
||||
- **Module 4** — the AI lives in your editor or CLI (an "agentic tool") and edits files directly.
|
||||
That same tool is the **MCP client** in this module; MCP is how you extend what it can reach.
|
||||
- **Module 5** — you commit the AI's config to the repo. MCP server configuration is more config
|
||||
- **Module 4** put the AI in your editor or CLI (an "agentic tool"), editing files directly. That
|
||||
same tool is the **MCP client** in this module; MCP is how you extend what it can reach.
|
||||
- **Module 5** had you commit the AI's config to the repo. MCP server configuration is more config
|
||||
worth committing, and the same "make it travel with the repo" instinct applies.
|
||||
|
||||
Helpful but not required: **Module 16** (containers) and **Module 17** (secrets) get referenced when
|
||||
@@ -32,14 +32,14 @@ editing your code and shipping it. Unit 4 is about giving it reach beyond the re
|
||||
|
||||
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 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.
|
||||
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 what makes
|
||||
your work survive a model swap.
|
||||
2. Connect an MCP server to your agentic tool and confirm the AI can call its tools, using either 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
|
||||
4. Watch the AI *use* that server (read and change real state through a tool call) and verify the
|
||||
effect outside the chat.
|
||||
5. State precisely what MCP does and doesn't give you, including the one caveat this module
|
||||
deliberately defers: **installing an MCP server is installing code that runs with access to your
|
||||
@@ -52,23 +52,23 @@ By the end of this module you can:
|
||||
### The wall the AI keeps hitting
|
||||
|
||||
Everything so far has given the AI exactly one kind of reach: **files in your repo.** Module 4 let
|
||||
it read and write `cli.py`; Module 2 let it read your Git history. That's a lot — but watch where it
|
||||
it read and write `cli.py`; Module 2 let it read your Git history. That's a lot, but watch where it
|
||||
stops.
|
||||
|
||||
Ask your agentic tool, *"how many tasks are in my list and which are done?"* and it can answer,
|
||||
because the data happens to live in a file it can read. Now ask it something one inch further out:
|
||||
|
||||
- *"How many active users signed up this week?"* — the answer is in a database it can't query.
|
||||
- *"Is this docs page out of date versus the changelog?"* — the docs live in a system it can't read.
|
||||
- *"File a ticket for this bug."* — the tracker is an API it can't call.
|
||||
- *"How many active users signed up this week?"* The answer is in a database it can't query.
|
||||
- *"Is this docs page out of date versus the changelog?"* The docs live in a system it can't read.
|
||||
- *"File a ticket for this bug."* The tracker is an API it can't call.
|
||||
|
||||
The AI's response to all three is some flavour of *"I can't access that, but here's a script you
|
||||
could run"* — and you're back in the copy-paste loop from Module 1, just one level up. The model is
|
||||
could run,"* and you're back in the copy-paste loop from Module 1, just one level up. The model is
|
||||
plenty smart enough to do the work. It's **blind and handless** beyond your files. It can reason
|
||||
about your systems; it can't *touch* them.
|
||||
|
||||
You could solve this the bad way: paste a database dump into the chat, copy the AI's SQL out and run
|
||||
it yourself, paste the results back. That's Module 1's seam all over again — you as the integration
|
||||
it yourself, paste the results back. That's Module 1's seam all over again: you as the integration
|
||||
layer, manually shuttling data between the AI and the real system. MCP exists to delete that loop.
|
||||
|
||||
### What MCP is
|
||||
@@ -76,7 +76,7 @@ layer, manually shuttling data between the AI and the real system. MCP exists to
|
||||
The **Model Context Protocol (MCP)** is an open standard for connecting AI applications to external
|
||||
tools and data through a uniform interface. Two roles:
|
||||
|
||||
- An **MCP server** exposes capabilities — "here are the things I can do and the data I can provide."
|
||||
- An **MCP server** exposes capabilities: "here are the things I can do and the data I can provide."
|
||||
- An **MCP client** (embedded in your agentic tool) discovers those capabilities and calls them on
|
||||
the AI's behalf.
|
||||
|
||||
@@ -87,25 +87,24 @@ system, and the result comes back into the AI's context. No pasting, no scripts
|
||||
|
||||
If you've ever written or consumed an HTTP API, the instinct transfers cleanly: a server advertises
|
||||
a set of operations; a client calls them with arguments and gets structured results back. The
|
||||
difference is what it's *for* — MCP is shaped specifically so an AI can **discover** what's available
|
||||
difference is what it's *for*: MCP is shaped specifically so an AI can **discover** what's available
|
||||
at runtime (names, descriptions, argument schemas) and decide which call to make, rather than a human
|
||||
reading docs and hardcoding the call.
|
||||
|
||||
### Why "a protocol, not a vendor feature" is the whole point
|
||||
### Why "a protocol, not a vendor feature" changes everything
|
||||
|
||||
This is the course thesis showing up in the architecture itself. MCP is a **standard**, like HTTP or
|
||||
SQL — not a button inside one company's product. The consequences are exactly the ones this course
|
||||
SQL, not a button inside one company's product. The consequences are exactly the ones this course
|
||||
keeps promising:
|
||||
|
||||
- **Write a server once; every compliant client can use it.** The `tasks` server you'll build in the
|
||||
lab works with any agentic tool that speaks MCP — today's and next year's. You are not building for
|
||||
lab works with any agentic tool that speaks MCP, today's and next year's. You are not building for
|
||||
a vendor; you're building for the protocol.
|
||||
- **Swap the model underneath and your servers don't care.** The server exposes `add_task`; it has
|
||||
no idea which model is on the other end of the client. Change models — which you will — and every
|
||||
connection you built keeps working. That's the durable-skill payoff stated in Module 1, now load-
|
||||
bearing instead of aspirational.
|
||||
- **The ecosystem compounds.** Because it's a shared standard, there's a large and growing catalogue
|
||||
of servers other people already wrote — for databases, cloud providers, ticket trackers, docs,
|
||||
no idea which model is on the other end of the client. Change models, which you will, and every
|
||||
connection you built keeps working. That's the durable-skill payoff Module 1 promised, made real.
|
||||
- **The catalogue grows on its own.** Because it's a shared standard, there's a large and growing
|
||||
set of servers other people already wrote: databases, cloud providers, ticket trackers, docs,
|
||||
browsers, your own internal tools. Connecting one is usually configuration, not coding.
|
||||
|
||||
MCP originated with one vendor and was released as an open spec; it's since been adopted across major
|
||||
@@ -119,11 +118,11 @@ An MCP server can offer three kinds of things. You'll mostly care about the firs
|
||||
- **Tools** — *actions the AI can take.* A tool is a named function with typed arguments and a
|
||||
description: `add_task(title)`, `run_query(sql)`, `create_issue(title, body)`. The AI reads the
|
||||
description, decides to call it, supplies the arguments, and gets a result. This is the "hands"
|
||||
half of the module title — tools are how the AI *does* things. (Tools can have side effects: they
|
||||
half of the module title; tools are how the AI *does* things. (Tools can have side effects: they
|
||||
write to your database, hit your API, change real state. That power is exactly why Module 22
|
||||
exists.)
|
||||
- **Resources** — *data the AI can read.* Read-only context the server makes available: a file, a
|
||||
database record, a docs page, the contents of a config. Where tools *do*, resources *inform* —
|
||||
database record, a docs page, the contents of a config. Where tools *do*, resources *inform*:
|
||||
they're how the AI gets eyes on a system, the parallel to "durable memory it can read" from
|
||||
Module 2, extended past your repo.
|
||||
- **Prompts** — *reusable prompt templates the server offers* for common operations against it (e.g.
|
||||
@@ -139,16 +138,16 @@ The client has to launch or reach the server and exchange messages with it. Two
|
||||
the distinction is practical:
|
||||
|
||||
- **stdio (local).** The client launches the server as a subprocess on your machine and talks to it
|
||||
over standard input/output — the same pipes a normal command-line program uses. This is the right
|
||||
over standard input/output, the same pipes a normal command-line program uses. This is the right
|
||||
default for anything local: your `tasks` server, a server that reads your filesystem, one that
|
||||
drives a local tool. No network, no ports, no auth to set up. **This is what the lab uses.**
|
||||
- **HTTP-based (remote).** For a server running somewhere else — a shared internal service, a
|
||||
vendor's hosted server — the client reaches it over HTTP. This is where authentication and network
|
||||
- **HTTP-based (remote).** For a server running somewhere else (a shared internal service, a
|
||||
vendor's hosted server), the client reaches it over HTTP. This is where authentication and network
|
||||
access enter the picture, and where the security stakes climb.
|
||||
|
||||
You don't pick the transport at random; it follows from where the server runs. Local tool over a
|
||||
real system on your box → stdio. Shared or third-party service → HTTP. (The exact name of the HTTP
|
||||
transport in the spec has changed more than once — see *Verify-before-publish* — but the local-vs-
|
||||
transport in the spec has changed more than once (see *Verify-before-publish*), but the local-vs-
|
||||
remote split is the durable idea.)
|
||||
|
||||
### Configuring a server: where the wiring lives
|
||||
@@ -162,7 +161,7 @@ like this:
|
||||
"mcpServers": {
|
||||
"tasks": {
|
||||
"command": "python",
|
||||
"args": ["/absolute/path/to/tasks-app/tasks_mcp_server.py"]
|
||||
"args": ["/home/you/ai-workflow-course/tasks-app/tasks_mcp_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,17 +170,17 @@ like this:
|
||||
Read it plainly: *"there's a server called `tasks`; to start it, run `python <that file>` and talk to
|
||||
it over stdio."* That's the whole contract for a local server.
|
||||
|
||||
Two honest notes, both flowing from the course's core promises:
|
||||
Two notes, both flowing from the course's core promises:
|
||||
|
||||
- **The filename and location of this config are tool-specific, and we won't pin them.** Some tools
|
||||
keep it in a project file, some in a user-level file, some let you add servers from a UI. The
|
||||
`mcpServers` *shape* above is widely shared, but check your tool's docs for where it reads it. The
|
||||
principle — "a server is a name plus how to launch or reach it" — outlives any one tool's filename,
|
||||
principle ("a server is a name plus how to launch or reach it") outlives any one tool's filename,
|
||||
exactly like the committed-instructions file in Module 5.
|
||||
- **This config is worth committing — with care.** A project-level MCP config means every teammate
|
||||
- **This config is worth committing, with care.** A project-level MCP config means every teammate
|
||||
and every agent that opens the repo gets the same tools wired up, which is the Module 5 instinct
|
||||
applied one level out. But MCP config often points at paths or, for HTTP servers, endpoints and
|
||||
credentials — and **credentials never go in the repo** (that's Module 17, and it's a hard rule).
|
||||
credentials, and **credentials never go in the repo** (that's Module 17, and it's a hard rule).
|
||||
Commit the wiring; keep the secrets in the environment.
|
||||
|
||||
### Where this is in the repo's reach, and where it's heading
|
||||
@@ -189,7 +188,7 @@ Two honest notes, both flowing from the course's core promises:
|
||||
Stack the units up and the picture is clear. Module 4 put the AI in your editor. This module gives
|
||||
that same AI hands beyond the repo. The next three modules build directly on it:
|
||||
|
||||
- **Module 21 (Skills)** teaches the AI *playbooks* — repeatable procedures it runs your way. Skills
|
||||
- **Module 21 (Skills)** teaches the AI *playbooks*, repeatable procedures it runs your way. Skills
|
||||
and MCP compose: MCP gives the AI the tools; a skill tells it *how and when* to use them.
|
||||
- **Module 22 (Securing third-party MCP servers and skills)** handles the danger this module is
|
||||
deliberately deferring (see *Where it breaks*). Read it before you install anything you didn't
|
||||
@@ -201,24 +200,24 @@ that same AI hands beyond the repo. The next three modules build directly on it:
|
||||
|
||||
## The AI angle
|
||||
|
||||
Most integration work wires systems together for *programs* to use — fixed clients calling fixed
|
||||
Most integration work wires systems together for *programs* to use: fixed clients calling fixed
|
||||
endpoints. MCP is shaped for a different consumer: **an AI that decides at runtime what it needs.**
|
||||
That changes what matters about the integration.
|
||||
|
||||
- **Discovery, not hardcoding.** A traditional client is written against specific API calls by a
|
||||
human. An MCP client hands the AI a *menu* — tool names, descriptions, argument schemas — and the
|
||||
human. An MCP client hands the AI a *menu* (tool names, descriptions, argument schemas) and the
|
||||
AI picks. Which means the **description you write for a tool is part of the interface**: it's how
|
||||
the model knows when to reach for `add_task` versus `list_tasks`. A vague docstring is a vague tool.
|
||||
(You'll feel this in the lab — the docstrings on the server functions are not decoration; they're
|
||||
(You'll feel this in the lab: the docstrings on the server functions are not decoration; they're
|
||||
what the AI reads.)
|
||||
- **It closes Module 1's loop at the systems layer.** The original copy-paste pain was shuttling code
|
||||
between a chat and a file. The same pain reappears one level out: shuttling *data* between the AI
|
||||
and your database, your tracker, your docs. MCP is the editor-integration moment for systems — the
|
||||
and your database, your tracker, your docs. MCP is the editor-integration moment for systems: the
|
||||
AI reaches them directly instead of you being the integration layer.
|
||||
- **It's the model-agnostic bet made concrete.** Every other module argues the workflow outlasts the
|
||||
model. MCP *is* that argument in protocol form: the server you write is bound to a standard, not a
|
||||
model. Swap the model and your hands stay attached.
|
||||
- **The reach is the risk.** The very thing that makes MCP powerful — real access to real systems —
|
||||
- **The reach is the risk.** The very thing that makes MCP powerful, real access to real systems,
|
||||
is why it needs its own security module. An AI with hands can do real damage as easily as real
|
||||
work. That's not a reason to avoid it; it's the reason Module 22 comes right after.
|
||||
|
||||
@@ -231,71 +230,74 @@ machine, any OS.
|
||||
|
||||
You'll do two things: **connect an existing MCP server** to confirm the client/server wiring works
|
||||
at all, then **build your own tiny server** over the `tasks-app` and watch the AI use it. The second
|
||||
is the one that lands the concept.
|
||||
is where the idea sticks.
|
||||
|
||||
**You'll need:**
|
||||
|
||||
- The `tasks-app` from Module 1/2 (a folder with `tasks.py`, `cli.py`, and ideally a Git repo so you
|
||||
can see and undo what the AI does — Module 2).
|
||||
can see and undo what the AI does, per Module 2).
|
||||
- Your agentic coding tool from Module 4, which is the **MCP client**. Find, in its docs, *where it
|
||||
reads MCP server configuration* and *how it shows that a server is connected* (often a list of
|
||||
connected servers or available tools).
|
||||
- Python 3.10+ and the official MCP Python SDK, installed into a virtual environment — read the
|
||||
**Python packages and which `python`** note just below *before* you run `pip`.
|
||||
- Python 3.10+ and the official MCP Python SDK, installed into a virtual environment. Read the
|
||||
**Python packages and which `python`** note just below before you have the agent set this up.
|
||||
- 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.
|
||||
runs via `npx` (needs Node) or `uvx` (needs uv); install whichever its documented `command`
|
||||
needs. Part B/C 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:
|
||||
> **Python packages and which `python`.** This lab's one dependency is the MCP SDK, and *how* it
|
||||
> gets installed decides whether the server ever connects. Two things bite people, and one is the
|
||||
> reason you point the agent at the work and then check the result yourself:
|
||||
>
|
||||
> - **PEP 668 ("externally-managed-environment").** On modern Debian/Ubuntu and Homebrew Python, a
|
||||
> global `pip install` is refused on purpose. The clean fix is a virtual environment per project:
|
||||
> global `pip install` is refused on purpose. The clean fix is a virtual environment per project.
|
||||
> Direct Claude Code (or sub your own agent) to set it up:
|
||||
>
|
||||
> ```bash
|
||||
> cd ~/ai-workflow-course/tasks-app
|
||||
> python3 -m venv .venv # one-time
|
||||
> source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
> python3 -m pip install "mcp[cli]"
|
||||
> ```
|
||||
> > *"In `~/ai-workflow-course/tasks-app`, create a `.venv` virtual environment, install `mcp[cli]`
|
||||
> > into it, then tell me the absolute path to that venv's python interpreter."*
|
||||
>
|
||||
> (If you'd rather not manage a venv: `pipx`, or `pip install --break-system-packages` — but a venv
|
||||
> is the clean default and keeps this lab's dependency out of your system Python.)
|
||||
> - **The install interpreter must match the config's launch command.** Your MCP client starts the
|
||||
> server by running the `"command"` in its config — *not* your activated shell — so activating a
|
||||
> venv does nothing to help the client find the SDK. You must point `"command"` at the venv's
|
||||
> **absolute** python path (e.g. `~/ai-workflow-course/tasks-app/.venv/bin/python`, or
|
||||
> `...\.venv\Scripts\python.exe` on Windows). If they don't match, the server dies on `import mcp`
|
||||
> and your tool just says "not connected" with no obvious reason — the exact failure this lab is
|
||||
> about avoiding.
|
||||
> It will run the equivalent of `python3 -m venv .venv` and `.venv/bin/python -m pip install
|
||||
> "mcp[cli]"`, and report a path like `/home/you/ai-workflow-course/tasks-app/.venv/bin/python`.
|
||||
> (If you'd rather not use a venv, the agent can fall back to `pipx` or
|
||||
> `pip install --break-system-packages`; a venv is the clean default and keeps this dependency out
|
||||
> of your system Python.)
|
||||
> - **The install interpreter must match the config's launch command.** This is the load-bearing
|
||||
> gotcha of the whole lab, so understand it even though the agent does the typing. Your MCP client
|
||||
> starts the server by running the `"command"` in its config, *not* from your activated shell, so
|
||||
> activating a venv does nothing to help the client find the SDK. The config's `"command"` must be
|
||||
> the venv's **absolute** python path (the one the agent just reported, e.g.
|
||||
> `/home/you/ai-workflow-course/tasks-app/.venv/bin/python`, or `...\.venv\Scripts\python.exe` on
|
||||
> Windows). If they don't match, the server dies on `import mcp` and your tool just says "not
|
||||
> connected" with no obvious reason: the exact failure this lab is about avoiding.
|
||||
>
|
||||
> Before wiring anything, verify with the *same* interpreter the config will launch:
|
||||
> Before wiring anything, confirm the SDK is reachable from the *same* interpreter the config will
|
||||
> launch. Run this one-line check yourself against the path the agent reported:
|
||||
>
|
||||
> ```bash
|
||||
> ~/ai-workflow-course/tasks-app/.venv/bin/python -c "import mcp; print('mcp ok')"
|
||||
> /home/you/ai-workflow-course/tasks-app/.venv/bin/python -c "import mcp; print('mcp ok')"
|
||||
> ```
|
||||
|
||||
### Part A — Connect an existing server (optional warm-up, ~10 min)
|
||||
|
||||
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
|
||||
wrote, but it's a warm-up. Parts B/C carry the real lesson 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.
|
||||
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` (e.g. `npx` or
|
||||
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
|
||||
3. Ask the AI to do something only that server enables. For example, with a fetch server, *"fetch
|
||||
example.com and summarize it"*; with a filesystem server scoped to a folder, *"list the files in
|
||||
that folder."* Watch the AI **call a tool** rather than tell you it can't.
|
||||
|
||||
@@ -303,14 +305,21 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
|
||||
> **Stop before you install anything you don't fully trust.** A reference server from the protocol's
|
||||
> own maintainers is a reasonable warm-up. A random server off the internet is untrusted code that
|
||||
> will run with your permissions — vetting that is **Module 22's** job, and it's not optional. For
|
||||
> will run with your permissions; vetting that is **Module 22's** job, and it's not optional. For
|
||||
> now, stick to first-party reference servers or the one you write next.
|
||||
|
||||
### Part B — Build a one-tool server over the tasks-app
|
||||
|
||||
1. Copy this module's `lab/tasks_mcp_server.py` into your `tasks-app` folder, next to `tasks.py` and
|
||||
`cli.py`. (It reuses `tasks.py` and shares the same `tasks.json`, so anything it changes shows up
|
||||
in `python cli.py list`.) The whole server is two tools:
|
||||
1. Have Claude Code (or sub your own agent) copy this module's `lab/tasks_mcp_server.py` into your
|
||||
`tasks-app` folder, next to `tasks.py` and `cli.py`, and confirm it landed there:
|
||||
|
||||
> *"Copy the starter file at `modules/20-mcp-servers-giving-the-ai-hands/lab/tasks_mcp_server.py`
|
||||
> into `~/ai-workflow-course/tasks-app/`, next to `tasks.py` and `cli.py`, then show me the
|
||||
> contents so I can read it."*
|
||||
|
||||
Then open the copied file yourself and read it. (It reuses `tasks.py` and shares the same
|
||||
`tasks.json`, so anything it changes shows up in `python cli.py list`.) The whole server is two
|
||||
tools:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
@@ -327,41 +336,50 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
return f"added: {title}"
|
||||
```
|
||||
|
||||
That's it — a tool is a normal function plus the docstring the AI reads to decide when to use it.
|
||||
That's it: a tool is a normal function plus the docstring the AI reads to decide when to use it.
|
||||
|
||||
2. Sanity-check it starts. From inside `tasks-app`:
|
||||
2. Sanity-check that it starts (optional, but it's a useful feel for what stdio does). Ask the agent
|
||||
to run the server with the venv python and report what happens:
|
||||
|
||||
```bash
|
||||
python3 -m pip install "mcp[cli]" # into the venv from the note above, once
|
||||
python tasks_mcp_server.py # it will sit there waiting for a client — that's correct
|
||||
```
|
||||
> *"Run `~/ai-workflow-course/tasks-app/.venv/bin/python tasks_mcp_server.py` from inside
|
||||
> `tasks-app` and tell me what it does, then stop it."*
|
||||
|
||||
It looks like it's hanging. It isn't — a stdio server waits for a client on its stdin/stdout.
|
||||
Press Ctrl-C; you don't run it by hand, the client launches it.
|
||||
It looks like it's hanging. It isn't: a stdio server waits for a client on its stdin/stdout, so
|
||||
there's nothing to print and no prompt to return to until a client connects. That waiting *is*
|
||||
the correct behavior. You don't run it by hand for real; the client launches it.
|
||||
|
||||
### Part C — Wire it into your agentic tool
|
||||
|
||||
3. Open `lab/mcp-config-example.json`. Copy the `tasks` entry into wherever your tool reads MCP
|
||||
config. Set `"command"` to the **absolute path of the python that has `mcp` installed** — the venv
|
||||
python from the note above, *not* a bare `python` — and set `args` to the **absolute** path to
|
||||
your `tasks_mcp_server.py`:
|
||||
3. Have the agent write the `tasks` config entry. It already knows both absolute paths (the venv
|
||||
python it just reported and the server file it just copied), so let it fill them in. Point it at
|
||||
wherever your tool reads MCP config, using `lab/mcp-config-example.json` as the shape:
|
||||
|
||||
> *"Add a `tasks` MCP server entry to <my tool's MCP config file>, using the shape in
|
||||
> `lab/mcp-config-example.json`. Set `command` to the absolute venv python path you reported and
|
||||
> `args` to the absolute path of the copied `tasks_mcp_server.py`. Do not use a bare `python`."*
|
||||
|
||||
The entry it writes should look like this, with real absolute paths swapped in for the
|
||||
placeholders:
|
||||
|
||||
```json
|
||||
"tasks": {
|
||||
"command": "/ABSOLUTE/PATH/TO/ai-workflow-course/tasks-app/.venv/bin/python",
|
||||
"args": ["/ABSOLUTE/PATH/TO/ai-workflow-course/tasks-app/tasks_mcp_server.py"]
|
||||
"command": "/home/you/ai-workflow-course/tasks-app/.venv/bin/python",
|
||||
"args": ["/home/you/ai-workflow-course/tasks-app/tasks_mcp_server.py"]
|
||||
}
|
||||
```
|
||||
|
||||
(On Windows the venv python is `...\.venv\Scripts\python.exe`.) A bare `"command": "python"` is the
|
||||
single most common reason the server "won't connect": the client launches whatever `python` is on
|
||||
*its* PATH, which is usually not the interpreter that has the SDK.
|
||||
(On Windows the venv python is `...\.venv\Scripts\python.exe`.) *Where* the config file lives is
|
||||
tool-specific; if your tool adds servers from a UI or your agent can't reach its config, edit the
|
||||
entry by hand as the fallback. Either way, a bare `"command": "python"` is the single most common
|
||||
reason the server "won't connect": the client launches whatever `python` is on *its* PATH, which
|
||||
is usually not the interpreter that has the SDK. That's why the `"command"` must be the absolute
|
||||
venv path.
|
||||
|
||||
4. Reload your agentic tool and confirm it shows the `tasks` server **connected**, with `list_tasks`
|
||||
4. Reload your agentic tool and verify it shows the `tasks` server **connected**, with `list_tasks`
|
||||
and `add_task` among its available tools. If it doesn't connect, the usual culprits are a wrong
|
||||
path, the wrong `python`, or the SDK not installed for that interpreter — re-run the
|
||||
`... .venv/bin/python -c "import mcp"` check from the note above against the *exact* path you put
|
||||
in `"command"`, then check the tool's MCP logs.
|
||||
path, the wrong `python`, or the SDK not installed for that interpreter. Re-run the
|
||||
`... .venv/bin/python -c "import mcp"` check from the note above against the *exact* path in
|
||||
`"command"`, then check the tool's MCP logs.
|
||||
|
||||
### Part D — Watch the AI use its new hands
|
||||
|
||||
@@ -369,16 +387,16 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
|
||||
> *"What's on my task list right now?"*
|
||||
|
||||
The AI should call `list_tasks` and answer from the live result — not from reading a file, not
|
||||
The AI should call `list_tasks` and answer from the live result, not from reading a file and not
|
||||
from memory. Many tools show the tool call inline ("called `tasks.list_tasks`"); watch for it.
|
||||
|
||||
6. Now have it act:
|
||||
|
||||
> *"Add a task: review the Module 20 lab."*
|
||||
|
||||
It should call `add_task("review the Module 20 lab")`. Then **verify the effect outside the AI**,
|
||||
which is the whole point — the change is real. Verify it the way you'd verify any runtime effect:
|
||||
by reading the *state*, not the repo:
|
||||
It should call `add_task("review the Module 20 lab")`. Then **verify the effect outside the AI**.
|
||||
This is the part that matters: the change is real, and the proof lives outside the chat. Check it
|
||||
the way you'd verify any runtime effect, by reading the *state*, not the repo:
|
||||
|
||||
```bash
|
||||
python cli.py list # the new task is there, because the server wrote the same tasks.json
|
||||
@@ -387,7 +405,7 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
|
||||
The AI just changed real state in a real system through a tool call. Notice what you did *not*
|
||||
reach for: `git diff`. `tasks.json` is deliberately gitignored (Module 2's `.gitignore` treats it
|
||||
as generated runtime state, not source), so `git diff` stays empty here — and that's correct, not a
|
||||
as generated runtime state, not source), so `git diff` stays empty here, and that's correct, not a
|
||||
bug. The proof the task list changed is the live state (`python cli.py list` / `cat tasks.json`),
|
||||
not version control; runtime data the app owns is exactly the kind of thing you keep *out* of
|
||||
history. No copy-paste, no script you ran by hand, no pasting `tasks.json` into a chat. That's
|
||||
@@ -402,20 +420,20 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
|
||||
## Where it breaks
|
||||
|
||||
The honest caveats — and one of them is large enough that it gets its own module.
|
||||
The caveats, and one of them is large enough that it gets its own module.
|
||||
|
||||
- **Installing an MCP server is installing code that runs with your access — and this module does not
|
||||
- **Installing an MCP server is installing code that runs with your access, and this module does not
|
||||
secure it.** A server you connect runs on your machine (stdio) or is trusted by your client (HTTP),
|
||||
with whatever permissions you give it: your files, your network, your credentials. A malicious or
|
||||
compromised server is malware with an AI driving it, and a server's tool descriptions can even
|
||||
carry instructions that try to steer the model (prompt injection). **This module deliberately
|
||||
stops here.** The attack surface — vetting servers, pinning versions, least-privilege, prompt
|
||||
injection — is **Module 22 (Securing Third-Party MCP Servers and Skills)**, and you should treat
|
||||
stops here.** The attack surface (vetting servers, pinning versions, least-privilege, prompt
|
||||
injection) is **Module 22 (Securing Third-Party MCP Servers and Skills)**, and you should treat
|
||||
it as required reading before connecting anything you didn't write. In this module: only first-
|
||||
party reference servers and the one you build yourself.
|
||||
- **A tool with side effects can do real damage as easily as real work.** Your `add_task` writes to
|
||||
real state. A `run_query` or `delete_user` tool does too. An AI that confidently calls the wrong
|
||||
tool with the wrong arguments isn't a typo in a file you can `git restore` — it might be a row
|
||||
tool with the wrong arguments isn't a typo in a file you can `git restore`; it might be a row
|
||||
deleted from a database Git never backed up (Module 12's limit). Keep destructive tools behind
|
||||
confirmation, scope them narrowly, and lean on the safety net: do this against test data first.
|
||||
- **The AI still has to *choose* the tool correctly.** MCP gives the model hands; it doesn't give it
|
||||
@@ -428,7 +446,7 @@ The honest caveats — and one of them is large enough that it gets its own modu
|
||||
kills it.")
|
||||
- **The spec and SDKs move fast.** This is expansion-zone material. Transport names, SDK APIs, and
|
||||
config conventions have all churned and will again. The *client/server, servers-offer-clients-call*
|
||||
model is durable; specific commands and field names are not — verify them at build time.
|
||||
model is durable; specific commands and field names are not, so verify them at build time.
|
||||
- **stdio servers are local-only by nature.** The lab's server runs on your machine for you. Sharing
|
||||
a server with a team, or reaching one that needs to run elsewhere, means the HTTP transport, which
|
||||
drags in auth, network access, and the containerization story from Module 16. Don't reach for that
|
||||
@@ -441,16 +459,16 @@ The honest caveats — and one of them is large enough that it gets its own modu
|
||||
**You're done when:**
|
||||
|
||||
- (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
|
||||
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
|
||||
asked it to add a task and then **verified the change outside the AI** by reading the runtime state
|
||||
(`python cli.py list` / `cat tasks.json`) — not `git diff`, because `tasks.json` is deliberately
|
||||
(`python cli.py list` / `cat tasks.json`), not `git diff`, because `tasks.json` is deliberately
|
||||
gitignored (Module 2).
|
||||
- You can explain the client/server model in one breath — *servers expose tools/resources/prompts;
|
||||
the client (your agentic tool) discovers and calls them on the AI's behalf* — and why "it's a
|
||||
- You can explain the client/server model in one breath (*servers expose tools/resources/prompts;
|
||||
the client (your agentic tool) discovers and calls them on the AI's behalf*) and why "it's a
|
||||
protocol, not a vendor feature" means your server survives a model swap.
|
||||
- You can state the one caveat this module defers: connecting an MCP server is running code with
|
||||
access to your systems, and **Module 22** is where that risk gets handled.
|
||||
|
||||
Reference in New Issue
Block a user