Everything so far has run over stdio, which means one machine. Today we changed one line and put the server on the network.
0. First: The SDK Renamed Its Server Class
MCP Python SDK v2 renamed FastMCP to MCPServer. Every example from here on uses it.
# v1 (old) — fails on a fresh install
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("cafe")
# v2 (current)
from mcp.server import MCPServer
mcp = MCPServer("cafe")
- The decorators don’t change.
@mcp.tool(),@mcp.resource()and@mcp.prompt()work exactly as before. - There’s no shim.
pip install mcpnow installs v2, so the old import raisesModuleNotFoundError: No module named 'mcp.server.fastmcp'. - Staying on v1 for now? Pin it with
mcp>=1.28,<2. Otherwise the next install upgrades you silently. - Don’t confuse this with the standalone FastMCP project. That’s a separate library with its own versions. This rename only affects the class inside the official SDK.
1. The Language Underneath: JSON-RPC
MCP clients and servers talk in JSON-RPC 2.0. Every request has the same shape:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": { "name": "place_order", "arguments": { "table": 4 } }
}
idmatches a response to its request.methodsays what’s wanted, such astools/list,tools/callorresources/read.paramscarries the details.
You never write this. You write a Python function with @mcp.tool(), and the SDK converts it to and from JSON-RPC.
The model doesn’t write it either. Here’s who does what:
Model → "call place_order with table 4" (a tool call, in the model's own format)
Client → translates it into JSON-RPC → sends it to the server
Server → runs the function → replies in JSON-RPC
Client → hands the result back to the model
The model only ever sees tools and results. JSON-RPC lives between the client and the server, and that’s exactly the job the client exists to do.
2. Transport Options
| Transport | Use it for |
|---|---|
| stdio | Local development, one machine |
| Streamable HTTP | Across machines; the server becomes a web service |
| In-memory | Automated tests only |
| Custom | Possible, rarely needed |
| Deprecated; replaced by Streamable HTTP |
3. Switching to Streamable HTTP
The key point: your tools, resources and prompts don’t change at all. Only the way the server starts changes.
from mcp.server import MCPServer
mcp = MCPServer("cafe")
# ... every @mcp.tool(), @mcp.resource() and @mcp.prompt() exactly as before ...
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
"streamable-http"uses a hyphen. With an underscore it won’t start.- In v2, host and port go on
run(), not on the constructor. - The server listens at
http://127.0.0.1:8000/mcp, with one endpoint,/mcp, by default. - Uvicorn, a Python ASGI server, handles the HTTP traffic underneath and processes requests asynchronously.
Or start it from the CLI without editing the file:
uv run mcp run cafe_server.py --transport streamable-http
On the host:
| Host | Who can reach it |
|---|---|
127.0.0.1 | Only your machine. Use this while testing |
0.0.0.0 | Anyone on your network |
There’s no authentication yet, so binding to 0.0.0.0 hands your tools to everyone on the Wi-Fi. Only use it when you mean to.
4. How the HTTP Exchange Works
- Every client-to-server message is an HTTP POST to
/mcp. - The server replies either with a single JSON response or by opening an SSE stream when it has several messages to send back.
So SSE didn’t disappear. The old transport was retired, and SSE survives as one of the ways Streamable HTTP responds.
5. Testing with the MCP Inspector
Choose Streamable HTTP in the Inspector, enter the server URL, and connect. The Inspector discovers the server’s tools, resources and prompts, and lets you call them by hand.
Public servers we tried:
| Server | Result |
|---|---|
| Microsoft Learn | Worked straight away, with no authentication |
| Our calculator and cafe servers | Worked over HTTP |
| GitHub, Swiggy Dineout | Blocked by authentication |
That last row is the important one. Authentication is the main barrier to using public MCP servers, and it’s the next thing we have to solve.
Also worth knowing: Firecrawl offers an MCP server for scraping and crawling web pages.
6. A First Look at the Client
v2 ships a first-class Client in the same package. With your server running over HTTP:
import asyncio
from mcp import Client
async def main():
async with Client("http://127.0.0.1:8000/mcp") as client:
result = await client.call_tool("place_order", {"table": 4, "items": ["chai"]})
print(result.structured_content)
asyncio.run(main())
A URL means Streamable HTTP. This is the piece Claude Desktop and the Inspector have been playing for us, and it’s where we’re headed.
7. Into Production
Once a server runs over Streamable HTTP, it’s a web application. You can deploy it inside the organisation’s network or to the cloud behind a public address. From there, DevOps treats it like any other web service: URLs, configuration, scaling and monitoring.
8. What’s Next
- Security: HTTPS, plus authentication and authorisation, so only the right callers reach the right tools.
- Wiring into real systems: MCP servers that sit in front of existing organisational APIs.
- Writing an MCP client properly: starting from the
Clientabove, so the client’s job stops being a black box.
✅ Action Items
- Migrate your cafe server to
MCPServer: change the import and the class name, and leave the rest alone. - Switch it to Streamable HTTP, bound to
127.0.0.1. - Connect from the MCP Inspector using the
/mcpURL, and callplace_order. - Call it from the
Clientexample in section 6. - Commit the cafe server code to the repository.
⏭️ Next up: securing MCP servers with authentication.
