AI EngineeringZero to ProductionHome·About·Contact
Claude & Anthropic · Chapter C4

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.

⏱️ ~2 hours🧪 4 labs🎯 Beginner→Tech-lead

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.
▶ Runnable companionEvery code block in this lesson is also a standalone file under 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

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, the desktop app, your own app) can use it. Instead of N custom integrations, you build one server that speaks a standard protocol.

MCP client (Claude) Claude Code/app MCP protocol standard Your MCP server tools + data
🗺️ How to read this diagram

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

PrimitiveIsExample
Toolan action the model can callsearch_docs, create_ticket
Resourcedata the model can reada file, a DB row, an API result
Prompta reusable prompt template"summarize this incident"

3 · Connect Claude to a server essential

You register a server in the client's config; the client discovers its tools and lets Claude call them. Here's a Claude Code MCP config.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
config · register an MCP server
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.
▶ How this works

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.

  1. mcpServers is a list of servers you're registering. Each key ("docs" here) is a name you choose for one server.
  2. command and args together tell the client how to launch the server: run python with the argument mcp/server.py. So the client literally starts your script as a separate program.
  3. 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.
  4. 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

A server is a few decorated functions. FastMCP handles the protocol; you write the tools. (Runs with pip install mcp.)

Python · a minimal MCP server
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")
▶ How this works

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.

  1. mcp = FastMCP("docs") creates the server object and names it docs. This object knows how to speak the MCP protocol so you don't have to.
  2. 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.
  3. 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.
  4. 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.
  5. 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

Like a Skill's description (K4) or a tool schema (Ch 4), the tool's docstring/description is how Claude decides when to call it. Write it around when to use it.

Python · score a tool description (runs)
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, [])
▶ How this works

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.

  1. d = desc.lower() makes a lowercase copy so the checks aren't fooled by capitalization. notes = [] starts an empty list to collect problems.
  2. The first if looks 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.
  3. The second if flags descriptions shorter than 25 characters as too terse — too little text for Claude to reliably match to a user's request.
  4. return (not notes), notes hands back a pair: True if the list of problems is empty (a good description), plus the list itself so you can read what to fix.
  5. The two print calls 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

An MCP server runs code and can touch data/systems. Only connect servers you trust, give tools least privilege, and keep the client's approval prompts on for anything state-changing. Model the risk check before enabling a server.

Python · vet an MCP server before connecting (runs)
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'])
▶ How this works

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.

  1. 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.
  2. The first check flags a server whose trusted_source is missing or false — you don't know who wrote it.
  3. The second flags a server that has network_access but has not been audited — code that can reach the internet and hasn't been reviewed is a classic danger.
  4. The third looks for a delete tool with no human_gate — a destructive action with no human approval step. That combination is exactly what you don't want running unattended.
  5. return (not risks), risks gives back True only 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.

Treat a third-party MCP server like installing softwareIt runs on your machine with your permissions. Vet the source, review what tools it exposes, and prefer least-privilege. This connects to LLM security — an untrusted server is an attack surface.

7 · Professional — where MCP fits your stack professional

MCP shines when a capability is used by multiple clients/agents: build the ticket-system server once, and Claude Code, your app, and every subagent (K5) can use it. For a one-client, one-off tool, a plain function is simpler — don't cargo-cult a server.

8 · Tech-lead — MCP servers as shared infrastructure tech-lead

A lead treats internal MCP servers like shared APIs: versioned, documented, access-controlled, monitored. One well-built server becomes reusable across every agent and teammate — the tool layer the org builds on. (The deep version is K6.)

Build once, everyone benefitsWhen a lead ships a trusted MCP server for a core system, every agent and engineer gains that capability with a config line — the same leverage as a shared client library.

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.json pointing 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.

Exercise 1 · Register an MCP server in the client configBeginner

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 mcpServers map whose key docs is the name you choose
  • A command and args that 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.

Exercise 2 · Build a minimal MCP server with FastMCPIntermediate

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.

Exercise 3 · Score a tool description so Claude will call itAdvanced

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.

Exercise 4 · Vet an MCP server before connecting (trust boundary)Expert

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.

Exercise 5 · Decide when MCP fits vs a plain functionProfessional

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.

Exercise 6 · Treat internal MCP servers as shared infrastructureIndustry scenario

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

✓ Knowledge check

MCP is described as the "USB-C of AI tooling" — what problem does that standardization solve?

Show answer
Instead of building N custom integrations (one per app), you write a capability once as an MCP server and any MCP-aware client — Claude Code, the desktop app, your own code — can use it.
✓ Knowledge check

What are MCP's three primitives, and what is each one?

Show answer
A Tool is an action the model can call (e.g. search_docs); a Resource is data the model can read (a file, DB row, API result); and a Prompt is a reusable prompt template.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in