Build Your Own MCP Server in Python: A Hands-On 2026 Guide
You have a tool — an internal API, a database, a SaaS endpoint — that your AI assistant cannot reach. The tools are there. The model just has no way to call them.
That is the exact gap Model Context Protocol (MCP) was built to close, and in 2026 it is the default way production AI agents talk to the world. If you are building agents that need real data — not just chat — you will end up writing an MCP server. This guide shows you how, end to end, with code you can run today.
Why This Matters
MCP is a JSON-RPC 2.0 protocol that standardizes how AI applications connect to external tools, resources, and prompts. Think of it as USB-C for AI: one standard connector, thousands of devices. Before MCP, every agent integration was a bespoke glue script — one client for OpenAI function calling, another for LangChain tools, another for your in-house agent. Every one of them broke the moment the model or the framework changed.
In 2026, the ecosystem has consolidated hard around MCP:
- Every major agent framework (Claude, OpenAI Agents, LangGraph, Hermes Agent, Cursor, Windsurf) ships a native MCP client.
- The protocol spec is stable — JSON-RPC 2.0, three capability surfaces: tools, resources, prompts.
- Transports are boring and reliable: stdio for local, Streamable HTTP for remote.
-
Server SDKs are mature — the Python
mcppackage with FastMCP gives you a full server in ~20 lines.
The result: write one server, and every MCP-compatible assistant on the planet can use your tools. That is the entire value proposition, and it is why the protocol went from niche to table stakes in under a year.
What an MCP Server Actually Is
An MCP server is a program that speaks JSON-RPC 2.0 over a transport. It exposes three capability surfaces:
| Surface | What it does | Example |
|---|---|---|
| Tools | Functions the model can call (with side effects or not) |
get_weather(city), create_issue(repo, title)
|
| Resources | Read-only data the model can load |
file:///logs/app.log, db://users/42
|
| Prompts | Reusable prompt templates the client can render | "Summarize this PR" with a placeholder for the PR number |
Tools do things. Resources are things. Prompts are recipes. That mental model covers 95% of what you will build.
Building Your First Server — the FastMCP Way
The fastest path is the official Python SDK with the FastMCP helper. Install it:
pip install "mcp[fastmcp]"
Here is a complete, working server that wraps a fictional REST API (say, your company's ticket system):
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("ticket-server", log_level="WARNING")
@mcp.tool()
async def get_ticket(ticket_id: str) -> str:
"""Fetch a support ticket by ID. Returns JSON with status, priority, and assignee."""
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"https://api.example.com/tickets/{ticket_id}")
resp.raise_for_status()
return resp.text
@mcp.tool()
async def search_tickets(query: str, limit: int = 5) -> str:
"""Search support tickets by free-text query. Returns up to `limit` results."""
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
"https://api.example.com/tickets",
params={"q": query, "limit": limit},
)
resp.raise_for_status()
return resp.text
if __name__ == "__main__":
mcp.run(transport="stdio")
That is it. Two tools, zero boilerplate. Run it:
python3 ticket_server.py
Nothing happens yet — stdio servers wait for a client. The FastMCP class derives the tool schemas from your type hints and docstrings, so the model sees get_ticket(ticket_id: str) with the description as its help text. Docstrings are not optional — they are the model's documentation. Write them like you would for a human.
Exposing a Resource
Resources give the model read-only access to data. This one exposes the last N lines of a log file:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("log-server", log_level="WARNING")
@mcp.resource("logs://app/{lines}")
def recent_logs(lines: int) -> str:
"""Last N lines of the application log."""
with open("/var/log/app.log") as f:
return "".join(f.readlines()[-lines:])
The URI scheme is yours to define. Clients list resources, then fetch them by URI. Keep resources side-effect free — the protocol and your users will thank you.
Registering the Server with Your Agent
Once the server runs, register it with whatever MCP client you use. In Hermes Agent's config, a stdio server looks like this:
mcp_servers:
ticket_server:
command: "/home/you/venv/bin/python3"
args: ["/home/you/ticket_server.py"]
env:
TICKET_API_KEY: "sk-..."
timeout: 120
The client spawns the process, speaks JSON-RPC over stdin/stdout, and auto-discovers your tools. For a remote deployment, switch to Streamable HTTP:
mcp_servers:
remote_api:
url: "https://my-server.example.com/mcp"
headers:
Authorization: "Bearer sk-..."
timeout: 180
Testing Your Server Without a Client
Do not hand-test stdio servers by eye. Drive the protocol directly with a small script that speaks JSON-RPC over stdin/stdout:
import asyncio, json, sys
async def test():
proc = await asyncio.create_subprocess_exec(
sys.executable, "ticket_server.py",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
# 1. Initialize
proc.stdin.write(json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"},
},
}) + "\n")
await proc.stdin.drain()
resp = json.loads(await asyncio.wait_for(proc.stdout.readline(), 5))
assert "result" in resp, f"init failed: {resp}"
# 2. Send the initialized notification
proc.stdin.write(json.dumps({
"jsonrpc": "2.0", "method": "notifications/initialized",
}) + "\n")
await proc.stdin.drain()
# 3. List tools
proc.stdin.write(json.dumps({
"jsonrpc": "2.0", "id": 2, "method": "tools/list",
}) + "\n")
await proc.stdin.drain()
resp = json.loads(await asyncio.wait_for(proc.stdout.readline(), 5))
tools = resp.get("result", {}).get("tools", [])
print(f"{len(tools)} tools: {[t['name'] for t in tools]}")
proc.terminate()
asyncio.run(test())
Expected output: 2 tools: ['get_ticket', 'search_tickets']. If the schema generation, transport, or registration is broken, this surfaces it in seconds — before any agent ever touches the server.
Choosing a Transport
| Transport | Best for | Trade-offs |
|---|---|---|
| stdio | Local tools, dev machines, agent-side servers | Zero network config, inherits process lifecycle; not reachable remotely |
| Streamable HTTP | Remote/team deployments, SaaS integrations | Needs an HTTP endpoint, auth, and lifecycle management |
| SSE (legacy) | Older clients | Deprecated in favor of Streamable HTTP — avoid for new builds |
Rule of thumb: stdio by default, HTTP when you must share the server. Remote MCP servers multiply your attack surface — every tool you expose remotely is an API endpoint with an LLM as the caller. Treat auth as mandatory, and scope tools to the least privilege the agent needs.
Anti-Patterns I've Seen in Production
After running multi-agent systems with MCP servers in the wild, these are the mistakes that actually hurt:
Tool schemas from unstructured params. If a tool takes one giant
**kwargsor a single opaque string, the model will guess wrong and burn tokens retrying. Type every parameter; constrain with enums and descriptions.Silent failures.
except Exception: passin a tool makes the model hallucinate success. Raise errors with structured messages — the agent needs to know the call failed so it can retry or tell the user.Ignoring timeouts. An HTTP tool that hangs forever freezes the whole agent loop. Always set explicit timeouts (10s in the examples above is a sane default) and let them propagate.
Exposing internal tools remotely. A
run_sqltool is fine on a local stdio server; published over HTTP with a weak token, it is a data exfiltration endpoint. Profile your tools by blast radius before choosing the transport.Not testing the protocol layer. Your integration tests hit the wrapped API, but nobody verifies
tools/listandtools/callover the actual transport. The 40-line test above catches more real bugs than any mock.Docstring rot. When you update a tool's behavior and forget the docstring, the model keeps describing the old behavior. Docstrings and schemas are code; review them in code review.
What a Complete Server Setup Looks Like
A production MCP server is more than one file:
mcp-server/
├── server.py # FastMCP app + tool/resource definitions
├── api_client.py # thin wrapper over the external API
├── config.py # env vars, pydantic-settings, single source of truth
├── tests/
│ ├── test_protocol.py # JSON-RPC over stdio (the test above)
│ └── test_tools.py # unit tests for tool logic
└── pyproject.toml
Keep tool definitions thin — they should read like a table of contents, with the real logic in testable modules. The tools layer is the contract with the model; the logic layer is the contract with your tests.
Pro Tips
-
Name tools like actions, not nouns:
search_tickets,assign_ticket, notticket_service. - Return JSON as text — clients and models handle it natively; don't over-structure your responses.
- Log the tool calls — an audit log of what the agent did with your tools is invaluable for debugging and for catching runaway loops.
- Version your protocol — pin the SDK, and keep the protocol version in your CI matrix so dependency bumps don't break agents silently.
- One concern per server — a "ticket server" and a "metrics server" are easier to secure, test, and reason about than one "everything server".
The Bottom Line
MCP is not another framework to learn — it is the interoperability layer that makes the agent ecosystem composable. Twenty lines of FastMCP code turns any internal API into a first-class tool for every MCP-compatible assistant. Start with a stdio server, test it at the protocol level, and only move to HTTP when you actually need remote access.
The agents you build in 2026 will be judged by what they can do, and what they can do is bounded by the tools you give them.
Built with Hermes Agent. Follow **NexMind AI* for more hands-on guides on production AI infrastructure.*
Want the full picture? This guide covers the mechanics of one server. The AI Agents & Automation Playbook goes further — production multi-agent architecture, orchestrating several MCP servers, agent-to-agent protocols, and how to turn agent systems into revenue instead of demos.
PS: Building agents that need reliable automation behind them? The playbook includes ready-to-deploy patterns for exactly that — grab it here →
Top comments (0)