Model Context Protocol (MCP) · Intermediate

Build an MCP server and connect it to Claude Code

4 min read

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]".

docs_server.py
"""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 stdout

Test it with the SDK's client, exactly as an AI application would connect:

try_server.py
"""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:

connect.sh
claude mcp add k8s-docs -- python3 /absolute/path/to/docs_server.py
claude mcp list

Then 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

  1. Add a get_section(doc, section) tool that returns a full section by name.

  2. Add a prompt explain_for_beginner(topic) and try it in a client that supports prompts.

  3. Connect the server to Claude Code and ask three questions; note when it calls the tool and when it does not.

Further reading