This is the documentation for v2 — looking for the v1 documentation?
Skip to content

Serve over stdio ​

A host that launches your server as a local child process talks to it over stdio: JSON-RPC requests arrive on stdin, responses leave on stdout. To host one endpoint that many clients connect to, serve the same factory over HTTP instead.

Serve a factory over stdio ​

serveStdio takes a factory; it owns the transport and calls the factory to build the instance that serves the connection.

ts
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';

const handle = serveStdio(() => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    // server.registerTool(...) — one factory builds the instance that serves the connection
    return server;
});

The process is now an MCP server. A host that spawns it lists and calls whatever the factory registered; until one does, the process waits on stdin.

Coming from v1?

serveStdio replaces the new StdioServerTransport() + server.connect(transport) wiring — run the codemod, then see the upgrade guide.

INFO

serveStdio serves older clients from the same factory by default; the legacy option and the full story are on Legacy clients. The entry also owns which protocol revision each connection negotiates — see Protocol versions.

Log to stderr, never stdout ​

Announce readiness with console.error, which writes to stderr.

ts
console.error('notes server is listening on stdio');

stdout is the JSON-RPC channel: the host parses every line of it as a protocol message. Add one console.log('debug: starting the notes server') to the program above and send it an initialize request. Its two output streams now carry:

[stdout] debug: starting the notes server
[stdout] {"result":{"protocolVersion":"2025-06-18","capabilities":{},"serverInfo":{"name":"notes","version":"1.0.0"}},"jsonrpc":"2.0","id":1}
[stderr] notes server is listening on stdio

The protocol channel opens with a line no JSON-RPC parser accepts, ahead of the initialize response. The console.error banner went to stderr, which the host keeps out of the channel and shows in its server log.

Test it with the Inspector ​

The MCP Inspector launches your server command itself and connects to it over stdio.

sh
npx @modelcontextprotocol/inspector node ./build/server.js

In the browser tab it opens, click Connect; the Tools tab lists and calls everything the factory registered, without configuring the server in a host.

Shut down cleanly ​

serveStdio returns a StdioServerHandle; its close() tears down the pinned server instance and the transport.

ts
process.on('SIGINT', () => {
    void handle.close();
});

close() resolves once the instance the factory built and the underlying transport are both shut down.

A signal handler is one path to teardown; the pipe itself is the other. When the client closes its end (stdin reaches end-of-file), the transport closes itself and the connection tears down automatically — no signal handler needed. Requests still in flight at that moment are aborted and never answered (EOF means the client is gone), so a client that wants answers keeps stdin open until it has read them. A server that holds nothing else keeping the event loop alive then exits on its own. If yours does hold a keep-alive handle — a timer, a connection pool, a file watcher — release it when the connection closes so the process can exit:

ts
serveStdio(() => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });

    // A handle that keeps the event loop alive: a heartbeat timer, a
    // connection pool, a file watcher, ...
    const heartbeat = setInterval(() => console.error('notes server: still serving'), 60_000);

    // Release it when the connection closes, so the process can exit.
    server.server.onclose = () => clearInterval(heartbeat);

    return server;
});

Recap ​

  • serveStdio(factory) is the stdio entry point: it owns the transport and calls your factory to build the instance that serves the connection.
  • stdout is the protocol channel; log with console.error.
  • One console.log puts a line no JSON-RPC parser accepts into the stream the host parses.
  • npx @modelcontextprotocol/inspector <command> exercises a stdio server without configuring it in a host.
  • The returned StdioServerHandle's close() tears down the pinned instance and the transport.
  • When the client closes the pipe (stdin EOF) the connection tears down by itself; release your own keep-alive handles on close so the process can exit.