Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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 weather server over streamable HTTP · from the Complete Agentic AI Course In 10 Hours · 284:03 to 288:32

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

python
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.

python
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())
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
shop.py
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.

ExampleFrom the video, run on MCP 2.2
python weather.py > weather.log 2>&1 &
sleep 3
python weather_client.py
kill $!

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.

Exampleshop.py, the end now
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.

Exampleclient.py
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://.

Example
python shop.py &
sleep 3
python client.py
kill $!
  • 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

stdioStreamable HTTP
ReachOne computerOther machines
Started byThe host, as a subprocessYou, on a port
The client needsStdioServerParametersA 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.
Watch out
The server accepts localhost by default. Behind a real host name every request is refused until that name is allowed through transport_security.
Try it yourself
  • Change the port to 8201 in both files.
  • Remove log_level="WARNING" and read what the server logs.
  • Run client.py with the server stopped and read the error.

Slow is fine. Stopping is the only problem.