stdio transport
MCP supports two transports. HTTP is for remote servers. stdio is for servers that run as a local subprocess under the client. Claude Desktop and Cursor both spawn your server as a subprocess and talk to it through standard input and output. That makes stdio the right default for local tooling.
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
server: Server = Server("mcp-pr-review-server")
async def main() -> None:
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options(),
)
if __name__ == "__main__":
asyncio.run(main())stdio_server is an async context manager. It hands you a read stream for requests from stdin and a write stream for responses to stdout. Anything you print to stdout must go through write_stream, otherwise you corrupt the protocol frame.
import json
from typing import Any
from mcp.types import TextContent
from service import fetch_pr_diff
def _ok(payload: Any) -> list[TextContent]:
return [TextContent(type="text", text=json.dumps(payload, indent=2, default=str))]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
if name == "fetch_pr_diff":
pr = await fetch_pr_diff(
arguments["owner"], arguments["repo"], int(arguments["pr_number"])
)
return _ok(pr.model_dump())
return [TextContent(type="text", text=f"Unknown tool: {name}")]The call_tool handler dispatches by name and always returns a list of content blocks. TextContent wraps a JSON payload so the client receives structured data it can parse.
HTTP makes sense when the server runs somewhere else, like a shared team server or a cloud deployment. stdio is the right pick whenever the server should run with the same file system access, environment variables, and credentials as the client. PR review is a local job, so stdio wins.
Quiz: Quiz
Loading practice…