MCP and Agent Protocols: Building and Consuming Tool Servers
The Model Context Protocol is how agents connect to tools, data, and prompts, and every major provider supports it. Build an MCP server in Python, design tools a model will use correctly, connect it to a coding agent, and learn where A2A and the other agent protocols fit.
Before MCP, every agent framework and every provider had its own way of describing tools, and every integration was written once per framework. The Model Context Protocol (MCP) fixed that with a small open standard: a server exposes tools, resources, and prompts; a client (a chat app, an IDE, a coding agent, your own harness) discovers and calls them. Anthropic released it in late 2024; it has since moved to open governance and is supported by every major model provider and agent product. Writing an MCP server is now the default way to make a system available to agents, and "built an MCP server" is a line hiring managers look for.
This module builds one, teaches the part that actually determines whether the model uses your tools well (the tool descriptions), and places MCP among the other protocols you will hear about.
The Shape of the Protocol
MCP has three roles and three primitives.
Roles. A host is the application the user is in (an IDE, a chat client, your agent). The host runs one or more clients, each connected to one server. A server is a process that exposes capabilities. Servers are typically small and single-purpose: one for your ticketing system, one for the database, one for the file system.
Primitives.
- Tools are functions the model can call, with a JSON schema for arguments. This is the primitive you will use most.
- Resources are readable data identified by URI (
tickets://12345,file:///repo/README.md). The host decides when to load them into context; the model does not call them like functions. - Prompts are reusable templates the server offers ("summarize this ticket for a handoff"), surfaced to the user as slash commands or menu items.
Transport. Servers run over stdio (the host launches the server as a subprocess, ideal for local tools) or over HTTP (for remote or shared servers). The protocol is the same on both.
Building a Server
The official Python SDK makes a server a few decorators. Here is a server for a support-ticket system with two tools and a resource.
python# server.py from mcp.server.fastmcp import FastMCP from tickets import TicketClient # your existing client mcp = FastMCP("tickets") client = TicketClient.from_env() @mcp.tool() def search_tickets(query: str, status: str = "open", limit: int = 10) -> list[dict]: """Search support tickets by free text. Use this to find tickets before reading or updating them. Returns at most `limit` tickets, newest first, each with id, title, status, and customer. `status` is one of: open, pending, closed, all. """ if status not in {"open", "pending", "closed", "all"}: raise ValueError("status must be open, pending, closed, or all") return [t.summary() for t in client.search(query, status=status, limit=min(limit, 50))] @mcp.tool() def add_internal_note(ticket_id: str, note: str) -> dict: """Add an internal (not customer-visible) note to a ticket. This has a side effect. Only call it after you have read the ticket and the note adds information a human agent would want. Returns the note id. """ return client.add_note(ticket_id, note, internal=True).as_dict() @mcp.resource("tickets://{ticket_id}") def get_ticket(ticket_id: str) -> str: """Full ticket thread as plain text, oldest message first.""" return client.get(ticket_id).as_text() if __name__ == "__main__": mcp.run() # stdio transport
Connect it to a coding agent and it is immediately usable:
bashclaude mcp add tickets -- python /path/to/server.py
Now "find the open tickets about login failures and add a note summarizing the common cause" is something the agent can do, with your server doing the work and the agent's harness deciding whether add_internal_note needs approval.
Tool Design Is Context Engineering
The model chooses and calls tools based entirely on the name, the description, and the schema. That text is the interface. Most "the model used the tool wrong" bugs are description bugs.
Names say what, descriptions say when. search_tickets is what. "Use this to find tickets before reading or updating them" is when. Models pick tools by matching the task to the when.
Describe the return shape. The model plans its next step from what it expects back. "Returns id, title, status, customer" lets it plan; "returns results" does not.
Constrain in the schema, then repeat in prose. An enum in the schema stops invalid values; the sentence in the docstring stops the model from trying them.
Say what has side effects. The docstring for add_internal_note tells the model it is consequential. Your harness enforces it; the description makes the model cooperate.
Keep the surface small. Five well-described tools beat twenty terse ones. If you need twenty, group them into separate servers per task so the host can load only the relevant one.
Return less. A tool that returns 50 KB of JSON floods the context. Return summaries with IDs, and offer a second tool (or a resource) to fetch detail on demand.
Consuming Servers From Your Own Harness
Your harness is a host. The client library gives you a session; from it you list the server's tools, convert their schemas to your model's tool format, and dispatch calls.
pythonfrom mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client params = StdioServerParameters(command="python", args=["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()).tools # name, description, inputSchema visible = [to_model_schema(t) for t in tools if t.name in allowed_for(task)] ... result = await session.call_tool("search_tickets", {"query": "login", "limit": 5})
Everything from the harness module still applies. MCP standardizes discovery and invocation; it does not decide permissions, budgets, or approvals. Those stay in your harness, and an MCP server you did not write is untrusted code: its tool descriptions are content the model reads, and a malicious server can inject through them. Treat third-party servers like third-party packages: pin them, review them, and scope what they can see.
Where the Other Protocols Fit
You will hear about several protocols. Most teams need one.
- MCP connects an agent to tools and data. Vertical. Use it.
- A2A (Agent-to-Agent) connects one agent to another: each publishes an agent card describing what it can do, and another agent can discover it and delegate a task. Horizontal. You need it when two independently owned agents (different teams, different companies) must cooperate. Inside one team, a subagent call is simpler.
- AG-UI and similar standardize streaming an agent's state to a user interface. Relevant if you are building the chat UI itself.
- Payment and commerce protocols let agents transact. Watch this space; do not build on it until your product needs it.
The stack that is becoming the default: MCP for tools, A2A between organizations, your harness in the middle owning policy.
Common Mistakes and Bad Instincts
- Terse descriptions. "Search tickets." The model has nothing to plan with.
- One giant server. Forty tools in every context.
- Returning everything. Summaries plus IDs, detail on demand.
- Trusting server descriptions. They are content. Review third-party servers.
- Letting MCP replace the harness. MCP is the plug; policy is still yours.
- Reaching for A2A internally. A subagent call is a function call.
Where to Go Next
- context-engineering-designing-what-the-model-sees: tool definitions are the largest stable block in most contexts
- harness-engineering-the-runtime-around-the-model: the policy layer MCP does not provide
- agent-evals-trajectories-tool-calls-and-regression-suites: measure whether the model actually uses your tools correctly
What to Practice Next
Build an MCP server for a system you have access to (a database, an internal API, a SaaS tool with an API) with at least three tools, one of which has a side effect, and one resource. Connect it to a coding agent and to your own harness. Then write a 15-task eval: for each task, the tools that should be called and in what order. Run it, and fix failures by improving descriptions and schemas before touching anything else. Publish the server with a README that shows the eval results.
Module 21 of 34 · Software Engineer to ML/AI Engineer
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsAgentic Coding: Working With Claude Code, Codex, and Cursor
Coding agents are now the default way software gets written. Learn the gather-act-verify loop, how to write CLAUDE.md and AGENTS.md files that actually steer an agent, when to use skills and subagents, and how to review agent output like a senior engineer.
Context Engineering: Designing What the Model Sees
The context window is a budget, and everything competes for it: the system prompt, the tool list, retrieved documents, memory, and the conversation so far. Learn to design the context deliberately, scope tools per task, compact without losing what matters, and treat cache hit rate as the metric it has become.
Harness Engineering: The Runtime Around the Model
Agent = model + harness. The harness is the deterministic runtime that validates, authorizes, executes, and logs every action the model proposes. Learn its five layers, build one from scratch, and adopt the loop that turns every agent failure into a permanent fix.