
Build Your First MCP Server in Python
A step-by-step Python tutorial for building, running, testing, and hardening a small MCP server with a typed tool.
A step-by-step Python tutorial for building, running, testing, and hardening a small MCP server with a typed tool.
The client discovers get order status.
This tutorial builds a small Python MCP server that exposes a typed order-status tool. The goal is not a production order system; it is a clean mental model for server construction, testing, and hardening.
The official Python SDK evolves. Verify the installed SDK documentation when copying code into a real project, especially transport and run-command details.
Outcome
You will create a server with a clear identity, one typed read-only tool, argument validation from type hints, a local execution path, and a checklist for production controls.
Prerequisites
Use a recent supported Python version and an isolated virtual environment. Install the official MCP Python SDK according to its current repository instructions.
The stable architectural pattern is: instantiate FastMCP, decorate a typed function as a tool, and run the server over the selected transport.
Step 1: define domain logic
Start with deterministic code. Keep business logic separate from protocol wiring so it can be tested without an MCP client.
ORDERS = { "4812": {"status": "delayed", "eta": "2026-08-08"}, "4813": {"status": "in_transit", "eta": "2026-08-06"}, }
def lookuporder(orderid: str) -> dict: order = ORDERS.get(orderid) if order is None: raise ValueError("Order not found") return {"orderid": order_id, **order}
In production, this function would call an API with a scoped service identity and enforce access to the order.
Step 2: expose a FastMCP tool
Create the server and wrap the domain function with a typed tool.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders-demo")
@mcp.tool() def getorderstatus(orderid: str) -> dict: """Return status and ETA for an order the current user may access.""" return lookuporder(order_id)
The function name, docstring, and type hints help form the contract. Prefer an explicit structured output model when the installed SDK supports the pattern you need.
Step 3: choose a transport
For local development, STDIO is often simplest. The host launches the Python process and exchanges messages through standard streams.
Keep standard output reserved for MCP messages. Send diagnostics to standard error or structured logging.
For a managed remote server, use the SDK current Streamable HTTP guidance. Remote deployment adds OAuth, TLS, origin validation, tenant isolation, rate limits, and service observability.
Step 4: run and inspect
Run the server using the command supported by your SDK or host configuration. Connect with an MCP-capable development client or official inspector tooling.
Verify:
- The client discovers getorderstatus.
- The schema requires order_id as a string.
- Order 4812 returns a structured result.
- An unknown order returns explicit failure.
- Debug logs do not corrupt STDIO output.
Step 5: test both layers
Unit-test lookup_order directly. Then add integration tests that connect a client session, list tools, call the tool, and inspect the result.
Include missing ID, overly long ID, unsupported characters, permission denial, backend timeout, and malformed backend data.
Do not test only through chat. Chat output can hide whether the contract behaved correctly.
Add authorization before real data
The demo has no identity. A production server must connect the request to an authenticated principal and enforce tenant and object access.
Do not accept a user_id argument from the model and trust it as identity. Identity must come from an authenticated connection or trusted host context.
Add timeouts and bounded results
Set deadlines on downstream calls. Return only fields needed by the host. Limit response size and avoid internal notes, payment data, or credentials.
For writes, add idempotency keys, approval boundaries, audit logs, and reconciliation for ambiguous outcomes.
Suggested structure
ordersmcp/ server.py domain.py auth.py settings.py tests/ testdomain.py test_tools.py
Separating protocol, domain, authentication, and configuration keeps the server understandable as it grows.
Common mistakes
- All business logic in decorated functions.
- Debug messages on STDIO output.
- Trusting model-supplied identity.
- Generic API or database tools.
- Returning secrets or large payloads.
- Copying old SDK run commands without checking the installed version.
Production checklist
- Pin and review the SDK version.
- Use explicit configuration and secret management.
- Enforce least-privilege downstream credentials.
- Add structured traces and metrics.
- Bound concurrency, size, and duration.
- Test cancellation, retries, and shutdown.
- Document side effects and approval rules.
My Take
A first MCP server should be boring: one coherent domain, one narrow tool, deterministic logic, and visible errors. Complexity should enter only when a real requirement demands it.




