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 →

stdio transport

stdio is the transport where the host runs your server as a child process and talks to it over standard input and output.

Last updated: 29 Sep, 2026 · MCP 2.2

The stdio transport · from the Complete Agentic AI Course In 10 Hours · 281:12 to 284:03

The video's math server ends with mcp.run(transport="stdio"), and the video explains what that means: the server uses standard input and output to receive tool calls and send back the results. It runs in a command prompt, the input arrives on the command line, hits a function, and the client reads the response from the same place. That suits testing a server and its client on your own machine.

Calling the math server over stdio

This client starts mathserver.py from MCPServer quickstart as a child process and calls the two tools the video's agent needs for (3 + 5) x 12:

python
import asyncio
import sys

from mcp import Client, StdioServerParameters

server = StdioServerParameters(command=sys.executable, args=["mathserver.py"])


async def main():
    async with Client(server) as client:
        added = await client.call_tool("add", {"a": 3, "b": 5})
        print(added.structured_content)
        product = await client.call_tool("multiple", {"a": 8, "b": 12})
        print(product.structured_content)


asyncio.run(main())
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
mathserver.py
from mcp.server import MCPServer

mcp=MCPServer("Math")

@mcp.tool()
def add(a:int,b:int)->int:
    """_summary_
    Add to numbers
    """
    return a+b

@mcp.tool()
def multiple(a:int,b:int)-> int:
    """Multiply two numbers"""
    return a*b

#The transport="stdio" argument tells the server to:

#Use standard input/output (stdin and stdout) to receive and respond to tool function calls.

if __name__=="__main__":
    mcp.run(transport="stdio")
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")

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()
ExampleFrom the video, run on MCP 2.2
python math_client.py

StdioServerParameters names the command to launch; sys.executable is the Python running the client, so the server starts in the same environment. The client started the server, sent two calls over its standard input, read 8 and 96 from its standard output, and shut it down. Run python mathserver.py on its own and nothing seems to happen: it is waiting on standard input for a client.

The shop server gets the same ending.

From here shop.py runs as a program of its own, so check its top first. The lessons from tool arguments through prompts each added an import, and together they read:

python
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
Exampleshop.py, added at the end
if __name__ == "__main__":
    mcp.run()

mcp.run() serves the server, over stdio when given no transport, and blocks until the input closes. It sits under if __name__ == "__main__": so that importing shop.py, as the earlier lessons did, does not start it.

Run python shop.py by itself and nothing seems to happen: it is waiting for a client to write the first message. Press Ctrl+C to stop it.

A client that starts the server

Exampleclient.py
import asyncio
import sys

from mcp import Client, StdioServerParameters

server = StdioServerParameters(command=sys.executable, args=["shop.py"])


async def main():
    async with Client(server) as client:
        result = await client.call_tool("lookup_order", {"order_id": "A17"})
        print(result.structured_content)


asyncio.run(main())

StdioServerParameters describes the command to launch. sys.executable is the Python running the client, so the server starts in the same environment. Entering async with starts the process; leaving it shuts the process down.

Example
python client.py
  • The same lookup as the structured-output lesson, now running across two processes instead of one.
  • The client started the server, talked to it over standard input and output, and shut it down on exit.

Two things stdio changes

Standard output is the connection. Anything else your server prints to stdout would corrupt the messages. The SDK redirects stray output while serving, but write logs with Python's logging module, which writes to standard error.

The server does not inherit your environment. The SDK starts it with a short allow-list of variables, such as PATH and HOME, so secrets in your shell do not leak into a process you might not have written. A server that needs an API key gets it through env=: StdioServerParameters(command=..., args=[...], env={"SHOP_API_KEY": "..."}).

The in-memory client vs stdio

Client(mcp)stdio
Runs the serverIn your processAs a child process
Good forTests and demosA local app launching your server
SecretsSharedPassed with env=

When stdio is the transport

  • A server that runs on the same computer as the app using it.
  • A desktop host like Claude Code or Cursor launching your server.
  • Passing a secret to the server through env= rather than the shell.
Watch out
Anything a stdio server prints to standard output corrupts the messages. Write logs through Python's logging, which goes to standard error.
Try it yourself
  • Add print("hello") inside lookup_order and run client.py again.
  • Pass env={"SHOP_NAME": "Mugs and Lamps"} and read it with os.environ in a tool.
  • Point args at a file that does not exist and read the error.
PreviousElicitation

You understood something today that you didn't yesterday.