Understanding MCP Client-Server Communication via Stdio

Mayank YadavMayank Yadav
3 min read

You might have seen tools like Cursor, Claude Code, or even VSCode’s Copilot Chat, where the AI assistant feels super integrated. But have you ever wondered how the underlying Model Context Protocol (MCP) client talks to the server? Especially over good old stdio? This post is all about that—mainly to teach you how pipe(), dup2(), fork(), and friends work together to make this magic happen.

What is MCP?

MCP (Model Context Protocol) is an open, client–server protocol developed by Anthropic that standardizes how tools, data sources, and applications provide context to large language models. Think of it like a USB‑C port for AI: MCP enables seamless, secure, and flexible connections between models and diverse resources—such as files, databases, APIs, and services—so developers can build modular, context‑aware agents and workflows without having to build custom integrations for each system.

MCP is transport-agnostic, meaning the client and server can talk over different channels, including:

  • HTTP SSE (Server-Sent Events)

  • HTTP (Streamable)

  • stdio (standard input/output)

No matter the transport, MCP uses JSON-RPC format to wrap the requests and responses.

In this blog, we're focusing only on the stdio transport.

Why stdio?

In the stdio transport mode, the client spawns the model server as a subprocess and communicates with it via standard input/output (stdin/stdout). That means the server writes responses to stdout, and reads requests from stdin.

This is fast, simple, and doesn’t even need a network.


Let's Code: Real Example in C

Below is a minimal C program that acts like an MCP client spawning a server process. It sets up pipes, uses dup2(), and executes another program (we’ll simulate the MCP server using cat, which just echoes input back).

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int stdin_pipe[2];   // Parent writes, child reads
    int stdout_pipe[2];  // Child writes, parent reads

    if (pipe(stdin_pipe) == -1 || pipe(stdout_pipe) == -1) {
        perror("pipe failed");
        exit(1);
    }

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        // Child process

        // Replace stdin with read end of stdin_pipe
        dup2(stdin_pipe[0], STDIN_FILENO);
        // Replace stdout with write end of stdout_pipe
        dup2(stdout_pipe[1], STDOUT_FILENO);

        // Close unused pipe ends
        close(stdin_pipe[0]);
        close(stdin_pipe[1]);
        close(stdout_pipe[0]);
        close(stdout_pipe[1]);

        // Simulate MCP server with `cat`
        char *args[] = { "/bin/cat", NULL };
        execvp(args[0], args);
        perror("exec failed");
        exit(1);
    } else {
        // Parent process

        // Close unused pipe ends
        close(stdin_pipe[0]);
        close(stdout_pipe[1]);

        // Send JSON-RPC message to child (simulated MCP server)
        char *message = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getCapabilities\",\"params\":{}}\n";
        write(stdin_pipe[1], message, strlen(message));

        // Read response from child
        char buffer[1024];
        ssize_t bytes_read = read(stdout_pipe[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("Received from server: %s", buffer);
        }

        // Cleanup
        close(stdin_pipe[1]);
        close(stdout_pipe[0]);
    }

    return 0;
}

Output

When you run this, you’ll get:

Received from server: {"jsonrpc":"2.0","id":1,"method":"getCapabilities","params":{}}

Exactly what we sent, echoed back by cat. But this is exactly how real MCP stdio transport works—just with an actual model server responding instead of cat.


How It Works (Recap)

  • We create two pipes: one for sending data to the child’s stdin, and one to receive data from the child’s stdout.

  • dup2() is used to hook the child’s stdin and stdout to the pipes.

  • execvp() replaces the child with the actual MCP server binary.

  • The parent writes a JSON-RPC message and reads back the server’s response.


Real Use Case: Cursor, Claude Code, etc.

Tools like Cursor or Claude Code do the exact same thing under the hood when using MCP over stdio:

  • They spawn the every MCP server as a subprocess

  • Hook its input/output to their main app using pipe() + dup2()

  • Send structured JSON-RPC messages to drive the conversation

This lets them tightly integrate MCP Server without running local HTTP servers or making network calls.

Understanding this helps you demystify not just MCP client-server integrations, but also core Unix process and IPC mechanics.

0
Subscribe to my newsletter

Read articles from Mayank Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mayank Yadav
Mayank Yadav