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 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:
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())- written in MCPServer quickstart
View the code here
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")
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()
python math_client.py{'result': 8}
{'result': 96}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:
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 ToolAnnotationsif __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
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.
python client.py{'id': 'A17', 'item': 'blue mug', 'status': 'shipped', 'total': 12.5}- 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 server | In your process | As a child process |
| Good for | Tests and demos | A local app launching your server |
| Secrets | Shared | Passed 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.
logging, which goes to standard error.Related
- Previous: Elicitation
- Next: Streamable HTTP transport
- Add
print("hello")insidelookup_orderand runclient.pyagain. - Pass
env={"SHOP_NAME": "Mugs and Lamps"}and read it withos.environin a tool. - Point
argsat a file that does not exist and read the error.
You understood something today that you didn't yesterday.