Streamable HTTP transport
Streamable HTTP is the transport that serves your server on a port so other machines and hosted agents can reach it.
Last updated: 29 Sep, 2026 · MCP 2.2
The video's second server stands in for a third-party API call. weather.py creates FastMCP("Weather") with one tool, get_weather, which returns a fixed sentence, It's always raining in California, where a real server would call a weather API. It ends with mcp.run(transport="streamable-http"). Started with python weather.py, it runs as an API service at a URL, on localhost port 8000 by default, with the MCP endpoint at /mcp, while python mathserver.py over stdio shows nothing.
Serving the weather server from the video
from mcp.server import MCPServer
mcp=MCPServer("Weather")
@mcp.tool()
async def get_weather(location:str)->str:
"""Get the weather location."""
return "It's always raining in California"
if __name__=="__main__":
mcp.run(transport="streamable-http", port=8001)
Two changes from the video: MCPServer in place of FastMCP, as in MCPServer quickstart, and port=8001. The video uses the default port, 8000, which many other development servers use too, and two servers cannot share a port.
import asyncio
from mcp import Client
async def main():
async with Client("http://127.0.0.1:8001/mcp") as client:
result = await client.call_tool("get_weather", {"location": "California"})
print(result.content[0].text)
asyncio.run(main())View the code here
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.types import ToolAnnotations
mcp = MCPServer("Shop support", log_level="WARNING")
ORDERS = {
"A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
"B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}
ARTICLES = {
"Where is my order?": "orders",
"Changing an order": "orders",
"How refunds work": "refunds",
"Refunds for damaged items": "refunds",
"Resetting your password": "account",
}
class Order(BaseModel):
id: str
item: str
status: Literal["processing", "shipped", "delivered"]
total: float
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def lookup_order(order_id: str) -> Order:
"""Look up an order by its id."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}. Order ids look like A17.")
return Order(id=order_id, **ORDERS[order_id])
@mcp.tool()
def search_help(
query: Annotated[str, Field(description="Words to look for in the help articles.")],
topic: Literal["orders", "refunds", "account"] | None = None,
limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
"""Search the help centre and return matching article titles."""
found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
return "; ".join(found[:limit]) or "No articles found."
@mcp.tool(
title="Refund an order",
annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False),
)
def refund_order(order_id: str, reason: str) -> str:
"""Refund the full total of an order to the customer's card."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}.")
return f"Refunded {ORDERS[order_id]['total']:.2f} for order {order_id}: {reason}."
@mcp.resource("policy://refunds", mime_type="text/markdown")
def refund_policy() -> str:
"""The shop's refund policy."""
return "# Refunds\n\nFull refund within 30 days of delivery. Damaged items: refund or replacement."
@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order:
"""The full record for one order."""
if order_id not in ORDERS:
raise ResourceNotFoundError(f"No order with id {order_id!r}.")
return Order(id=order_id, **ORDERS[order_id])
@mcp.prompt(title="Reply to a customer")
def reply_to_customer(ticket: str, tone: str = "friendly") -> list[Message]:
"""Draft a reply to a support ticket."""
return [
UserMessage(f"Write a {tone} reply to this support ticket:\n\n{ticket}"),
AssistantMessage("Hello, and thank you for getting in touch."),
]
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=8200)
Start the server in the background, run the client, and stop the server. > weather.log 2>&1 sends everything the server logs to a file, so it does not mix with the client's output.
python weather.py > weather.log 2>&1 &
sleep 3
python weather_client.py
kill $!It's always raining in California
The client reached the tool over HTTP, and the answer is the fixed sentence. The shop server gets the same transport on its own port, 8200.
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=8200)The same server, served on a port. The SDK builds the web application and runs it with uvicorn, and the MCP endpoint is at /mcp. Transport options like port belong to run(), not to MCPServer(...).
One other change, at the top of shop.py: the server line becomes mcp = MCPServer("Shop support", log_level="WARNING"). The default level logs every request, which is useful while debugging and noisy in the output below.
Replace client.py from the stdio lesson with this version, which connects by URL instead of starting the server.
import asyncio
from mcp import Client
async def main():
async with Client("http://127.0.0.1:8200/mcp") as client:
print(client.protocol_version)
result = await client.call_tool("lookup_order", {"order_id": "B42"})
print(result.structured_content)
asyncio.run(main())A URL string is all the client needs. It chooses Streamable HTTP from the http://.
python shop.py &
sleep 3
python client.py
kill $!2026-07-28
{'id': 'B42', 'item': 'desk lamp', 'status': 'processing', 'total': 48.0}- The client reached the server over HTTP, choosing Streamable HTTP from the
http://in the URL. - 2026-07-28 is the protocol version the client and server agreed on when they connected.
Headers and keys
A deployed server needs to know who is calling. The client's HTTP settings, headers included, go on an httpx2.AsyncClient passed through the transport, and a server can read request headers from ctx.headers. For real sign-in, the SDK implements the OAuth flow the MCP specification describes; it is named in the last lesson.
stdio vs Streamable HTTP
| stdio | Streamable HTTP | |
|---|---|---|
| Reach | One computer | Other machines |
| Started by | The host, as a subprocess | You, on a port |
| The client needs | StdioServerParameters | A URL |
When to serve over HTTP
- A server a team or hosted agents reach across the network.
- One server shared by many clients at once.
- A deployment behind a host name, with sign-in on top.
transport_security.Related
- Previous: stdio transport
- Next: MCP with FastAPI
- Change the port to 8201 in both files.
- Remove
log_level="WARNING"and read what the server logs. - Run
client.pywith the server stopped and read the error.
Slow is fine. Stopping is the only problem.