Claude with MCP
The Model Context Protocol is the USB-C of AI tooling: write a capability once as an MCP server, and any MCP-aware client — Claude Code, Claude Desktop, or your own API code — can use it. This chapter is the Claude-specific half: how to actually connect servers to Claude and let it call them.
Learning objectives
- Explain what MCP is and the problem it solves.
- Connect Claude to an MCP server and use its tools.
- Build a minimal MCP server exposing a tool.
- Reason about MCP security and standardize servers for a team.
30-second recap
- The Claude API: One call —
messages.create— takes a model, a system prompt and messages, and returns content blocks; tools and streaming are options on the same call. - Agent & tool foundations: The model asks to call a tool, your code runs it and returns the result, and the loop repeats until the model answers — with step caps so it can't run forever.
code/cl4-mcp/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.1 · What MCP is essential
Picture the drawer of chargers you've accumulated over the years. Every phone, camera and laptop shipped its own plug, so you kept a tangle of incompatible cables just in case. Then USB-C arrived and one connector fit almost everything. AI tooling had exactly that drawer-of-chargers problem: every app that wanted Claude to search a database, read a file, or file a ticket had to write its own bespoke integration, and none of that work carried over to the next app. MCP is the USB-C moment — one connector, written once, that any tool-using client can plug into.
Here's the idea in one sentence, and it's worth reading slowly: the Model Context Protocol is an open standard for connecting an LLM to outside tools and data. "Open standard" is the load-bearing phrase — MCP isn't an Anthropic-only product feature, it's a published specification anyone can implement, which is precisely why the same server you write today works with Claude, with your own code, and with other MCP-aware clients tomorrow. You write a capability once as an MCP server, and any MCP-aware client (Claude Code, the desktop app, your own application) can use it. Instead of N custom integrations — one per app — you build one server that speaks the protocol, and the N clients come to it.
The analogy that unlocks the whole chapter: think of it as a client/server split, the same shape as a web browser talking to a web server. The client lives where Claude runs and knows how to ask; the server is the program you write and knows how to do. They're two separate processes that talk over the standard protocol, and neither needs to know the other's internals — only the shared contract between them.
The common mistake is hearing "MCP" and picturing a new kind of AI model, or some magic that makes Claude smarter. It's neither. MCP adds no intelligence; it's plumbing — a well-specified way to move a request from the model to your code and a result back. The value isn't cleverness, it's reuse and interoperability: build the integration to your ticket system once and every agent, teammate, and app inherits it for the cost of a config line. Keep that framing as you read — everything below is just detail on how the client, the protocol, and the server play their three roles.
This picture is the whole idea of MCP (the Model Context Protocol) in three boxes. It shows how Claude reaches an outside capability — like a search tool or a database — through a shared standard instead of a custom, one-off connection.
- The left box (MCP client) is the thing Claude runs inside — Claude Code or the desktop app. When Claude wants to do something beyond chatting (look up a doc, create a ticket), the client is what actually reaches out.
- The middle box (MCP protocol) is the standard language both sides agree to speak. This is the point of MCP: because the format is standard, any client can talk to any server without custom glue code.
- The right box (your MCP server) is the program you write. It holds the actual
tools + data— the real work (searching, reading files, calling an API). - Read the arrows left to right: a request travels client → protocol → server, and the answer comes back the same way. The client and server are usually two separate programs talking over that protocol.
In short: "USB-C for AI": write the capability once as a server, and every MCP-aware client can plug into it. You build one server instead of one integration per app.
2 · The three primitives essential
Once you've decided to speak a standard, the next question is: standard for what? A protocol is only as useful as the vocabulary it agrees on. MCP's answer is refreshingly small — a server can offer just three kinds of thing, and every capability you'll ever build is one of them. Learning these three words is most of learning MCP; the rest is syntax.
Think of the three primitives as answering three different questions Claude might have. Can I do something? — that's a Tool, an action the model can call, like search_docs or create_ticket. Can I read something? — that's a Resource, a piece of data the model can pull in, like a file, a database row, or an API result. Is there a ready-made way to ask? — that's a Prompt, a reusable prompt template the server offers, like "summarize this incident." Verbs, nouns, and phrases: actions, data, and templates.
The distinction that trips people up is Tool versus Resource, and it's worth getting straight now. A Tool does — it runs code, and calling it can change the world (send an email, delete a record). A Resource is — it's content the model reads, ideally with no side effects, like opening a file. The rough rule of thumb: if invoking it could change something, it's a Tool and deserves the caution Tools get; if it only surfaces information, it's a Resource. Muddling the two — say, hiding a destructive action inside something you called a "resource" — is how you end up with a server that behaves surprisingly and dangerously.
Why does this matter beyond tidiness? Because the client treats the three differently: Tools are the ones Claude actively chooses to call mid-task, and they're the ones that get approval prompts and least-privilege scrutiny (section 6). Most of this chapter is about Tools for exactly that reason — they're where the action, and the risk, lives. The table below is your reference card; keep it in view as the labs build a Tool from scratch.
Read the three panels left to right — each one answers a different question Claude might have, and together they are everything an MCP server can offer.
- The left green panel (Tool) answers "can I do something?" — it's an action Claude actively calls, like
search_docsorcreate_ticket. The amber note flags that it may change the world, which is why Tools get approval prompts. - The middle blue panel (Resource) answers "can I read something?" — content the model pulls in (a file, a DB row) with, ideally, no side effects (green note).
- The right panel (Prompt) answers "is there a ready-made way to ask?" — a reusable template the server offers, like "summarize this incident."
- The rough rule underneath: if invoking it could change something it's a Tool; if it only surfaces information it's a Resource.
In short: Tool = verb (action, can change things), Resource = noun (data, read-only), Prompt = phrase (template). Don't hide a destructive action inside something you called a "resource."
| Primitive | Is | Example |
|---|---|---|
| Tool | an action the model can call | search_docs, create_ticket |
| Resource | data the model can read | a file, a DB row, an API result |
| Prompt | a reusable prompt template | "summarize this incident" |
3 · Connect Claude to a server essential
A server nobody knows about is just a script sitting on disk. You've understood what MCP is and the three things a server can offer — but Claude can't use a single one of them until the client is told the server exists. This section is the introduction step: the moment you hand the client an address and say "there's a capability over here, go talk to it."
The helpful surprise is how little you do. You don't wire up each tool by hand, register function names, or describe the protocol — you just tell the client how to start the server, and MCP handles the rest. Think of it like adding a contact to your phone: you save the name and number once, and from then on you just say who you want to reach. Here that "contact card" is a small block in the client's config file naming the server and the command that launches it.
The mechanism underneath is discovery, and it's the part beginners don't expect. When the client starts your server, the two shake hands over the protocol and the client asks the server what it can do; the server answers with its list of tools, their descriptions, and their input shapes. That's why you never enumerate tools in the config — add a new @mcp.tool() function to the server and the client picks it up automatically the next time it connects. The config points at the server; the server advertises its own tools.
The config below launches a stdio server — the simplest local case, where client and server are two processes on the same machine talking over standard-in/standard-out pipes (the same channels a normal program uses for input and output). The common confusion to head off: this file is not run like a program and prints nothing. It's a registration record. Its only job is to make Claude aware of the docs server so that, in a session, Claude can reach for its tools when a request calls for them. Read the walkthrough after it line by line.
@mcp.tool() and it appears automatically.
Follow the numbered steps: this is what happens between saving the config and Claude being able to call a tool.
- Step 1 — the
mcp.jsonbox holds just acommand + args: how to launch the server. That's all you write by hand. - Step 2 — the client (Claude Code) reads that and starts your server as a separate process.
- Step 3 — the dashed arrow back is the client asking the running server "what can you do?" — this is discovery.
- The amber box (step 4) is the server's answer: its list of tools with names, descriptions, and input shapes. You never enumerate tools in the config.
In short: The config points at the server; the server advertises its own tools. Add a decorated function and the client picks it up next connect — no config change.
mcp.json// .claude/mcp.json (or via `claude mcp add`)
{
"mcpServers": {
"docs": {
"command": "python",
"args": ["mcp/server.py"] // a stdio server (your process)
}
}
}
// Claude now sees the server's tools and can call them in a session.
Before Claude can use a tool, you have to tell the client where the server is. This little config file is that introduction — it registers one MCP server named docs so Claude Code knows how to start it and talk to it.
mcpServersis a list of servers you're registering. Each key ("docs"here) is a name you choose for one server.commandandargstogether tell the client how to launch the server: runpythonwith the argumentmcp/server.py. So the client literally starts your script as a separate program.- The comment calls it a stdio server — it means the client and server talk by writing back and forth over the program's normal input/output pipes (standard in / standard out), the simplest way to connect a local server.
- Once this is saved, Claude discovers the tools that server exposes and can call them during a chat — you don't wire up each tool by hand.
What the output means: Nothing prints — this is configuration, not a program. Its effect is that Claude Code now sees the docs server and its tools.
Try this: You can do the same thing without editing the file by running claude mcp add (shown in the comment). Try changing "docs" to another name and note that the name is just a label you pick.
4 · Intermediate — build a minimal server intermediate
Here's the moment MCP stops being a diagram and becomes something you own. Up to now the server has been "the box on the right" — the thing that does the work. Now you write it, and the reveal is that it's astonishingly small: a handful of ordinary Python functions with one line of decoration each. If you were bracing for protocol boilerplate — message framing, request IDs, capability negotiation — relax. You write none of it.
The reason is a library called FastMCP, and the way to think about it is a waiter in a restaurant. You (the kitchen) just cook — you write plain functions that do a job. FastMCP is the waiter who takes the order from the client, carries it to your function, plates the result, and delivers it back, speaking the protocol so you never have to. Your entire responsibility is the food: correct functions with clear descriptions. Everything about how the request and response travel is the waiter's problem, not yours.
The one piece of new syntax is the decorator — the @mcp.tool() line sitting on top of a function. A decorator is Python's way of tagging a function with extra behaviour without changing its body; here the tag means "expose this one to Claude as a callable tool." That word "expose" is the whole point: a function without the decorator is invisible to Claude, and a function with it becomes a capability the client can discover and call. Adding a tool is literally adding a decorated function — nothing more.
The mistake that costs people an afternoon is treating the function's docstring as a throwaway comment. It is not. MCP ships that docstring to Claude as the tool's official description — it's the entire basis on which Claude decides whether to call the tool. A blank or vague docstring produces a tool Claude technically has but never reaches for, which feels exactly like a bug and isn't one. We give the tool contract its own section (5) precisely because this is where beginners lose the most time. For now, read the server below and notice how the docstring names when to use the tool, not just what it does.
@mcp.tool() and FastMCP — the waiter that speaks the protocol — turns it into a capability the client can discover and call. Adding a tool is adding a decorated function.
Scan left to right: an ordinary function becomes a Claude-callable tool by passing through one line of decoration.
- The left box is your plain Python function (
search_docs) — on its own it is invisible to Claude (grey note). Its docstring is what names when to use it. - The middle box (FastMCP) is the "waiter": triggered by the
@mcp.tool()decorator, it takes the order, carries it to your function, and speaks the MCP protocol so you write none of it. - The right green box is the result: a callable tool the client can discover — now visible + callable.
- The two arrows show the one-way transformation: function → decorated by FastMCP → exposed tool.
In short: Without the decorator, just Python. With it, a capability Claude can call. FastMCP handles the plumbing; you only write the function and its docstring.
server.pyfrom mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs")
@mcp.tool()
def search_docs(query: str) -> list[str]:
"""Search the internal knowledge base. Use for policy/product questions."""
corpus = {"refund": "Refunds within 30 days.", "hours": "Support 9-5 ET."}
return [v for k, v in corpus.items() if query.lower() in k]
if __name__ == "__main__":
mcp.run(transport="stdio")
This is a complete — if tiny — MCP server: the program on the other end that actually does the work. The surprise is how little code it takes. FastMCP handles all the protocol plumbing; you just write ordinary Python functions and mark which ones are tools.
mcp = FastMCP("docs")creates the server object and names itdocs. This object knows how to speak the MCP protocol so you don't have to.- The
@mcp.tool()line above the function is a decorator — it tells the server "expose this function to Claude as a callable tool." Without it, the function would just be normal Python that Claude can't see. - The triple-quoted line inside the function is the docstring, and it is not just a comment: MCP sends it to Claude as the tool's description. Notice it says when to use the tool ("policy/product questions") — that's how Claude decides to call it.
- The function body is plain Python: it searches a small dictionary and returns the matching values. The
-> list[str]type hint tells MCP the tool gives back a list of strings. mcp.run(transport="stdio")at the bottom starts the server and makes it listen over stdio — the same channel the config file above expected.
What the output means: Run directly, it prints nothing and waits: a server sits idle until a client connects and asks it to run search_docs. Calling search_docs("refund") would return ["Refunds within 30 days."].
Try this: Add a second entry to the corpus dictionary and a matching query word. Every function you decorate with @mcp.tool() becomes a new capability Claude can call.
5 · The tool contract — a description that gets called intermediate
You can write a flawless tool and watch Claude ignore it completely. The code runs, the server connects, everything looks right — and yet when you ask the question the tool was built for, Claude answers from memory or says it can't help. Nine times out of ten the tool isn't broken; its description is. This is the single most common frustration people hit with MCP, and it deserves its own section because the fix is a mindset, not a line of code.
Here's the reframe: Claude never sees your function's body. At decision time it sees a menu — a list of tool names and their descriptions — and it picks the one whose description best matches what the user just asked. So the description isn't documentation about the tool; it is the tool, as far as Claude's choice is concerned. Think of it like a job posting. "Search" is a title with no job description — nobody knows when to apply. "Search the internal knowledge base; use for policy, product, and refund questions" tells Claude exactly the situations to reach for it. Same tool, wildly different hit rate.
The instinct to fight is describing what the tool does instead of when to use it. "Searches documents" is what; "use for policy and product questions" is when. Claude already has to match a user's request to a tool, so it needs the trigger conditions — the situations, the keywords, the kinds of question — far more than a mechanical summary. A good description reads like advice to a new teammate: reach for this one whenever someone asks about X or Y. This is the same principle behind a Skill's description (K4) or a tool schema (Ch 4) — the model routes on the words you give it.
The lab below turns that judgment into something you can test. It's a tiny linter that scores a description the way a reviewer would: does it name a when-to-use, and is it long enough to match reliably? Treating your descriptions as testable artifacts — lint them, iterate until they pass — is what separates tools that fire at the right moment from tools that quietly gather dust.
Follow the top row first, then compare the two description boxes below it.
- The top-left box is a user request ("refund policy?"). The arrow leads to Claude, which reads only the menu of tool descriptions — the grey note reminds you it never sees the function body.
- The red box is a weak description —
"searches". It says what the tool does but never when to reach for it, so Claude skips it (dashed arrow, no match). - The green box is a strong description that names the trigger conditions ("Use for policy/refund questions"). It matches the request, so Claude calls it (solid arrow).
- Same underlying tool, opposite outcomes — the only thing that changed is the words in the description.
In short: Describe when to use a tool, not just what it does. To Claude's routing decision, the description isn't documentation about the tool — it is the tool.
tool_desc.pydef score_tool_desc(desc):
d = desc.lower(); notes = []
if not any(w in d for w in ["use ", "when", "for"]): notes.append("say WHEN to use it")
if len(desc) < 25: notes.append("too terse to trigger reliably")
return (not notes), notes
print(score_tool_desc("searches"))
print(score_tool_desc("Search the internal KB. Use for policy/product/procedure questions."))
(False, ['say WHEN to use it', 'too terse to trigger reliably'])
(True, [])
Claude chooses which tool to call almost entirely from the tool's description. A vague description means the tool never gets used at the right moment. This little checker scores a description the way a reviewer would, so you catch weak ones before shipping.
d = desc.lower()makes a lowercase copy so the checks aren't fooled by capitalization.notes = []starts an empty list to collect problems.- The first
iflooks for trigger words like"when"or"for". If none appear, the description never says when to use the tool, so it adds a note. Saying when-to-use is the single most important thing a description does. - The second
ifflags descriptions shorter than 25 characters as too terse — too little text for Claude to reliably match to a user's request. return (not notes), noteshands back a pair:Trueif the list of problems is empty (a good description), plus the list itself so you can read what to fix.- The two
printcalls test it: a bad description ("searches") and a good one that names when to use it.
What the output means: First line is (False, ['say WHEN to use it', 'too terse to trigger reliably']) — two problems found. Second is (True, []) — the good description passes with no notes.
Try this: Feed it your own server's docstring from server.py. If it fails, rewrite the docstring to name a concrete when — e.g. "Use for refund and support-hours questions."
6 · Advanced — MCP is a trust boundary advanced
The friendliness of the config file hides a sharp edge. Adding an MCP server is two lines of JSON, which makes it feel as harmless as adding a bookmark. It is not. When you connect a server, you're granting a program the right to run on your machine, with your permissions, and to act on Claude's behalf. That's closer to installing an app than saving a link — and the casual ergonomics are exactly what make the risk easy to miss.
The mental model to hold is a trust boundary: a line where things you control meet things you don't, and where you should slow down and check credentials. A convenient way to picture it is handing someone the keys to your house. A trusted friend who's just fetching the mail is one thing; a stranger, holding a key that also opens the safe, whom nobody has vetted, is another entirely. The server is the person; the tools it exposes are which rooms the key opens. You want to know who wrote it, what it can reach, and whether anyone's watching when it does something irreversible.
Three properties turn a server from "probably fine" into "genuinely dangerous," and they stack. First, an untrusted source — you don't know who wrote the code you're about to run. Second, network access without an audit — code that can reach the internet and hasn't been reviewed can quietly exfiltrate whatever it touches. Third, a destructive tool with no human gate — an action that deletes or overwrites, running with no approval prompt in the loop. Any one warrants caution; together they're a server you simply don't connect.
The professional habits that defuse all of this are least privilege (give a tool only the access it truly needs, never blanket permissions) and keeping the client's approval prompts on for anything state-changing, so a human sees and okays irreversible actions. The common mistake is treating vetting as a one-time gut feeling; the fix, shown in the lab, is to encode the judgment as a repeatable checklist you run before enabling any server. Model the risk first, connect second.
The dashed vertical line is the trust boundary: everything left of it you control, everything right of it you don't. Read the right column top to bottom.
- The three red boxes on the right are the risk properties that stack: (1) an untrusted source — you don't know who wrote it; (2) network access that hasn't been audited — it can reach the internet unreviewed; (3) a destructive tool with no human gate — it can delete with no approval step.
- The green box on the left is your side of the line: least privilege plus keeping approval prompts on is what defuses the danger.
- The red line at the bottom is the rule: any single property warrants caution; all three together is a server you simply don't connect.
In short: Connecting a server ≈ installing an app with your credentials. Vet the source, review the tools, prefer least privilege, and gate anything irreversible.
vet_server.pydef vet_server(server):
risks = []
if not server.get("trusted_source"): risks.append("untrusted source")
if server.get("network_access") and not server.get("audited"): risks.append("network access, unaudited")
if "delete" in server.get("tools", []) and not server.get("human_gate"): risks.append("destructive tool, no gate")
return (not risks), risks
print(vet_server({"trusted_source": True, "tools": ["search"], "audited": True}))
print(vet_server({"trusted_source": False, "network_access": True, "tools": ["delete"]}))
(True, [])
(False, ['untrusted source', 'network access, unaudited', 'destructive tool, no gate'])
An MCP server is real software that runs on your machine with your permissions — it can touch files, networks and systems. So connecting one is a trust decision, not a casual install. This function turns that judgment into a repeatable checklist you run before enabling a server.
risks = []starts an empty list; each check that fails will append a reason, and an empty list at the end means the server looks safe.- The first check flags a server whose
trusted_sourceis missing or false — you don't know who wrote it. - The second flags a server that has
network_accessbut has not beenaudited— code that can reach the internet and hasn't been reviewed is a classic danger. - The third looks for a
deletetool with nohuman_gate— a destructive action with no human approval step. That combination is exactly what you don't want running unattended. return (not risks), risksgives backTrueonly when zero risks were found, plus the list of any concerns.
What the output means: First line (True, []) — the trusted, audited, search-only server passes. Second (False, ['untrusted source', 'network access, unaudited', 'destructive tool, no gate']) — three red flags on the sketchy server.
Try this: Add a rule of your own — e.g. flag any server exposing a "write" tool without a gate. Treat a third-party MCP server like installing an app: vet the source, review its tools, prefer least privilege.
7 · Professional — where MCP fits your stack professional
The trap of learning a shiny new tool is wanting to use it everywhere. You've now built a server, connected it, and vetted it — and the natural next thought is "great, I'll make everything an MCP server." Resist it. The professional skill isn't building servers; it's knowing when a server earns its keep and when a plain function is the better answer. Reaching for the protocol on a one-off tool is over-engineering with extra steps.
The deciding question is how many clients will use this capability. MCP's entire payoff is reuse across a boundary: write the ticket-system server once, and Claude Code, your own app, and every subagent (K5) plug into the same thing with a config line. That's the "USB-C" leverage — one server instead of one integration per app. But leverage only pays off when there's something to reuse. If exactly one program will ever call the capability, the protocol, the separate process, and the config all buy you nothing.
An analogy: you don't stand up a shared microservice to add two numbers for a single script — you write a function. You do stand one up when three teams need the same billing logic and you're tired of it drifting out of sync in three codebases. Same calculus here. A helper used by one client, once, is a function; a capability that multiple clients or agents will lean on is a server. The rough rule of thumb: two or more consumers → server; a single caller → function.
Start at the top box — the one question that decides it — then follow the branch that matches your situation.
- The top box asks the deciding question: how many callers will use this capability?
- The left branch (one caller) lands on a plain function — no separate process, no protocol, no config. The green note: keep it simple.
- The right branch (two or more) lands on an MCP server — the "build once, every client plugs in" leverage, where the overhead is a bargain.
- The split shows the tradeoff visually: a server's cost is fixed, so it only pays off when there's reuse to spread it across.
In short: Two or more consumers → server; a single caller → function. Don't cargo-cult a server where a function does the job.
The mistake to name plainly is cargo-culting — copying the server pattern because it's the "proper" way, not because your situation needs it. A server adds a process to launch, a protocol to speak, and config to maintain; that overhead is a bargain when the capability is shared and pure waste when it isn't. Match the tool to the reuse, and you'll build servers exactly where they multiply your effort and functions everywhere else.
8 · Tech-lead — MCP servers as shared infrastructure tech-lead
The moment a server is shared, it stops being your script and becomes everyone's dependency. A tool three teams rely on can't be quietly edited on a Friday afternoon, can't silently change what it returns, and can't disappear because you refactored. That shift in stakes is what separates a personal server from infrastructure — and stewarding that shift is the tech-lead's job in this chapter.
The most useful reframe is to stop thinking of an internal MCP server as "an AI thing" and start treating it exactly like a shared internal API — because operationally that's what it is. Nobody would ship a company-wide API with no version numbers, no docs, no access control, and no monitoring; the same discipline applies the instant a server has more than one consumer. The four load-bearing properties are the same ones you'd demand of any service others depend on: versioned (semver, so clients can pin and you can evolve without breaking them), documented (every tool's description names when to use it — the section-5 lesson, now a contract others read), access-controlled (least privilege, with auth on state-changing tools), and monitored (log tool calls, alert on failures and abuse).
Notice this is the section-6 trust-boundary lesson turned inward. When you connect a third-party server, you vet it; when you ship one to your org, you're the party being trusted, so the non-negotiables become your responsibility: least privilege by default and approval prompts left on for anything destructive. A shared server that skips these doesn't just risk your machine — it hands every teammate who connects the same sharp edge, multiplied.
The payoff makes the discipline worth it. One well-built, trusted server becomes the tool layer the whole organization builds on — every agent and engineer gains that capability for the cost of a config line, the same leverage as a shared client library that everyone imports instead of everyone re-implementing. That is the endgame of MCP: not a scattering of one-off scripts, but a small, governed set of servers that turn "connect to our systems" from a project into a one-liner. (The deep version — registries, remote transports, org rollout — is K6.)
Read left to right: many consumers converge on one server, and that shared server must carry API-grade properties (the right-hand stack).
- The left column — team A, team B, agents — are the multiple consumers. The arrows show them all depending on the same thing.
- The middle green box is the one shared server: the org's tool layer that everyone plugs into.
- The right stack is the discipline it now owes them: versioned so clients can pin, documented (each tool's description names when to use it), access-controlled (least privilege), monitored (logs and alerts), and the red one — human-gated for destructive tools (the non-negotiable from §6, turned inward).
- The bottom line is the payoff: every consumer gains the capability for a single config line.
In short: Once a server has more than one consumer it's an internal API — version it, document it, control access, monitor it, and keep approval prompts on for anything irreversible.
Exercise C4.1 — Build & connect a server
Context: The whole chapter comes together when you build a server, write a description Claude will actually trigger on, connect it, and vet it as if it were third-party — the full lifecycle in one pass.
Your task: Build a minimal MCP server with one tool (a search or lookup), write a triggering description (score it with tool_desc.py), connect it to Claude Code via config, confirm Claude calls it, then run vet_server as if reviewing a third-party server before connecting.
Requirements:
- Write a server with one
@mcp.tool()function - Give it a description that says when to use it, and score it until it passes
- Register it in
.claude/mcp.jsonpointing at the server script - Launch a session, ask something matching the when-to-use, and confirm Claude calls it
- Run the vetting checks (trusted source, audited network access, gated destructive tools) as a dry run
💡 Hint: If Claude won't call your tool, the description is almost always the culprit — score it first, then debug the wiring.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: MCP is the "USB-C of AI tooling": write a capability once as a server and any MCP-aware client can use it. It starts with registering a server in the client config.
Your task: Following section 3, write a .claude/mcp.json that registers one stdio MCP server named docs, launched by running python mcp/server.py, so Claude Code discovers its tools.
Requirements:
- An
mcpServersmap whose keydocsis the name you choose - A
commandandargsthat tell the client how to launch the server - A stdio server — client and server talk over standard in/out pipes
- Once saved, Claude discovers the server's tools automatically
- This is configuration, not executed code
💡 Hint: The config just tells the client how to start the server; tool discovery is automatic once the process is launched over stdio.
Show solution
// .claude/mcp.json (or via `claude mcp add`)
{
"mcpServers": {
"docs": {
"command": "python",
"args": ["mcp/server.py"] // a stdio server (your process)
}
}
}
// Claude now sees the server's tools and can call them in a session.
mcpServers lists the servers you register; the key "docs" is a name you choose. command plus args tell the client how to launch the server — it literally starts python mcp/server.py as a separate program. It is a stdio server, meaning client and server talk over standard in/out pipes, the simplest local transport. Once saved, Claude discovers the server's tools automatically; you don't wire up each tool by hand. You can do the same without editing the file via claude mcp add. This is configuration, not executed code.
Context: Building a server is a few lines with FastMCP, which handles the protocol plumbing. The one thing you must get right is the tool's description — it's how Claude decides when to call it.
Your task: Reproduce section 4: build a minimal MCP server named docs that exposes one search_docs tool over stdio using FastMCP.
Requirements:
- Create the server with
FastMCP("docs") - Expose the function with the
@mcp.tool()decorator - Write a docstring that says when to use the tool — MCP sends it to Claude as the description
- Use type hints so MCP knows the return type
- Start it with
mcp.run(transport="stdio"); needs an API key to drive from a client
💡 Hint: The docstring is not a comment here — it's the tool description Claude reads to decide whether this is the right tool for the request.
Show solution
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs")
@mcp.tool()
def search_docs(query: str) -> list[str]:
"""Search the internal knowledge base. Use for policy/product questions."""
corpus = {"refund": "Refunds within 30 days.", "hours": "Support 9-5 ET."}
return [v for k, v in corpus.items() if query.lower() in k]
if __name__ == "__main__":
mcp.run(transport="stdio")
FastMCP("docs") creates the server object and handles all the MCP protocol plumbing. The @mcp.tool() decorator exposes an ordinary Python function to Claude as a callable tool — without it the function stays invisible. The docstring is not just a comment: MCP sends it to Claude as the tool's description, and it names when to use the tool. mcp.run(transport="stdio") starts the server listening over the same stdio channel the config expects. Runs with pip install mcp; a server sits idle until a client connects.
Context: Claude chooses which tool to call almost entirely from its description, so a vague or terse one silently never gets called. A linter for descriptions catches that before it ships.
Your task: Reproduce section 5's tool_desc.py: write score_tool_desc(desc) that flags a description if it never says when to use the tool or is too terse, and test it on a bad and a good description.
Requirements:
- Flag the description if it lacks trigger words like "use", "when", or "for"
- Flag it if it's below a minimum length (too terse to match reliably)
- Return a pass with no problems, or a fail with the list of problems
- Show a bad description (e.g. "searches") failing with both reasons
- Show a good when-to-use description passing; pure Python, no server or API key
💡 Hint: A good description names the situations the tool is for — "searches" says what it does but never when Claude should reach for it.
Show solution
def score_tool_desc(desc):
d = desc.lower(); notes = []
if not any(w in d for w in ["use ", "when", "for"]): notes.append("say WHEN to use it")
if len(desc) < 25: notes.append("too terse to trigger reliably")
return (not notes), notes
print(score_tool_desc("searches"))
print(score_tool_desc("Search the internal KB. Use for policy/product/procedure questions."))
(False, ['say WHEN to use it', 'too terse to trigger reliably'])
(True, [])
Claude chooses which tool to call almost entirely from the tool's description, so a vague one never gets used at the right moment. The first check looks for trigger words like "when" or "for" — saying when-to-use is the single most important thing a description does. The second flags descriptions under 25 characters as too terse for Claude to match reliably. It returns True only when the notes list is empty, plus the notes to fix. This is runnable pure Python — no server or API key needed.
Context: An MCP server runs code on your machine with your permissions, so connecting one is a trust decision, not a casual install. A vetting checklist encodes the questions to ask first.
Your task: Reproduce section 6's vet_server.py: write a checklist that flags an untrusted source, network access without an audit, and a destructive delete tool with no human gate.
Requirements:
- Flag a server whose source isn't trusted
- Flag network access that hasn't been audited
- Flag a destructive tool (e.g.
delete) that has no human gate - Pass only when zero risks are found; otherwise return the list of concerns
- Show a clean search-only server passing and a sketchy one failing on all three; pure Python, no server needed
💡 Hint: Treat a third-party server like installing an app with your credentials — vet the source, review the tools, and prefer least privilege before you connect.
Show solution
def vet_server(server):
risks = []
if not server.get("trusted_source"): risks.append("untrusted source")
if server.get("network_access") and not server.get("audited"): risks.append("network access, unaudited")
if "delete" in server.get("tools", []) and not server.get("human_gate"): risks.append("destructive tool, no gate")
return (not risks), risks
print(vet_server({"trusted_source": True, "tools": ["search"], "audited": True}))
print(vet_server({"trusted_source": False, "network_access": True, "tools": ["delete"]}))
(True, [])
(False, ['untrusted source', 'network access, unaudited', 'destructive tool, no gate'])
An MCP server is real software that runs on your machine with your permissions — it can touch files, networks, and systems — so connecting one is a trust decision, not a casual install. This function turns that judgment into a repeatable checklist: unknown author, internet-reaching-but-unreviewed code, and a destructive action with no human approval step are all red flags. Treat a third-party server like installing an app: vet the source, review its tools, prefer least privilege, and keep the client's approval prompts on for anything state-changing. Runnable pure Python — no server needed.
Context: Not everything should be a server. The professional call is knowing when a plain function is simpler and when a server genuinely earns its keep — when a capability is shared across clients.
Your task: Per section 7, a colleague wants to wrap a one-off, single-client helper as an MCP server. Write a short decision rule (and a code sketch) for when a plain function is the right call and when a server earns its keep.
Requirements:
- Rule: reach for a server only when the capability is used by multiple clients or agents
- For a single client / one-off, a plain function is simpler — don't cargo-cult a server
- Sketch the condition (e.g. shared across ≥2 clients → server)
- Note the payoff: build once, every client plugs in
- Runnable illustrative Python; the substance is the design decision
💡 Hint: The server's value is reuse across clients — if there's exactly one caller, the protocol overhead buys you nothing.
Show solution
# Rule: reach for an MCP server only when a capability is used by
# MULTIPLE clients/agents. For one client, one-off, a plain
# function is simpler — don't cargo-cult a server.
def decide(capability):
if capability["num_clients"] >= 2 or capability["shared_across_agents"]:
return "MCP server: build once, every client plugs in"
return "plain function: one client, one-off — keep it simple"
print(decide({"num_clients": 1, "shared_across_agents": False}))
print(decide({"num_clients": 3, "shared_across_agents": True}))
plain function: one client, one-off — keep it simple
MCP server: build once, every client plugs in
MCP shines when a capability is used by multiple clients or agents: build the ticket-system server once and Claude Code, your app, and every subagent can use it — that is the "USB-C of AI tooling" payoff of one server instead of N integrations. But for a single-client, one-off tool a plain function is simpler; don't cargo-cult a server where a function does. This is runnable illustrative Python; the real leverage is the design decision.
Context: A lead ships an internal MCP server like shared API infrastructure — versioned, documented, access-controlled, monitored, human-gated — so it becomes reusable org-wide leverage.
Your task: Per section 8, describe how a lead should ship an internal MCP server so it becomes reusable org infrastructure, and capture the standards as a checklist a team can enforce.
Requirements:
- Versioned (semver; clients pin a version)
- Documented (every tool's docstring says when to use it)
- Access-controlled (least privilege; auth on state-changing tools)
- Monitored (log tool calls; alert on failures/abuse)
- Human-gated (approval prompts stay ON for destructive tools)
- Runnable illustrative Python; the substance is the standards checklist
💡 Hint: Least privilege plus approval prompts on state-changing tools are the non-negotiables — the MCP-as-trust-boundary lesson applied to infrastructure you own.
Show solution
# A lead treats an internal MCP server like a shared API.
SHARED_SERVER_STANDARDS = {
"versioned": "semver the server; clients pin a version",
"documented": "each tool's docstring says WHEN to use it",
"access_controlled": "least-privilege; auth on state-changing tools",
"monitored": "log tool calls; alert on failures/abuse",
"human_gated": "approval prompts stay ON for destructive tools",
}
def ready_to_share(server):
missing = [k for k in SHARED_SERVER_STANDARDS if not server.get(k)]
return (not missing), missing
print(ready_to_share({k: True for k in SHARED_SERVER_STANDARDS}))
print(ready_to_share({"versioned": True, "documented": True}))
(True, [])
(False, ['access_controlled', 'monitored', 'human_gated'])
A lead treats internal MCP servers like shared APIs: versioned, documented, access-controlled, and monitored. One well-built, trusted server becomes the tool layer the whole org builds on — every agent and engineer gains that capability with a single config line, the same leverage as a shared client library. This ties back to MCP as a trust boundary: least privilege and keeping approval prompts on for anything state-changing are non-negotiable. Runnable illustrative Python; the substance is the standards checklist.
✓ Checkpoint — you can move on when you can…
- Explain MCP and its three primitives.
- Connect Claude to a server and build a minimal one.
- Write a triggering tool description.
- Vet a server's security; standardize servers for a team.
Knowledge check check yourself
MCP is described as the "USB-C of AI tooling" — what problem does that standardization solve?
Show answer
What are MCP's three primitives, and what is each one?