mcp-servers-config.json server launch format
Stdio MCP servers are declared in a JSON config with an `mcpServers` object keyed by server name, each having `command`, `args` and `env`. Example: {"mcpServers": {"brave-search": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": {"BRAVE_API_KEY": "..."}}}}.
Choosing the interpreter command for a stdio server script
Clients that launch a server by script path pick the command from the file extension: `.py` -> `python` on Windows (`process.platform === "win32"`) and `python3` elsewhere, `.js` -> `node` (or `process.execPath`), `.jar` -> `java -jar`, `.rb` -> `ruby`, `.csproj` or a directory -> `dotnet run --project <path> --no-build`. Unknown extensions should raise an error.
Rust client server command must include the runtime
When launching a server with `cargo run -- <command...>`, the arguments must form a complete command: `cargo run -- python ./server/weather.py` and `cargo run -- node ./server/build/index.js` are correct, while `cargo run -- ./server/weather.py` fails because a Python script is not necessarily executable by itself. If a command cannot be found, use its absolute path or ensure it is on PATH.
When to pick MCPB over remote HTTP
Use the MCP Bundles (MCPB) path when the server must touch the user's machine: reading local files, driving desktop applications, or talking to localhost services. MCPB archives use the .mcpb extension and bundle the runtime so users do not need Node or Python installed. The MCPB spec lives at https://github.com/modelcontextprotocol/mcpb.
npx -y flag in MCP server launch commands
In stdio launch configs, `"command": "npx"` runs the server through Node.js's npx, and the `-y` argument auto-confirms installation of the server package so the launch is non-interactive. Node.js (LTS recommended) must be installed; verify with `node --version`.
mcpServers JSON schema for a stdio server in Claude Desktop
Claude Desktop launches local stdio MCP servers from a JSON object keyed `mcpServers`. Each entry is a friendly server name mapping to an object with `command` (the executable, e.g. `npx`), `args` (array of arguments), and optionally `env`. Example: {"mcpServers": {"filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop", "/Users/username/Downloads"]}}}. On Windows the paths use escaped backslashes, e.g. "C:\\Users\\username\\Desktop".
Stdio servers log to stderr, never stdout
Stdio-based MCP servers may use stderr for all of their logging, so the per-server log file `mcp-server-SERVERNAME.log` is not limited to errors. Stdout on a stdio transport is reserved for protocol messages.
Undefined working directory for stdio servers launched by a client
When an MCP client launches a stdio server, the working directory may be undefined (for example `/` on macOS) because the client could be started from anywhere. Always use absolute paths in configuration files and `.env` files. Only when testing a server directly from the command line is the working directory the directory where the command was run.
Never log to stdout on stdio transport
MCP servers using the local stdio transport must never write log messages to stdout, because stdout carries the JSON-RPC protocol stream and any extra output corrupts it. All logging must go to stderr, which the host application captures automatically.
stdio servers inherit only a limited subset of environment variables
MCP servers launched over stdio inherit only a limited, platform-dependent subset of environment variables automatically. To override defaults or supply your own, add an `env` key in the client config, e.g. {"mcpServers": {"myserver": {"command": "mcp-server-myapp", "env": {"MYAPP_API_KEY": "..."}}}}.
Inspector stdio: use -- to pass args to your server
When launching a stdio server with the MCP Inspector, everything positional is the command line, e.g. `mcp-inspector node build/index.js -- --verbose --config /etc/myserver.conf`. You must put `--` before any arguments meant for your server; without the separator flags like `--verbose` are parsed by the Inspector and never reach the server.
Inspector stdio: -e for env vars and --cwd for working directory
Give a spawned stdio MCP server process environment variables with repeated `-e` flags and a working directory with `--cwd`, e.g. `mcp-inspector -e API_KEY=abc123 -e REGION=us-east-1 --cwd ~/projects/my-server node build/index.js`.
Debug stdio server stderr in the TUI Console tab
Because a stdio MCP server must keep stdout clean for JSON-RPC, its diagnostic output goes to stderr; the Inspector TUI surfaces that stderr from the connected stdio server process in the Console tab, opened with the `o` key.
Use stdio transport to limit local server exposure
MCP servers intended to run locally SHOULD use the stdio transport so that access is limited to just the spawning MCP client. If an HTTP transport is used locally instead, access MUST be restricted — for example by requiring an authorization token, or by using unix domain sockets or other IPC mechanisms with restricted access — otherwise an insecure server left listening on localhost can be reached by other processes or via DNS rebinding.
stdio proxy architectures can escalate XSS to RCE
The stdio transport is not inherently vulnerable, but in proxy architectures where a separate local proxy service spawns MCP servers as child processes over stdio, an attacker who gains client-side code execution (e.g. via a javascript: OAuth URL) can steal the client-to-proxy authentication token, make authenticated requests to the local proxy, and have the proxy spawn arbitrary commands as if they were legitimate MCP server commands, achieving RCE with user privileges. This does not apply to direct stdio usage without a proxy.
Hardening MCP proxy services that spawn stdio servers
MCP proxy services SHOULD sandbox or containerize spawned stdio processes, restrict their file system access, log all stdio transport usage for security monitoring, and require additional authorization for potentially dangerous commands. Clients SHOULD isolate proxy communication in a separate security context, apply least privilege to proxy process permissions, and sandbox or containerize the proxy service itself.
C# stdio servers must use CreateEmptyApplicationBuilder
When building a C# MCP server that uses STDIO transport, create the host with `Host.CreateEmptyApplicationBuilder(settings: null)` rather than `CreateDefaultBuilder`, because the default builder writes additional messages to the console and would corrupt the JSON-RPC stream. This is only required for STDIO transport servers.
Never write to stdout in a STDIO MCP server
For STDIO-based MCP servers, any write to stdout corrupts the JSON-RPC message stream and breaks the server. Language-specific offenders: Python `print()`, JavaScript/TypeScript `console.log()`, Java `System.out.println()`/`System.out.print()`, Kotlin `println()`, C# `Console.WriteLine()`/`Console.Write()`, Ruby `puts`/`print`, Rust `println!()`/`print!()`. For HTTP-based servers, stdout logging is fine because it does not interfere with HTTP responses.
TypeScript server startup over stdio
Connect a TypeScript MCP server to stdio with `const transport = new StdioServerTransport(); await server.connect(transport);` and log startup with `console.error("Weather MCP Server running on stdio")`. Catch fatal errors from `main()` and `process.exit(1)`.
Spring AI stdio server must silence banner and console logging
For a Spring AI stdio MCP server set `spring.main.bannerMode=off` and blank `logging.pattern.console=` (YAML: `spring.main.banner-mode: off` and an empty `logging.pattern.console`) so nothing extra is printed to stdout and the JSON-RPC stream stays valid.
Kotlin stdio transport and session lifecycle
Kotlin servers create `StdioServerTransport(System.`in`.asInput(), System.out.asSink().buffered())`, then inside `runBlocking` call `server.createSession(transport)` and keep the process alive by joining a `Job` completed from `session.onClose { done.complete() }`. Run in development with `./gradlew run`; for production build a shadow JAR and run `java -jar build/libs/weather-0.1.0-all.jar`.
Rust: running an MCP server over stdio
To run an rmcp server on stdio, build the transport as a tuple of stdin/stdout and serve it: `#[tokio::main] async fn main() -> Result<()> { let transport = (tokio::io::stdin(), tokio::io::stdout()); let service = Weather::new().serve(transport).await?; service.waiting().await?; Ok(()) }`. Build with `cargo build --release`; the binary lands in `target/release/<name>`.
Go: running the server on stdio transport
Run a Go MCP server with `server.Run(context.Background(), &mcp.StdioTransport{})`, logging fatal errors with `log.Fatal(err)`. Build with `go build -o weather .` to produce a binary at `./weather`.
Never write to stdout from a stdio MCP server (Go)
For STDIO-based servers, never use `fmt.Println()` or `fmt.Printf()`: writing to standard output corrupts the JSON-RPC message stream and breaks the server. Use `log.Println()` (which defaults to stderr) or `fmt.Fprintln(os.Stderr, ...)`/`fmt.Fprintf(os.Stderr, ...)` instead, or a logging library that writes to stderr or files.
stdio servers must log diagnostics to stderr
For stdio MCP servers, diagnostics belong on `stderr` — that is what the Inspector's Console tab displays. Anything written to stdout would corrupt the JSON-RPC stream.
stdio transport: client launches server as subprocess
In the stdio transport, the client launches the MCP server as a subprocess. The two ends communicate over the subprocess's standard streams, where the server reads JSON-RPC messages from stdin and writes JSON-RPC messages to stdout.
stdio message format: newline-delimited JSON-RPC
Each message is a single JSON-RPC request, notification, or response. Messages are delimited by newlines and MUST NOT contain embedded newlines. The wire format is one newline-delimited JSON-RPC message per line over a reliable bidirectional byte stream.
stdio: server stderr usage and client interpretation
The server MAY write UTF-8 strings to stderr for any logging purposes including informational, debug, and error messages. The client MAY capture, forward, or ignore the server's stderr output and SHOULD NOT assume stderr output indicates error conditions.
stdio: stdout must contain only valid MCP messages
The server MUST NOT write anything to its stdout that is not a valid MCP message. The client MUST NOT write anything to the server's stdin that is not a valid MCP message.
stdio client message types: requests and notifications only
The client sends messages by writing JSON-RPC requests and notifications to the server's stdin, one message per line. The client MUST NOT write JSON-RPC responses.
stdio server message types: responses and notifications
The client reads server messages from stdout, one message per line. All messages share this single channel with no per-request streams. The server writes three kinds of messages: (1) Responses to client requests correlated by JSON-RPC id, (2) Notifications that relate to an in-flight request such as notifications/progress and notifications/message, (3) Notifications delivered for an active subscriptions/listen request correlated using the io.modelcontextprotocol/subscriptionId field in _meta. The server MUST NOT write JSON-RPC requests to stdout.
stdio request metadata location
All request metadata for the stdio transport is carried inline in the JSON-RPC message body. The protocol version, per-request capabilities, and optional client identity live in _meta.io.modelcontextprotocol/* fields; the method name and arguments live where JSON-RPC puts them. There is no header layer.
stdio cancellation: send notifications/cancelled
To cancel an in-flight request, the client MUST send a notifications/cancelled notification referencing the request's ID. Because stdio is a single shared bidirectional channel, there is no per-request stream to close. Servers SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it.
stdio shutdown procedure
The client SHOULD initiate shutdown by: (1) Closing the input stream to the child process (the server), (2) Waiting for the server to exit, (3) If the server does not exit within a reasonable time, forcibly terminating the process using the mechanism appropriate for the operating system. On POSIX systems, forced termination typically escalates from SIGTERM to SIGKILL. On Windows where POSIX signals are not available, clients can use TerminateProcess or Job Objects.
stdio server graceful shutdown signal
Servers SHOULD exit promptly when their standard input is closed or reads return end-of-file. This is the primary graceful-shutdown signal and the only portable one, so honoring it reduces the need for forced termination.
stdio server-initiated shutdown
The server MAY initiate shutdown by closing its output stream to the client and exiting.
stdio unexpected termination: client should restart
If the server process exits unexpectedly, the client SHOULD restart it. Because the protocol is stateless, any in-flight requests are simply lost and the client can retry them against the fresh process. Active subscriptions/listen streams must also be re-established after restart.
stdio backward compatibility: use server/discover probe
A client that supports both modern (per-request-metadata) MCP versions and a legacy version that requires an initialize handshake SHOULD probe with server/discover before sending any other request, setting its preferred modern version in _meta. The probe has three possible outcomes: (1) The server returns a DiscoverResult: the server is modern. Select a mutually supported version from supportedVersions and continue. (2) The server returns a recognized modern JSON-RPC error such as UnsupportedProtocolVersionError: the server is modern but does not support the requested version. Use one of the versions in its advertised supported list. Do not fall back to initialize. (3) The server returns any other error, or does not respond within a reasonable timeout: the server is legacy. Fall back to the initialize handshake.
stdio backward compatibility: legacy server error codes
The fallback to initialize MUST NOT be keyed to one specific error code: legacy servers respond to unknown pre-initialize requests with implementation-defined errors (commonly -32601 or -32602) or not at all.
stdio backward compatibility probing recommendation
A client that only supports modern versions does not need to probe, but probing is still RECOMMENDED: some legacy servers do not validate that a request arrives after initialize and would process an era-ambiguous method (such as tools/call) under legacy semantics. Probing yields a deterministic failure instead.
stdio custom transports: reuse framing and message rules
Custom transports built on similar channels SHOULD reuse the newline-delimited JSON-RPC framing and the message rules from stdio; only the subprocess-specific aspects (launch, stderr, shutdown by closing the stream, process restart) need channel-specific equivalents.