Notes: M16: Building MCP servers & clients
In M9 you gave an agent tools, but those tools were trapped inside your one script. If a teammate's app, or Claude Desktop, or your IDE wanted to use them, they'd each need custom wiring. MCP (Model Context Protocol) fixes that: it's an open standard for how AI apps and tools talk, so a tool you build once works in any MCP-aware app. M9 used MCP as a survey; here you actually build both sides, a server and a client, and (bonus) it all runs locally with no key.
What MCP is (and isn't)
MCP is a protocol, not a framework: a shared agreement on the messages an AI app and a tool provider exchange. The analogy from M9: "USB-C for AI tools." Before USB-C every device had its own plug; now one standard plug works everywhere. Before MCP, every app integrated every tool its own way; with MCP, a tool that speaks MCP plugs into any app that speaks MCP.
Two roles: - An MCP server exposes capabilities, tools (functions the AI can call), and also resources (data) and prompts (templates). You build a server to share your tools. - An MCP client consumes them. The client lives inside an AI app (Claude Desktop, an IDE, your agent); it connects to servers, discovers what they offer, and calls them on the model's behalf.
flowchart LR
subgraph App["AI app (has an MCP client)"]
M["model / agent"]
end
App -->|"1 list tools / 2 call tool"| S1["your MCP server<br/>(calculate, lookup_ioc, …)"]
App --> S2["other MCP servers<br/>(GitHub, files, DB, …)"]
Building a server (FastMCP)
The official mcp SDK includes FastMCP, which makes a server almost trivial: decorate a function
with @mcp.tool() and it's published, its name, description (from the docstring), and
input schema (from the type hints) are advertised to clients automatically (the same tool-schema
idea from M9/M6, now over MCP):
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("greenleaf-tools")
@mcp.tool()
def lookup_ioc(indicator: str) -> str:
"""Look up the reputation of an IP, domain, or file hash."""
...
if __name__ == "__main__":
mcp.run() # serve over stdio (the default transport)
lookup_ioc.
Building a client
A client launches/connects to a server, does the handshake (initialize), then discovers
(list_tools) and calls (call_tool), the exact dance an app like Claude Desktop performs for
you:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(command=sys.executable, args=["mcp_server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools() # discover
result = await session.call_tool("lookup_ioc", {"indicator": "8.8.8.8"}) # call
Why this matters
You already know how to give your agent tools (M9). MCP makes those tools portable: - Reuse across apps: write a tool once; use it in Claude Desktop, your IDE, your agent, a teammate's app. No per-app glue. - Use others' servers: there's a growing ecosystem of MCP servers (GitHub, file systems, databases, search). Your agent can consume them the same way. - Clean separation: the tool's logic lives in the server; the AI app just speaks MCP.
It's the difference between a tool that works in your demo and a tool the whole ecosystem can use.
Security (it still applies)
A tool exposed over MCP is still code that runs. Everything from M10 holds, more so because more apps can reach it: least privilege (only expose tools that are safe to expose), validate inputs, be wary of tool poisoning (a malicious server description trying to manipulate the model) and of connecting to untrusted servers. Treat an MCP server you didn't write like any third-party dependency, review it.
Go deeper (optional, not needed for today's win)
- **Resources & prompts:** besides tools, MCP servers can expose **resources** (read-only data the model can pull in, like files) and **prompts** (reusable prompt templates). Tools are the most common; the SDK has decorators for the others (`@mcp.resource()`, `@mcp.prompt()`). - **Transports:** **stdio** (local subprocess, what we use) and **HTTP/SSE** (remote servers over the network). The Anthropic API can also connect to remote MCP servers directly (an `mcp_servers` parameter), and the SDK has helpers to use MCP tools with the tool runner (M9 go-deeper). - **Claude Desktop / IDEs:** these ship an MCP client, add your server to their config (a small JSON entry pointing at `python mcp_server.py`) and the app gains your tools. - **Other languages:** MCP has SDKs in TypeScript, etc., the protocol is language-agnostic, which is the whole point of a standard.Check yourself
Lock in today's win, answer each in your head, then reveal.
1. What is MCP, in one line, and is it a framework?
Show answer
A standard (protocol) for how AI apps and tools talk, "USB-C for AI tools", not a framework. A tool that speaks MCP plugs into any app that speaks MCP, with no custom per-app glue.
2. What's the difference between an MCP server and an MCP client?
Show answer
A server exposes capabilities (tools, resources, prompts), you build one to share your tools. A client lives inside an AI app and consumes them, it connects, discovers, and calls. You built both.
3. How does @mcp.tool() turn a function into a tool?
Show answer
It publishes the function to clients: the name, the description (from the docstring), and the input schema (from the type hints) are advertised automatically, the same tool-schema idea as M9/M6, now over the MCP protocol.
4. What are the two steps a client does after connecting?
Show answer
Discover (list_tools, ask the server what it offers) and call (call_tool with a name +
arguments). After an initialize handshake. That discover-then-call dance is what an app like
Claude Desktop does for you.
5. Why build a tool as an MCP server instead of just inside your agent?
Show answer
Portability/reuse: write it once and any MCP-aware app (Claude Desktop, IDEs, other agents, teammates) can use it with zero extra wiring, and your agent can also consume the growing ecosystem of others' MCP servers. A tool stops being demo-only and becomes ecosystem-wide.
New words (also in resources/glossary.md): MCP (recap), MCP server,
MCP client, FastMCP, @mcp.tool(), stdio transport, tool discovery (list_tools), tool poisoning.
Source: original, written for this course. The MCP server/client code follows the official mcp
Python SDK (FastMCP + ClientSession/stdio_client) and was verified end-to-end for real: the
client launches the server over stdio, lists the tools, and calls them (see the solution README). No
API key or network needed. Diagrams are original.