cd /news/ai-agents/building-production-model-context-pr… · home › topics › ai-agents › article
[ARTICLE · art-162889] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Building Production Model Context Protocol (MCP) Servers in Pure Python: Stdio, SSE & Pydantic V2

A developer published an architectural guide for building production-grade Model Context Protocol (MCP) servers in pure Python using asyncio, json, and Pydantic V2. The guide covers JSON-RPC 2.0 wire format, Pydantic data contracts for type-safe schema validation, automatic tool-schema extraction from Python type hints and docstrings, and stdio-based transport for clients like Claude Desktop and Cursor.

read3 min views1 publishedNov 3, 2026

In the rapidly evolving landscape of agentic AI, connecting Large Language Models to real-world data and execution environments has historically been plagued by fragmentation. Every developer was writing bespoke function-calling wrappers, brittle regex parsers, and custom HTTP endpoints.

The Model Context Protocol (MCP), open-sourced by Anthropic and rapidly adopted by Claude Desktop, Cursor, and Antigravity, has emerged as the universal standard—the USB-C of Artificial Intelligence.

In this architectural guide, we build a production-grade, asynchronous MCP server from scratch in pure Python using asyncio, json, and pydantic.

At the wire level, MCP communicates using pure JSON-RPC 2.0. Every message transmitted between client and server is a newline-delimited JSON payload.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "execute_sqlite_query",
    "arguments": {
      "db_path": "production.db",
      "query": "SELECT id, email, status FROM customers WHERE active = 1;"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"id\": 101, \"email\": \"alex@example.com\", \"status\": \"active\"}]"
      }
    ],
    "isError": false
  }
}

To maintain 100% type safety and automatic schema validation, we define our core protocol data contracts:

from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field

class JSONRPCRequest(BaseModel):
    jsonrpc: str = "2.0"
    id: Optional[Union[str, int]] = None
    method: str
    params: Optional[Dict[str, Any]] = None

class JSONRPCResponse(BaseModel):
    jsonrpc: str = "2.0"
    id: Optional[Union[str, int]] = None
    result: Optional[Any] = None
    error: Optional[Dict[str, Any]] = None

class ToolInputSchema(BaseModel):
    type: str = "object"
    properties: Dict[str, Any] = Field(default_factory=dict)
    required: List[str] = Field(default_factory=list)

class ToolDefinition(BaseModel):
    name: str
    description: str
    inputSchema: ToolInputSchema

class CallToolResult(BaseModel):
    content: List[Dict[str, str]] = Field(default_factory=list)
    isError: bool = False

Rather than maintaining duplicate JSON schemas by hand, our server inspects Python functions, extracts docstrings, and derives parameters directly from native type hints:

import inspect
from typing import Callable, get_type_hints

def extract_schema(func: Callable) -> ToolDefinition:
    sig = inspect.signature(func)
    type_hints = get_type_hints(func)

    properties = {}
    required = []

    type_map = {
        int: "integer",
        float: "number",
        str: "string",
        bool: "boolean",
        list: "array",
        dict: "object"
    }

    for param_name, param in sig.parameters.items():
        if param_name in ("self", "cls"):
            continue
        py_type = type_hints.get(param_name, str)
        json_type = type_map.get(py_type, "string")

        properties[param_name] = {"type": json_type, "description": f"Parameter {param_name}"}
        if param.default is inspect.Parameter.empty:
            required.append(param_name)

    return ToolDefinition(
        name=func.__name__,
        description=(func.__doc__ or "").strip(),
        inputSchema=ToolInputSchema(properties=properties, required=required)
    )

For local tools configured in Claude Desktop or Cursor, communication happens over standard I/O streams (sys.stdin and sys.stdout). Stream flushing is critical:

import sys
import json
import asyncio

async def run_stdio_server(server):
    loop = asyncio.get_event_loop()
    reader = asyncio.StreamReader()
    protocol = asyncio.StreamReaderProtocol(reader)
    await loop.connect_read_pipe(lambda: protocol, sys.stdin)

    while True:
        line = await reader.readline()
        if not line:
            break
        try:
            req_json = json.loads(line.decode("utf-8").strip())
            resp = await server.handle_request(req_json)
            if resp is not None:
                sys.stdout.write(json.dumps(resp) + "\n")
                sys.stdout.flush()
        except Exception as e:
            err_resp = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": str(e)}}
            sys.stdout.write(json.dumps(err_resp) + "\n")
            sys.stdout.flush()

Warning: Never use unbuffered print() statements inside an MCP Stdio server. Any rogue stdout logging will break the JSON-RPC stream parser. Always direct internal server logs to sys.stderr.

Once your Python script is ready, add it to your claude_desktop_config.json:

{
  "mcpServers": {
    "my_python_tools": {
      "command": "python",
      "args": ["-m", "mcpmaster_pro.server_cli"]
    }
  }
}

Now, Claude Desktop natively sees your custom tools, knows their exact parameter requirements, and executes them with full local privacy and $0 ongoing cloud API subscription bills.

Want the complete, production-ready implementation?

Our new flagship guide and commercial repository is now live:

🚀 Production Model Context Protocol (MCP) with Python

Includes:

mcpmaster_pro`` LAUNCH20 Or get the complete 4-book + 8-repository bundle:

🏛️ The Sovereign Software Architect Quad-Vault

── more in #ai-agents 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-production-…] indexed:0 read:3min 2026-11-03 · —