Build an MCP server
Reading about MCP only goes so far. This chapter builds a small server for a food delivery company's orders system, in Python and in TypeScript, tests it without any AI involved, and plugs it into an agent. The whole thing is under fifty lines in either language, which is a good sign: most of the effort in a real server is in designing good tools, not in the protocol.
Both official SDKs moved to a v2 line built for the 2026-07-28 specification. If you find older
tutorials using FastMCP in Python or the @modelcontextprotocol/sdk package in TypeScript, those
are the v1 names.
Before writing any code: which two or three tools would you expose from an orders system to a support agent, and which tool would you deliberately not expose?
Install#
uv init orders-mcp && cd orders-mcp
uv add "mcp[cli]" # Python 3.11 or laterThe server#
Two read only tools and one resource. Read the descriptions as carefully as the code: they are what the model reads when it decides which tool to call.
# server.py
from mcp.server import MCPServer
mcp = MCPServer("orders")
@mcp.tool()
def get_order_status(order_id: str) -> str:
"""Delivery status and ETA of one order. Use when the customer asks
where an order is. order_id is the number shown in the app, e.g. 44821."""
order = orders_db.get(order_id) # your real data access
if order is None:
return f"No order {order_id}. Ask the customer to check the number."
return f"Order {order_id}: {order.status}, ETA {order.eta}."
@mcp.tool()
def list_recent_orders(customer_id: str, days: int = 30) -> str:
"""The customer's orders from the last N days (max 90), newest first.
Use when the customer refers to an order without giving its number."""
orders = orders_db.recent(customer_id, min(days, 90))
return "\n".join(f"{o.id} | {o.restaurant} | {o.status}" for o in orders) or "No orders."
@mcp.resource("orders://policy/refunds")
def refund_policy() -> str:
"""The current refund policy, for the host to load as context."""
return policies.load("refunds")Three details worth copying. The description says when to use the tool, not just what it does.
Errors are written for the model: "ask the customer to check the number" lets it recover on the
next turn. And in the TypeScript version, logs go to stderr, because on stdio, standard output
carries the protocol and a stray console.log corrupts it.
Test it with no AI involved#
Before any model sees your server, poke it directly with the MCP Inspector, a small web UI that lists your tools and lets you call them by hand.
uv run mcp dev server.pyIf a tool behaves badly in the Inspector, no amount of prompting will fix it in an agent.
Plug it into an agent#
Most hosts take the same information: a name, and the command that starts the server. For example, with Claude Code:
claude mcp add orders -- uv run mcp run server.pyOther hosts (Cursor, VS Code, desktop chat apps) use a JSON config file with the same two pieces. And in your own application, the model provider's SDK or your agent framework can connect to MCP servers directly, so the same server serves a coding assistant today and your support bot tomorrow.
Going remote#
For a server many people share, run it over Streamable HTTP instead of stdio, for example with
uv run mcp run server.py --transport streamable-http in Python, behind your normal web stack.
Because the protocol is now stateless, you can run several copies behind a load balancer like any
API. Remote servers must handle authorization, which is the subject of the next chapter.
The fastest way to build a bad MCP server is to wrap every endpoint as a tool. Models do better with a few task shaped tools than forty thin ones, as the tool design chapter explains. Design the tools first, then write the server.
Checkpoint#
1. Why must a stdio MCP server never print debug output to standard output?
2. What is the best first test of a new MCP server?
3. What changed in the v2 Python SDK compared with older tutorials?
- Python:
MCPServerand@mcp.tool(). TypeScript:McpServerandregisterTool. - Descriptions say when to use a tool. Errors tell the model how to recover.
- Test in the Inspector before any agent.
- stdio: logs to stderr. Remote: Streamable HTTP plus authorization.
An MCP server is a small program. In Python I create an MCPServer and decorate functions as tools and resources, in TypeScript I create an McpServer and call registerTool with a zod schema and serve it over stdio. The work that matters is tool design: a few task shaped tools whose descriptions say when to use them, and errors written so the model can recover. I test every tool by hand in the MCP Inspector before any model sees it, keep logs on stderr because stdout is the protocol channel, and register the server with a host by giving it a name and the start command. For shared use I run it over Streamable HTTP, which scales like any stateless API now, and add authorization.