Understanding MCP Client-Server Communication via Stdio

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’sstdin
andstdout
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.
Subscribe to my newsletter
Read articles from Mayank Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
