Who this is for and what you will be able to do
If you want a local MCP server that runs on your own machine and is called by Claude or another MCP client, this guide is for you. By the end you will have a Python server with tools that communicates over stdio, a way to test it with MCP Inspector, a Claude Desktop configuration that loads it, and a checklist for the most common failures.[1]
What stdio is and why it is the default for local servers
The stdio transport lets an MCP server communicate with clients through standard input and output streams. It is the most commonly used transport in the current MCP specification, and it is recommended for local MCP servers.[1]
The model is simple. The client launches the server as a subprocess. The server reads JSON-RPC messages from stdin and writes responses to stdout. Each message is a single JSON-RPC request, notification or response, delimited by newlines, and it must not contain embedded newlines.[1]
Compared with the older SSE approach, a stdio server needs no web server setup and no HTTP endpoints. It is simpler to implement and to debug. The standalone SSE transport was deprecated as of MCP Specification 2025-06-18 and replaced by Streamable HTTP, which is the choice for remote servers.[1][3]
Three rules you must follow
Because stdio depends on clean streams, three rules matter:[1]
- The server must not write anything to
stdoutthat is not a valid MCP message. - The server may write UTF-8 strings to
stderrfor logging. - The client must not write anything to the server's
stdinthat is not a valid MCP message.
Many first bugs come from a stray print that writes to stdout. The client then fails to parse the output. Send logs to stderr instead.[1]
Prerequisites
- Python 3.8 or later.
- The MCP Python SDK, installed with
pip install mcp. - A basic understanding of async programming, enough to read
async withandawait.[1]
The exercise builds a Python server named example-stdio-server. It starts with one greeting tool, adds addition and multiplication tools, and finishes with a tool that returns server information.[1]
Step 1: a minimal Python server
The following example shows the basic structure of a stdio server: import the classes, create the server, define a tool with a decorator, and run it through stdio_server.[1]
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Create server instance
server = Server("example-server")
@server.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
async def main():
async with stdio_server(server) as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())The key piece is the stdio_server context manager. It wraps stdin and stdout as read and write streams and passes them to server.run. The server itself does not deal with networking or ports.[1]
Tools are defined with the @server.tool() decorator. The type annotations on the function's parameters and return value become the tool's input and output description, and the docstring above the function states what the tool does.[1]
Step 2: add more tools
Add a multiplication tool and a server-information tool next to the greeting tool:[1]
@server.tool()
def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers"""
return a + b
@server.tool()
def calculate_product(a: int, b: int) -> int:
"""Calculate the product of two numbers"""
return a * b
@server.tool()
def get_server_info() -> dict:
"""Get information about this MCP server"""
return {
"server_name": "example-stdio-server",
"version": "1.0.0",
"transport": "stdio",
"capabilities": ["tools"]
}The dictionary returned by get_server_info contains the server name example-stdio-server, the version 1.0.0, the transport stdio and the capability tools. It is a quick way to confirm that the server you connected to is the one you wrote.[1]
Run the server
Save the code as server.py and start it from the command line:[1]
python server.pyThe server then waits for input on stdin. That is expected: it is waiting for a client to send messages, so you do not type JSON-RPC by hand. Let the Inspector or Claude send them.[1]
Test with MCP Inspector
MCP Inspector is a debugging and testing tool. It lets you view server capabilities, test tools with different parameters, monitor JSON-RPC messages and debug connection issues.[1][2]
To start the Inspector against your Python server, run:[1][2]
npx @modelcontextprotocol/inspector python server.pyOnce the Inspector is open, test in this order: check the server capabilities, call get_greeting with a few different names, call calculate_sum with several pairs of numbers, and finally call get_server_info to check the metadata. The Inspector also shows the JSON-RPC messages exchanged between client and server, which helps when a message looks wrong.[1]
Connect the server to Claude Desktop
The Claude Desktop configuration file is %APPDATA%\Claude\claude_desktop_config.json on Windows and ~/Library/Application Support/Claude/claude_desktop_config.json on Mac. Register the server under mcpServers, using the command python and the path to the server file as the argument:[1]
{
"mcpServers": {
"example-stdio-server": {
"command": "python",
"args": ["path/to/your/server.py"]
}
}
}Replace path/to/your/server.py with the real path to server.py on your computer. After saving, restart Claude so the new configuration loads.[1]
Then try a few prompts, such as "Can you greet me using the greeting tool?", "Calculate the sum of 15 and 27" or "What's the server info?". If Claude calls your tools and returns results, the whole chain works.[1]
Debugging tips
- Use
stderrfor logging and never write tostdout. - Make sure every JSON-RPC message is newline-delimited.
- Test simple tools first, and add complex functionality once they work.
- Use the Inspector to verify message formats.[1]
To step through the server in VS Code, add a Python debug configuration to .vscode/launch.json, with "type": "python", "program": "server.py" and "console": "integratedTerminal". Set breakpoints, start the debugger, and send requests with the Inspector.[1]
Common problems
If the server will not start, confirm that the dependencies are installed (pip install mcp), check the Python syntax and indentation, and read the console error messages.[1]
If tools do not appear, check that the @server.tool() decorators are present and that the tool functions are defined before main().[1]
For connection issues, confirm that the server uses stdio correctly, that no other process interferes, and that the Inspector command syntax is right.[1]
The TypeScript version
The TypeScript example uses the lower-level Server class. It sets up ListToolsRequestSchema and CallToolRequestSchema handlers, one to list tools and one to call them. Its imports and startup code look like this:[1]
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "example-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);When a tool is called, the handler returns an array of content items with type set to text. An unknown tool name raises an error.[1]
Next steps
The next step is Streamable HTTP, the other supported transport, which suits remote servers. For security and deployment, MCP's security best practices and deployment guidance are the natural follow-ups.[1]
Adapted from lesson 3 of Microsoft's open-source course *MCP for Beginners*[1], released under the MIT License.
Sources
- 1MCP Server with stdio TransportMicrosoft mcp-for-beginners (MIT)
- 2MCP InspectorModel Context Protocol
- 3MCP Specification 2026-07-28Model Context Protocol
Cite this page: Writing an MCP server with the stdio transport. TellWorld, https://www.tellworld.net/practice/practice-mcp-stdio-server. Updated 2026-11-14