Introduction

The stdio transport is the simplest and most common way to connect an MCP client to a server running on the same machine. The server runs as a child process, and all communication happens over standard input and standard output. This lesson covers how stdio works, when to use it, and the details of message framing.

Key Concepts

  • stdio transport: A communication mechanism where the MCP client spawns the server as a subprocess and exchanges JSON-RPC messages via stdin and stdout.
  • Subprocess model: The client starts the server process, owns its lifecycle, and terminates it when the connection ends.
  • Newline-delimited JSON: Each JSON-RPC message is a single line of JSON text terminated by a newline character. No additional framing is needed.
  • stderr for logging: The server must never write protocol messages to stderr. It is reserved exclusively for human-readable debug logs.

Real World Context

When you configure an MCP server in Claude Desktop or VS Code, the application typically launches the server as a local subprocess. For example, a file system server might be started with npx @modelcontextprotocol/server-filesystem /path/to/directory. The IDE spawns this process, sends JSON-RPC requests to its stdin, and reads responses from its stdout. This is the default transport for all local MCP integrations.

Deep Dive

How stdio Communication Works

The client starts the MCP server as a child process. Once the process is running, the client writes JSON-RPC messages to the server's stdin, and the server writes responses back to its stdout. Each message is a complete JSON object on a single line.

Here is what the message flow looks like at the process level:

text
Client (parent process)          Server (child process)
     |                                  |
     |--- stdin: {"jsonrpc":"2.0",...} -->|
     |                                  |
     |<-- stdout: {"jsonrpc":"2.0",...} --|
     |                                  |
     |--- stdin: {"jsonrpc":"2.0",...} -->|
     |                                  |
     |<-- stdout: {"jsonrpc":"2.0",...} --|

This diagram shows the bidirectional flow. The client writes to the server's stdin pipe, and the server writes back to stdout.

Starting a Server as a Subprocess

In TypeScript, you connect a client to a stdio server using the SDK's StdioClientTransport:

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
});

const client = new Client(
  { name: "my-app", version: "1.0.0" },
  { capabilities: {} }
);

await client.connect(transport);

The StdioClientTransport handles spawning the process, piping stdin/stdout, and parsing messages. You do not need to manage the child process manually.

Message Framing

Each message is a single JSON object followed by a newline (\n). There are no length prefixes or other framing mechanisms. The simplicity of this format makes it easy to debug by reading the raw stream.

A single request looks like this on the wire:

json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0.0"}}}

The server responds with a single JSON line on stdout. Each message must be self-contained on one line.

stderr Is for Logging Only

The server process must never write protocol messages to stderr. The stderr stream is reserved for human-readable debug output that tools like Claude Desktop or the MCP Inspector can display in a debug console.

typescript
// Server-side: use stderr for debug logging
console.error("[DEBUG] Processing tool call: get-weather");
console.error("[DEBUG] Fetching data from API...");

// NEVER write JSON-RPC messages to stderr
// Always use stdout for protocol communication

This separation ensures that debug output never corrupts the protocol stream.

When to Use stdio

The stdio transport is the right choice when:

  • The server runs on the same machine as the client
  • You are building CLI tools or desktop applications
  • You want the simplest possible setup with no network configuration
  • You need process-level isolation (the server is a separate process)

Common Pitfalls

  1. Writing debug output to stdout — Any non-JSON-RPC text on stdout will corrupt the protocol stream and cause parsing errors. Always use stderr for logging and diagnostic output.
  2. Forgetting newline delimiters — Each JSON message must end with a newline character. Omitting it will cause the client to wait indefinitely for the end of the message.
  3. Buffering issues — Some runtimes buffer stdout by default. Ensure your server flushes stdout after each message, or the client will never receive responses.

Best Practices

  1. Use the SDK transports — The StdioClientTransport and StdioServerTransport classes handle process management, message framing, and buffering correctly. Do not implement the protocol from scratch.
  2. Log to stderr with structured prefixes — Use prefixes like [DEBUG], [INFO], or [ERROR] on stderr so debugging tools can filter log levels.
  3. Handle process termination gracefully — When the client disconnects, the server should detect the closed stdin pipe and shut down cleanly, releasing any resources.

Summary

  • The stdio transport runs the MCP server as a subprocess, communicating via stdin and stdout.
  • Messages are newline-delimited JSON, one complete JSON-RPC object per line.
  • stderr is reserved for debug logging and must never carry protocol messages.
  • Use stdio for local processes, CLI tools, and desktop applications where the client and server share a machine.
  • The SDK provides StdioClientTransport and StdioServerTransport to handle all low-level details.

Code Examples

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
});

const client = new Client(
  { name: "my-app", version: "1.0.0" },
  { capabilities: {} }
);

await client.connect(transport);

// List available tools from the server
const tools = await client.listTools();
console.log("Available tools:", tools);
✓ Completed