MCP Server Tutorial Python: Build and Deploy in 2026
Follow this mcp server tutorial python guide to build, test, and deploy a production-ready MCP server with Claude in under an hour.
By Solomon Udoh · AI Architect & Certification Lead

This mcp server tutorial python guide walks through every layer of a production-ready Model Context Protocol server: project setup, tool registration, error handling, and deployment alongside a Claude agent. By the end you will have a working server you can connect to Claude Code or any MCP-compatible client, and you will understand the design decisions the CCA-F exam tests in Domain 2 (Tool Design & MCP Integration, 18% of the exam).
What is MCP and why does it matter for Claude agents?
MCP (Model Context Protocol) is an open protocol that lets a language model call external tools through a standardised interface. Instead of embedding tool logic inside a system prompt or hard-coding API calls in your orchestration layer, you expose capabilities as named tools on an MCP server. Claude discovers those tools at runtime, reads their descriptions, and decides when to invoke them.
The practical payoff is significant. Tool logic lives in one place, versioned independently of your prompts. Multiple agents or Claude Code sessions can share the same server. And because the protocol is typed, you get structured error signals rather than free-text failure messages that the model has to interpret.
Per Anthropic's MCP documentation, the protocol defines three primitives: tools (callable functions), resources (readable content), and prompts (reusable prompt templates). This tutorial focuses on tools, which are the primitive Claude uses most frequently in agentic loops.
How do you set up a Python MCP server project?
Start with a clean virtual environment and the official Python SDK. The SDK handles the transport layer (stdio or HTTP/SSE) so you can focus on tool logic.
python -m venv .venvsource .venv/bin/activate # Windows: .venv\Scripts\activatepip install mcp anthropic python-dotenv
Create the project skeleton:
mkdir my_mcp_server && cd my_mcp_servertouch server.py .env README.md
Store secrets in .env, never in source:
# .envGITHUB_TOKEN=ghp_xxxxxxxxxxxxDATABASE_URL=postgresql://user:pass@host/db
Load them at startup with python-dotenv:
# server.py (top of file)from dotenv import load_dotenvload_dotenv()import osGITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
This pattern matters for the exam: the MCP scoping hierarchy concept covers how environment variable expansion works inside MCP config files, and questions often test whether you know to keep credentials out of the config JSON itself. See also Environment Variable Expansion in MCP Config for the exact syntax Claude Code expects.
How do you register tools on an MCP server?
The Python SDK exposes a FastMCP class (or the lower-level Server class for full control). FastMCP is the right default for most teams.
# server.pyfrom mcp.server.fastmcp import FastMCPfrom mcp import typesimport httpx, osfrom dotenv import load_dotenvload_dotenv()mcp = FastMCP("my-server")@mcp.tool()async def search_github_issues(repo: str,query: str,state: str = "open",) -> list[dict]:"""Search GitHub issues in a repository.Args:repo: Owner/repo string, e.g. 'anthropics/anthropic-sdk-python'.query: Free-text search query.state: 'open', 'closed', or 'all'. Defaults to 'open'.Returns a list of matching issues with id, title, url, and state fields."""token = os.environ["GITHUB_TOKEN"]url = f"https://api.github.com/search/issues"params = {"q": f"{query} repo:{repo} state:{state}", "per_page": 10}headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}async with httpx.AsyncClient() as client:r = await client.get(url, params=params, headers=headers)r.raise_for_status()items = r.json().get("items", [])return [{"id": i["number"], "title": i["title"], "url": i["html_url"], "state": i["state"]}for i in items]
Notice the docstring. Claude uses the tool description as its primary signal for deciding whether to call this tool. A vague description like "searches GitHub" will cause misrouting; a precise one that names the input types and return shape will not. This is the core insight behind Tool Descriptions as Selection Mechanism.
Tool descriptions are the primary mechanism by which Claude selects among available tools. A description that omits the return shape or the scope of the tool is a latent routing bug.
How do you handle errors correctly in an MCP tool?
MCP defines a typed error model. When a tool fails, you should return a structured error response rather than raising an unhandled exception, which would surface as an opaque transport error to the client.
The protocol distinguishes four categories of failure. The table below maps them to the correct response pattern:
| Error category | Cause | Correct response |
|---|---|---|
| Access failure | Auth token missing or expired | isError: true with ACCESS_DENIED code |
| Not found | Resource does not exist | isError: true with NOT_FOUND code |
| Upstream error | Third-party API returned 5xx | isError: true with UPSTREAM_ERROR code |
| Valid empty result | Query succeeded, zero rows | isError: false, return empty list |
The last row is the one candidates most often get wrong. An empty search result is not an error; returning isError: true for it causes the agent to retry unnecessarily or escalate to a human when no escalation is warranted. See Access Failure vs Valid Empty Result for the exam-level treatment.
Here is the corrected tool with proper error handling:
from mcp.server.fastmcp import FastMCPfrom mcp.types import TextContentimport httpx, osmcp = FastMCP("my-server")@mcp.tool()async def search_github_issues(repo: str, query: str, state: str = "open") -> dict:"""Search GitHub issues. Returns {issues: [...], error: null} on successor {issues: [], error: {code, message}} on failure."""token = os.environ.get("GITHUB_TOKEN")if not token:return {"issues": [], "error": {"code": "ACCESS_DENIED", "message": "GITHUB_TOKEN not configured"}}url = "https://api.github.com/search/issues"params = {"q": f"{query} repo:{repo} state:{state}", "per_page": 10}headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}try:async with httpx.AsyncClient(timeout=10.0) as client:r = await client.get(url, params=params, headers=headers)if r.status_code == 401:return {"issues": [], "error": {"code": "ACCESS_DENIED", "message": "Invalid token"}}r.raise_for_status()items = r.json().get("items", [])# Empty list is a valid result, not an errorreturn {"issues": [{"id": i["number"], "title": i["title"], "url": i["html_url"]}for i in items],"error": None,}except httpx.TimeoutException:return {"issues": [], "error": {"code": "UPSTREAM_ERROR", "message": "GitHub API timed out"}}except httpx.HTTPStatusError as exc:return {"issues": [], "error": {"code": "UPSTREAM_ERROR", "message": str(exc)}}
The MCP isError Flag Pattern and Structured Error Metadata concepts cover how Claude interprets these fields when deciding whether to retry, reroute, or surface the failure to a human.
How do you run and test the server locally?
The SDK ships with a stdio transport that Claude Code and the MCP Inspector both support. Add an entry point:
# bottom of server.pyif __name__ == "__main__":mcp.run(transport="stdio")
Run it:
python server.py
To connect it to Claude Code, add a .mcp.json in your project root:
{"mcpServers": {"my-server": {"command": "python","args": ["server.py"],"env": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"}}}}
The ${GITHUB_TOKEN} syntax tells Claude Code to expand the variable from the shell environment at launch time, keeping the token out of the committed file. This is the pattern tested in Environment Variable Expansion in MCP Config.
For automated testing, use the MCP Inspector CLI:
npx @modelcontextprotocol/inspector python server.py
The Inspector lists all registered tools, lets you call them with arbitrary inputs, and shows the raw JSON response. Run it before connecting to any Claude client; it catches schema mismatches and missing environment variables before they cause confusing agent behaviour.
For unit tests, mock the HTTP layer with pytest and respx:
pip install pytest pytest-asyncio respx
# test_server.pyimport pytest, respx, httpxfrom server import search_github_issues@pytest.mark.asyncio@respx.mockasync def test_empty_result_is_not_error():respx.get("https://api.github.com/search/issues").mock(return_value=httpx.Response(200, json={"items": []}))result = await search_github_issues("owner/repo", "nonexistent-query")assert result["error"] is Noneassert result["issues"] == []@pytest.mark.asyncio@respx.mockasync def test_auth_failure_returns_access_denied():respx.get("https://api.github.com/search/issues").mock(return_value=httpx.Response(401))result = await search_github_issues("owner/repo", "query")assert result["error"]["code"] == "ACCESS_DENIED"
How do you write tool descriptions that Claude will use correctly?
Tool description quality is the single highest-leverage variable in MCP server design. The Writing Effective Tool Descriptions concept identifies three failure modes:
| Failure mode | Symptom | Fix |
|---|---|---|
| Too broad | Claude calls the tool for unrelated tasks | Narrow the scope in the first sentence |
| Missing return shape | Claude misinterprets the response | Document field names and types in the docstring |
| Overlapping tools | Claude picks the wrong tool consistently | Split or rename; see Tool Splitting for Specificity |
A well-formed description follows this template:
[Action verb] [specific resource] [scope constraint].Args:param_name: [type] [what it controls] [valid values or format].Returns [shape]: [field descriptions].Do NOT use this tool for [anti-use-cases].
The "Do NOT use this tool for" line is optional but valuable when two tools have overlapping surface areas. It costs a few tokens in the tool manifest but saves many more by preventing misrouting.
When multiple tools are available, Claude selects based on the tool description alone. A description that does not specify scope is equivalent to no description.
How do you add MCP resources for read-only content?
Resources expose read-only content (documentation, configuration, data catalogues) without the overhead of a tool call. They are appropriate when the content is static or changes slowly and Claude needs to read it before deciding which tools to invoke.
@mcp.resource("config://team-standards")async def team_standards() -> str:"""Returns the team's coding standards as markdown."""with open("standards.md") as f:return f.read()
Claude Code can read this resource at session start and use it to inform subsequent tool calls. The MCP Resources for Content Catalogs concept covers when resources outperform tools for this pattern.
What does a production deployment look like?
For a shared server that multiple agents or users access, stdio is not sufficient. Use the HTTP/SSE transport and put an authentication gateway in front of it.
# server.py (HTTP transport variant)if __name__ == "__main__":mcp.run(transport="sse", host="0.0.0.0", port=8080)
A minimal production checklist:
| Concern | Recommendation |
|---|---|
| Authentication | Validate a bearer token on every request before routing to tools |
| Secrets | Inject via environment variables; never hardcode |
| Logging | Emit structured JSON logs with tool_name, duration_ms, error_code |
| Rate limiting | Apply per-client limits at the gateway, not inside tool handlers |
| Timeout | Set explicit timeouts on all upstream HTTP calls (10 s is a reasonable default) |
| Schema validation | Use Pydantic models for tool inputs; reject malformed calls early |
For teams managing multiple MCP servers across projects, the MCP Server Integration Best Practices concept covers the governance patterns the CCA-F exam expects you to know, including how to decide between building a new server and consuming an existing one (see Build vs Use Decision for MCP Servers).
How does this connect to the CCA-F exam?
Domain 2 (Tool Design & MCP Integration) carries 18% of the exam weight. The task statements in that domain test your ability to write tool descriptions that route correctly, handle the four error categories, distinguish access failures from valid empty results, and configure MCP servers in Claude Code's three-level hierarchy.
As of 3 June 2026, more than 10,000 individuals have earned the CCA-F certification. The exam rewards deterministic, structured solutions: a tool that returns a typed error object will always score better in scenario questions than one that raises an unhandled exception and lets Claude infer the failure mode from stack-trace text.
If you want to drill these concepts with adaptive questions, our Tool Design & MCP Integration concept library covers all 30 task statements mapped to Domain 2, and our practice exams are scored on the same 100-to-1000 scale as the real exam, with 720 as the passing bar. AI Skill Certs is an independent prep platform; we are not affiliated with or endorsed by Anthropic.
Frequently asked questions
What Python version do I need to build an MCP server?
Can I use FastAPI or Flask instead of the MCP SDK?
How do I connect my Python MCP server to Claude Code?
How many tools should a single MCP server expose?
Does the MCP Python SDK support async tool handlers?
How do I debug an MCP server when Claude is not calling my tool?
People also ask
How do I build an MCP server in Python from scratch?
What is the difference between MCP tools and MCP resources?
How do I test an MCP server without a Claude client?
How do I pass secrets to a Python MCP server securely?
Can a Python MCP server handle multiple users at once?
About the author
AI Architect & Certification Lead
Solomon Udoh is an AI Architect who designs and ships production agent systems on the Claude API and Claude Code. He built AI Skill Certs' adaptive engine and authored its 174-concept knowledge graph, mapping every Claude Certified Architect - Foundations objective to hands-on, exam-aligned practice.
- Designs production multi-agent systems on the Claude API and Agent SDK
- Author of the AI Skill Certs knowledge graph (174 mapped exam concepts)
- Builds with MCP, Claude Code, structured outputs, and agentic loops daily
- Reviews every concept page against the official Anthropic exam guide
You might also like
Ready to put it into practice?
Study every exam concept with an adaptive tutor.