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 json2import sys34ORDERS = {"A123": {"order_id": "A123", "status": "shipped", "tracking_id": "TRK-889"}}56TOOLS = [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]131415def 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])192021RUN = {"get_order": get_order}222324def 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 MCPServer2from mcp.server.mcpserver.exceptions import ToolError34ORDERS = {"A123": {"status": "shipped", "tracking_id": "TRK-889"}}56mcp = MCPServer("quickcart")789@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]151617if __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.py3. Plug it into a real agent
Claude Code:
claude mcp add quickcart -- python /full/path/to/quickcart_server.pyClaude 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?”