Complete STDIO Server
Build a fully functional MCP server using STDIO transport.
Building on Previous Lessons
In this step, we'll create stdio_transport.py — a reusable
STDIO transport. Then we'll create an example that uses all our previous modules.
This lesson imports from jsonrpc.py, mcp_core.py,
tools.py, and resources.py. Make sure all previous
modules are in your project directory.
Your Project Structure
your_project/
jsonrpc.py # Lesson 3
mcp_core.py # Lesson 4
tools.py # Lesson 5
resources.py # Lesson 6
stdio_transport.py # NEW - We'll create this
stdio_example.py # NEW - Example using transport
In this lesson:
- Filename in header = Write this code to that file
- "Add after Section X" = Append to your existing file
- bash blocks = Commands to run in your terminal
- "Expected Output" = What you should see (don't write this)
Section 1: Create the STDIO Transport
Create: stdio_transport.py
Create a new file with the STDIO transport:
"""
stdio_transport.py - STDIO transport for MCP servers.
This module builds on mcp_core.py from Lesson 4.
"""
import sys
import json
import logging
# Import from Lesson 4
from mcp_core import handle_request
# =============================================================================
# Section 1: Logging Setup (stderr only!)
# =============================================================================
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[logging.StreamHandler(sys.stderr)] # CRITICAL: stderr only!
)
logger = logging.getLogger("mcp-stdio")
Try It
python -c "from stdio_transport import logger; logger.info('Test')"
You should see a log message to stderr.
Section 2: STDIO Transport Class
Edit: stdio_transport.py
Add after Section 1:
# =============================================================================
# Section 2: STDIO Transport
# =============================================================================
def write_response(response: dict) -> None:
"""Write a JSON-RPC response to stdout."""
# Use compact JSON - no pretty printing for STDIO!
json_str = json.dumps(response, separators=(',', ':'))
sys.stdout.write(json_str + "\n")
sys.stdout.flush() # CRITICAL: Always flush!
logger.debug(f"Sent response for id={response.get('id')}")
def run_stdio_server() -> None:
"""
Run MCP server using STDIO transport.
Reads JSON-RPC messages from stdin (one per line).
Writes responses to stdout.
Logs to stderr.
"""
logger.info("STDIO MCP Server starting...")
while True:
try:
# Read one line from stdin
line = sys.stdin.readline()
# EOF - client disconnected
if not line:
logger.info("EOF received, shutting down")
break
# Skip empty lines
line = line.strip()
if not line:
continue
# Parse JSON
try:
request = json.loads(line)
except json.JSONDecodeError as e:
logger.error(f"JSON parse error: {e}")
continue
# Use shared handler from mcp_core
response = handle_request(request)
# Send response (None for notifications)
if response is not None:
write_response(response)
except KeyboardInterrupt:
logger.info("Interrupted, shutting down")
break
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
logger.info("Server shutdown complete")
Add a test block at the end:
# =============================================================================
# Run if executed directly
# =============================================================================
if __name__ == "__main__":
run_stdio_server()
Section 3: Example with Tools
Create: stdio_example.py
Create a server that uses STDIO transport with tools:
"""
stdio_example.py - STDIO server using tools.py
Run: python stdio_example.py
Test: echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python stdio_example.py
"""
from datetime import datetime
# Import from our modules
from mcp_core import router
from tools import ToolRegistry, register_tool_handlers, text_result
from stdio_transport import run_stdio_server
# =============================================================================
# Register tools
# =============================================================================
tool_registry = ToolRegistry()
@tool_registry.register(
name="echo",
description="Echo back a message",
input_schema={
"type": "object",
"properties": {"message": {"type": "string", "description": "Message to echo"}},
"required": ["message"]
}
)
def echo(args: dict) -> dict:
return text_result(args.get("message", ""))
@tool_registry.register(
name="get_time",
description="Get current server time",
input_schema={"type": "object", "properties": {}, "required": []}
)
def get_time(args: dict) -> dict:
return text_result(f"Current time: {datetime.now().isoformat()}")
@tool_registry.register(
name="add_numbers",
description="Add two numbers",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
)
def add_numbers(args: dict) -> dict:
a = args.get("a", 0)
b = args.get("b", 0)
return text_result(f"{a} + {b} = {a + b}")
# Register with the MCP router
register_tool_handlers(router, tool_registry)
# =============================================================================
# Run the server
# =============================================================================
if __name__ == "__main__":
run_stdio_server()
Section 4: Test Your Server
Test 1: Initialize Handshake
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}' | python stdio_example.py
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"mcp-server","version":"1.0.0"}}}
Test 2: List Tools
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
) | python stdio_example.py 2>/dev/null
Expected: Two JSON responses - initialize result and tools list with echo, get_time, add_numbers.
Test 3: Call a Tool
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Hello, STDIO!"}}}'
) | python stdio_example.py 2>/dev/null
Expected: Response with "text": "Hello, STDIO!"
Claude Desktop Configuration
To use this server with Claude Desktop, add it to your configuration file:
{
"mcpServers": {
"stdio-demo": {
"command": "python",
"args": ["/absolute/path/to/stdio_server.py"]
}
}
}
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json
What You've Built
You've created a reusable STDIO transport:
| File | Purpose |
|---|---|
stdio_transport.py |
Reusable STDIO transport using mcp_core.py |
stdio_example.py |
Example server combining STDIO + tools |
The key insight: the same MCP router handles requests regardless of transport!
mcp_core.pyhandles MCP protocol logictools.pyregisters toolsstdio_transport.pyhandles I/O over stdin/stdout
- Never use
print()(it goes to stdout!) - Always call
flush()after writing - Never pretty-print JSON (breaks line-based protocol)
- Always log to stderr only
What's Next?
In the next lesson, we'll explore HTTP/SSE transport, which enables remote MCP servers and web-based clients.