Skip to content
Wiki

Build a minimal MCP server with the Python SDK's MCPServer, @mcp.tool and stdio

DrFritzi · Reviewed · Updated 28 Sept 2026 · Markdown

Answer

Install the mcp package, create MCPServer("Demo") from mcp.server, decorate a typed function with @mcp.tool(), and call mcp.run(), which serves over stdio by default. Test it with uv run mcp dev server.py, which opens the MCP Inspector. This page describes mcp 2.2.0 on Python 3.11, the version we ran. The SDK README says v2 supports MCP specification 2026-07-28, and that mcp>=1.28,<2 keeps you on the older v1 line.

Details

1. Install

uv add "mcp[cli]"      # or: pip install "mcp[cli]"

The cli extra provides the mcp command used for mcp dev. Plain mcp is enough for a server that you run with python. PyPI lists Python 3.10 or later as required.

2. Write the server (server.py)

from mcp.server import MCPServer

mcp = MCPServer("Demo")


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


if __name__ == "__main__":
    mcp.run()

The README and first-steps guide show the class and decorator. MCPServer.run(transport='stdio', ...) also accepts 'sse' and 'streamable-http', so a bare mcp.run() means stdio. The docs run a stdio server with python server.py.

3. Test it

  • Inspector: uv run mcp dev server.py starts the server and the MCP Inspector together.
  • Any client: write JSON-RPC lines to the process's stdin and read stdout.

Verification

We ran the file above in a fresh virtualenv with mcp 2.2.0 and sent initialize, notifications/initialized, tools/list and tools/call as newline-delimited JSON. Real replies, trimmed to the interesting part:

initialize  -> "protocolVersion":"2025-11-25","serverInfo":{"name":"Demo","version":""}
tools/list  -> {"description":"Add two numbers.","inputSchema":{"properties":{"a":{"title":"A","type":"integer"},"b":{"title":"B","type":"integer"}},"required":["a","b"],"type":"object","title":"addArguments"},"name":"add","outputSchema":{"properties":{"result":{"title":"Result","type":"integer"}},"required":["result"],"type":"object","title":"addOutput"}}
tools/call {"a":2,"b":3} -> {"content":[{"text":"5","type":"text"}],"isError":false,"structuredContent":{"result":5}}
tools/call {"a":"x","b":3} -> {"content":[{"text":"Error executing tool add: 1 validation error for addArguments\na\n  Input should be a valid integer, ...","type":"text"}],"isError":true}

Our initialize asked for 2025-11-25 and the server answered with it. We did not test a 2026-07-28 client.

How the schema is built

You write Client sees
Function name add Tool name
Docstring """Add two numbers.""" Tool description
Type hints a: int, b: int inputSchema (both required)
Return type -> int outputSchema and structuredContent, plus a text block

Pitfall: writing to stdout

On stdio, stdout carries the protocol. The SDK docs say that while serving it diverts output that is flushed to stdout, such as a flushed print(), to stderr. They also say buffered output or prints made before serving starts can still reach the wire, and that logging, which writes each record to stderr, is the right tool. Log with logging or print(..., file=sys.stderr), and do not rely on the diversion.

See also

Sources