Model Context Protocol (MCP) · Intermediate
Build an MCP server and connect it to Claude Code
Why it matters
The Model Context Protocol is an open standard for connecting AI applications to tools and data. Write a server once and any MCP client can use it: Claude Code, Claude Desktop, IDEs and your own agents. MCP appeared in about a quarter of the Forward Deployed Engineer job descriptions we read and is a domain of Anthropic's CCAR-F certification.
The concept
An MCP server offers capabilities to a client (the AI application):
Tools: functions the model can call, with a name, description and input schema (like the tools on the AI agents shelf).
Resources: read-only data identified by a URI that the client can load into context.
Prompts: reusable prompt templates the user can pick.
Transports:
stdio: the client starts the server as a local process and talks over stdin and stdout. Simple and private.
Streamable HTTP: the server runs as a web service for remote clients (next lesson).
The official Python SDK builds the schema from type hints and docstrings. Version 2 of the SDK calls the high-level server class MCPServer (it was FastMCP in version 1).
Worked example: a docs search server
Reuse search.py, chunks.json and vectors.npy from A RAG system from scratch, with citations, and pip install "mcp[cli]".
"""An MCP server that lets any MCP client (Claude Code, Claude Desktop, IDEs) search the Kubernetes docs index."""
from mcp.server.mcpserver import MCPServer
from search import dense, records
mcp = MCPServer("k8s-docs")
@mcp.tool()
def search_docs(question: str, k: int = 4) -> list[dict]:
"""Search the indexed Kubernetes concept pages and return the most relevant excerpts with their sources.
Args:
question: A natural-language question about Kubernetes.
k: How many excerpts to return (1 to 8).
"""
k = max(1, min(k, 8))
return [{"id": int(i), "doc": records[i]["doc"], "section": records[i]["section"], "text": records[i]["text"]}
for i in dense(question, k)]
@mcp.resource("docs://index")
def index_summary() -> str:
"""Which documents are indexed, and how many excerpts each has."""
counts: dict[str, int] = {}
for record in records:
counts[record["doc"]] = counts.get(record["doc"], 0) + 1
return "\n".join(f"{doc}: {n} excerpts" for doc, n in sorted(counts.items()))
if __name__ == "__main__":
mcp.run() # stdio transport: the client starts this process and talks over stdin and stdoutTest it with the SDK's client, exactly as an AI application would connect:
"""Connect to the server like an MCP client would, list its tools and call one."""
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server = StdioServerParameters(command="python3", args=["docs_server.py"])
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("tools:", [t.name for t in tools.tools])
result = await session.call_tool("search_docs", {"question": "How do I roll back a deployment?", "k": 2})
for block in result.content:
print(block.text[:160])
index = await session.read_resource("docs://index")
print(index.contents[0].text.splitlines()[0])
asyncio.run(main())It lists ['search_docs'], returns two excerpts from the deployment page including "Rolling Back to a Previous Revision", and reads the first line of the index resource (configuration_configmap: 12 excerpts). The tool's input schema, generated from the type hints, has question and k.
Connect it to Claude Code:
claude mcp add k8s-docs -- python3 /absolute/path/to/docs_server.py
claude mcp listThen ask Claude Code a Kubernetes question; it can call search_docs and answer from the documentation. For Claude Desktop, add the same command under mcpServers in its configuration file. The MCP Inspector (npx @modelcontextprotocol/inspector python3 docs_server.py) lets you call tools by hand while developing.
Common mistakes
Printing to stdout in a stdio server. Stdout carries the protocol; log to stderr.
Tools that do too much. One focused tool with a clear description is used more reliably than a do-everything tool.
No input limits. Clamp values like
k, validate ids and paths; the client is ultimately driven by model output.Returning huge payloads. Everything returned enters the model's context. Return what is needed, with sources.
Interview angle
"What is MCP and why would a company adopt it?" A standard protocol so one integration works with many AI clients; tools, resources and prompts; local and remote transports. Then show you know the risks: least-privilege tools, input validation, and treating tool output as untrusted.
Practice task
Add a
get_section(doc, section)tool that returns a full section by name.Add a prompt
explain_for_beginner(topic)and try it in a client that supports prompts.Connect the server to Claude Code and ask three questions; note when it calls the tool and when it does not.