Skip to content

Build an MCP server

By hand first, then with the official SDK, plugged into Claude.

Write the server side yourself, no SDK: read a message, send a reply. Once you've done it by hand, every SDK is obvious.

Sign in to run your code. It's free.

Write it yourself

Each input line is a JSON-RPC message from an MCP client. Finish handle(msg): return the reply as a dict, or None for notifications (messages with no id).

tools/list returns {"tools": TOOLS}. tools/call runs RUN[name](**arguments) and returns {"content": [{"type": "text", "text": ...}], "isError": False}; if the tool raises, the same shape with the error message and isError: True.

An unknown tool is error -32602 with message Unknown tool: <name>. Any other method is error -32601, Method not found. Errors look like {"jsonrpc": "2.0", "id": ..., "error": {"code": ..., "message": ...}}.

▸ Runs first: set-up code (30 lines). Open to read it.
1import json
2import sys
3
4ORDERS = {"A123": {"order_id": "A123", "status": "shipped", "tracking_id": "TRK-889"}}
5
6TOOLS = [
7 {
8 "name": "get_order",
9 "description": "Look up an order by its id. Returns status and tracking id.",
10 "inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
11 }
12]
13
14
15def get_order(order_id):
16 if order_id not in ORDERS:
17 raise ValueError("No order " + order_id + ". Order ids look like A123.")
18 return json.dumps(ORDERS[order_id])
19
20
21RUN = {"get_order": get_order}
22
23
24def serve(handle):
25 """Reads one JSON-RPC message per line from stdin, like the stdio transport, and prints each reply."""
26 for line in sys.stdin:
27 if line.strip():
28 reply = handle(json.loads(line))
29 if reply is not None:
30 print(json.dumps(reply, sort_keys=True))

Now the real thing: fifteen lines with the official SDK, connected to tools you already use.

1. The server, with the official Python SDK

Save as quickcart_server.py. The decorator reads the type hints and docstring to build the schema you wrote by hand. In SDK v2, FastMCP was renamed MCPServer; most tutorials online still show the old name.

1from mcp.server import MCPServer
2from mcp.server.mcpserver.exceptions import ToolError
3
4ORDERS = {"A123": {"status": "shipped", "tracking_id": "TRK-889"}}
5
6mcp = MCPServer("quickcart")
7
8
9@mcp.tool()
10def get_order(order_id: str) -> dict:
11 """Look up an order by its id. Returns status and tracking id."""
12 if order_id not in ORDERS:
13 raise ToolError(f"No order {order_id}. Order ids look like A123.")
14 return ORDERS[order_id]
15
16
17if __name__ == "__main__":
18 mcp.run()
$pip install "mcp>=2"

2. Poke it with the MCP Inspector

It opens in your browser and shows the same messages you just stepped through.

$npx @modelcontextprotocol/inspector python quickcart_server.py

3. Plug it into a real agent

Claude Code:

$claude mcp add quickcart -- python /full/path/to/quickcart_server.py

Claude Desktop (claude_desktop_config.json) or Cursor (.cursor/mcp.json):

1{
2 "mcpServers": {
3 "quickcart": {
4 "command": "python",
5 "args": ["/full/path/to/quickcart_server.py"]
6 }
7 }
8}

Then ask it: “Where is order A123?”