Глава 9

Для локального MCP используйте stdio дисциплинированно

Локально host запускает server как дочерний процесс и общается с ним через stdio. Тонкость в том, что stdin и stdout целиком принадлежат протокольным кадрам JSON-RPC - поэтому один случайный console.log способен сломать соединение, подмешав мусор в поток сообщений. Весь диагностический вывод идёт в stderr, и это не стилистика, а условие работоспособности.

Сервер запускается helper-ом, который умеет обслуживать и современных, и legacy-пиров, а ошибки пишет именно в stderr.

TypeScript
export function startStdioServer(): void {
  serveStdio(() => createDeveloperMcpServer(), {
    legacy: "serve",
    onerror: (error) => console.error("MCP server error:", error)
  });
}

if (import.meta.url === `file://${process.argv[1]}`) {
  startStdioServer();
}

Host, в свою очередь, поднимает дочерний процесс и открывает к нему транспорт.

TypeScript
export async function runMcpDemo(): Promise<McpDemoResult> {
  const serverScript = fileURLToPath(new URL("./server.ts", import.meta.url));
  const transport = new StdioClientTransport({
    command: process.execPath,
    args: ["--import", "tsx", serverScript],
    cwd: process.cwd(),
    stderr: "pipe"
  });
  const client = new Client(
    { name: "developer-release-client", version: "1.0.0" },
    {
      versionNegotiation: { mode: "auto" },
      enforceStrictCapabilities: true,
      defaultCacheTtlMs: 5_000
    }
  );

Такой транспорт хорош не везде, и границы применимости стоит очертить сразу. Stdio уместен для IDE-плагина, desktop-host, локального CLI - когда сервер установлен рядом с host и работает с локальным workspace. Он не годится, когда клиентов много и они ходят по сети, когда нужно независимое масштабирование, browser-origin или централизованная OAuth-авторизация - там начинается удалённый транспорт, о котором отдельная глава.

Пока же для локального сценария есть несколько операционных правил, нарушение которых стоит дороже всего.

  • На stdout - только протокольные сообщения. Для логов - stderr.
  • Секреты передавайте через окружение процесса только от доверенного launcher и минимальным набором.
  • Закрывайте дочерний процесс при disconnect, timeout и завершении host.
  • Не принимайте произвольную командную строку от модели: исполняемый файл и аргументы задаёт доверенная конфигурация.
  • В integration-тесте действительно spawn-ьте процесс - иначе вы не проверяете framing и shutdown.
Классическая поломка. console.log("Server started") на stdout выглядит для клиента как мусор вместо JSON-RPC. В companion-проекте onerror использует console.error, то есть stderr - и соединение остаётся чистым.

Сервер мы запускаем - теперь напишем клиента, который его находит и вызывает. И здесь главное слово - discovery.

Ссылки