AI & Machine Learning5 min read153 views

Part 2: Async Primitives, Standard I/O, and python environment

A
Admin·

Hey, welcome back. In Part 1, we covered the theory. Now, you’re probably itching to start writing Python files and connecting them to Claude or Cursor.

But here is the truth: if you try to build an MCP server without understanding asynchronous programming, standard input/output streams, or python environment isolation, your code will crash, it will hang silently, and you won't have a clue how to fix it.

In this part, we are going to focus entirely on the fundamentals. We'll cover Python environment setup, asynchronous programming, standard I/O redirection, and how JSON-RPC packets travel across standard streams. If any of these concepts are new to you, pause and click the attached links to read up.

Let's build a foundation you can actually stand on.

1. The Setup: Python, Pip, and Virtual Environments

Before writing a single line of code, we need a clean, predictable workspace.

1.1. Python and Pip

Python is our runtime interpreter. pip is Python's default package manager. It connects to the Python Package Index (PyPI) to download libraries like mcp or openai.

1.2. Why Virtual Environments venv are essentials

If you install libraries globally on your machine pip install mcp, they end up in your system's global Python folder. The moment you work on a second project that requires a different version of the same library, your global environment breaks.

A virtual environment venv is a self-contained directory containing its own Python interpreter and its own isolated set of installed packages.

To create and activate a virtual environment, open your terminal and run:

bash

# Create a virtual environment named 'venv'

python -m venv venv

# Activate it (Windows PowerShell)

.\venv\Scripts\Activate.ps1

# Activate it (Mac/Linux)

source venv/bin/activate

Once activated, any package you install using pip stays locked inside this project directory.


2. You need to understand Async: asyncio and async with

MCP is built around real-time concurrency. The server has to listen for incoming tools requests, read files, execute shell commands, and stream progress notifications to the client—all at the same time. If you write synchronous (blocking) code, your server will freeze the entire host client while waiting for a single slow tool to complete.

### 2.1. Concurrency with asyncio

In Python, we achieve concurrency using async def to declare coroutines and await to yield control back to the event loop.

import asyncio

async def fetch_data():

    print("Start fetching...")

    await asyncio.sleep(2)  # Non-blocking pause

    print("Done fetching!")

    return {"data": 123}

If you call fetch_data(), it doesn't run immediately. It returns a coroutine object. You must await it inside an active event loop.

2.2. Asynchronous Context Managers async with

When dealing with processes, files, or network sockets, you must ensure they are properly initialized and closed, even if your code crashes. Context managers handle this setup and teardown lifecycle automatically.

For asynchronous resources, we use async with:

# The context manager ensures the connection closes safely when the block exits

async with httpx.AsyncClient() as client:

    response = await client.get("https://api.github.com")

    print(response.status_code)

3. Standard I/O (stdio) and the JSON-RPC protocol

The most common way a host (like Claude Desktop) connects to a local MCP server is over the stdio transport. Under the hood, this is just basic operating system process communication.

3.1. Standard Streams: Stdin, Stdout, and Stderr

Every process you launch gets three standard channels:

1. sys.stdin (Standard Input): Receives data sent to the process.

2. sys.stdout (Standard Output): Writes normal output data.

3. sys.stderr (Standard Error): Writes logs and error messages.

When Cursor or Claude launches your MCP server, it spawns it as a subprocess. The client writes JSON-RPC request packets directly to the server's stdin, and reads the server's responses directly from the server's stdout.

- Learn more: Python sys Stream Course

3.2. How JSON-RPC Packets Travel

Here is a raw look at what travels across those streams. When the client wants to list tools, it writes this string to your server's stdin:

{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

Your server reads this from stdin, runs the internal routing handler, and writes the response back to stdout:

{

  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "read_file",
        "description": "Read the contents of a file",
        "inputSchema": {
          "type": "object",
          "properties": {"path": {"type": "string"}}
        }
      }
    ]
}

3.3. The Stderr Rule (Be careful here)

Because stdout is the dedicated channel for sending JSON-RPC responses, you must never print normal text to stdout.

If your code contains print("Server started!") or logging.info("Calling tool..."), those plain-text strings are written directly to stdout. The client tries to parse "Server started!" as a valid JSON-RPC message, fails, and immediately terminates the connection.

WARNING
**Rule of Standard Streams: In an MCP stdio server, stdout belongs strictly to the JSON-RPC protocol. All debugging logs, error messages, and startup notices MUST be redirected to sys.stderr.


Wrapping Up Part 2

We have laid the foundation:

1. We set up a virtual environment venv) to isolate our libraries.

2. We learned to think asynchronously with asyncio and manage resource lifecycles with async with.

3. We mapped out how standard streams work, how JSON-RPC packets look under the hood, and why debugging output must go to stderr.

You now have the technical vocabulary and understanding of the Python async primitives. In Part 3, we will start writing the actual server code, defining custom tools, managing read-only resources, and routing prompt templates. See you in the next post! Please consider subscription to my newsletter and join 5k+ subscribers who are reading and building with me.

Tags#python#asyncio#model-context-protocol#mcp-server

Reading progress

0% read

Auto-completes after you reach the end and linger for a moment.

You made it to the end

Get more like this in your inbox

Every week I write about machine learning, engineering patterns, and things I'm building. Practical, no fluff — straight to your inbox.

Subscribe to the newsletter

Get thoughtful updates on AI, engineering, and product work.

We respect your privacy using double opt-in. Unsubscribe at any time.