Method·9 min read·8 July 2026

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

MCP Server Tutorial Python: Build and Deploy in 2026

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.

bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install mcp anthropic python-dotenv

Create the project skeleton:

bash
mkdir my_mcp_server && cd my_mcp_server
touch server.py .env README.md

Store secrets in .env, never in source:

bash
# .env
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
DATABASE_URL=postgresql://user:pass@host/db

Load them at startup with python-dotenv:

python
# server.py (top of file)
from dotenv import load_dotenv
load_dotenv()
import os
GITHUB_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.

python
# server.py
from mcp.server.fastmcp import FastMCP
from mcp import types
import httpx, os
from dotenv import load_dotenv
load_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.

Anthropic , Claude Tool Use Documentation

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 categoryCauseCorrect response
Access failureAuth token missing or expiredisError: true with ACCESS_DENIED code
Not foundResource does not existisError: true with NOT_FOUND code
Upstream errorThird-party API returned 5xxisError: true with UPSTREAM_ERROR code
Valid empty resultQuery succeeded, zero rowsisError: 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:

python
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent
import httpx, os
mcp = 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 success
or {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 error
return {
"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:

python
# bottom of server.py
if __name__ == "__main__":
mcp.run(transport="stdio")

Run it:

bash
python server.py

To connect it to Claude Code, add a .mcp.json in your project root:

json
{
"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:

bash
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:

bash
pip install pytest pytest-asyncio respx
python
# test_server.py
import pytest, respx, httpx
from server import search_github_issues
@pytest.mark.asyncio
@respx.mock
async 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 None
assert result["issues"] == []
@pytest.mark.asyncio
@respx.mock
async 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 modeSymptomFix
Too broadClaude calls the tool for unrelated tasksNarrow the scope in the first sentence
Missing return shapeClaude misinterprets the responseDocument field names and types in the docstring
Overlapping toolsClaude picks the wrong tool consistentlySplit or rename; see Tool Splitting for Specificity

A well-formed description follows this template:

text
[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.

Anthropic , Claude Tool Use Documentation

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.

python
@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.

python
# server.py (HTTP transport variant)
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8080)

A minimal production checklist:

ConcernRecommendation
AuthenticationValidate a bearer token on every request before routing to tools
SecretsInject via environment variables; never hardcode
LoggingEmit structured JSON logs with tool_name, duration_ms, error_code
Rate limitingApply per-client limits at the gateway, not inside tool handlers
TimeoutSet explicit timeouts on all upstream HTTP calls (10 s is a reasonable default)
Schema validationUse 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?
Python 3.10 or later is required by the official MCP Python SDK. Python 3.11 and 3.12 are both well-tested. The SDK uses modern type-hint syntax and async/await throughout, so older versions will not work without significant modification.
Can I use FastAPI or Flask instead of the MCP SDK?
You can expose an HTTP/SSE endpoint from any framework, but you would need to implement the MCP protocol wire format manually. The official Python SDK handles framing, schema generation, and transport negotiation for you. Using the SDK is strongly recommended unless you have a specific reason to avoid it.
How do I connect my Python MCP server to Claude Code?
Add a `.mcp.json` file to your project root with a `mcpServers` key. Set `command` to `python`, `args` to your server file, and pass secrets via the `env` object using `${VAR_NAME}` expansion syntax. Claude Code reads this file at startup and launches the server process automatically.
How many tools should a single MCP server expose?
Keep the tool count low enough that Claude can distinguish between them without ambiguity. There is no hard limit in the protocol, but exposing more than 10 to 15 tools on a single server increases the risk of misrouting. Split tools by domain or role if the count grows beyond that range.
Does the MCP Python SDK support async tool handlers?
Yes. The SDK is built on asyncio and supports both `async def` and regular `def` tool handlers. Async handlers are preferred for any tool that makes network calls, as they avoid blocking the server's event loop while waiting for upstream responses.
How do I debug an MCP server when Claude is not calling my tool?
Run the MCP Inspector (`npx @modelcontextprotocol/inspector python server.py`) to verify the tool is registered and its schema is valid. Then check the tool description: Claude selects tools based on the description text, so a vague or missing description is the most common cause of a tool being ignored.

People also ask

How do I build an MCP server in Python from scratch?
Install the `mcp` package, create a `FastMCP` instance, and decorate async functions with `@mcp.tool()`. Each function's docstring becomes the tool description Claude uses for routing. Run the server with `mcp.run(transport='stdio')` and connect it via a `.mcp.json` config file in your project.
What is the difference between MCP tools and MCP resources?
Tools are callable functions that perform actions or retrieve dynamic data; Claude invokes them during an agentic loop. Resources are read-only content endpoints, suitable for static documentation or configuration that Claude reads before deciding which tools to call. Tools are the more commonly used primitive.
How do I test an MCP server without a Claude client?
Use the MCP Inspector CLI (`npx @modelcontextprotocol/inspector python server.py`). It lists registered tools, lets you invoke them with custom inputs, and displays the raw JSON response. For unit tests, mock the HTTP layer with `respx` and `pytest-asyncio` to test tool handlers in isolation.
How do I pass secrets to a Python MCP server securely?
Store secrets in environment variables and load them with `python-dotenv` at startup. In the `.mcp.json` config, reference them with `${VAR_NAME}` syntax so Claude Code expands them from the shell environment at launch. Never hardcode tokens in the config file or commit them to version control.
Can a Python MCP server handle multiple users at once?
Yes, using the HTTP/SSE transport instead of stdio. Run the server with `mcp.run(transport='sse')` and place an authentication gateway in front of it to validate per-user tokens, enforce rate limits, and route traffic. The stdio transport is single-process and suited only for local or single-agent use.

About the author

Solomon Udoh

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.

Start studying