Files
2026-05-27 23:29:53 +01:00

3.1 KiB

stdio

(no submodules)

Purpose

StdioTransport frames JSON-RPC messages over a newline-delimited JSON byte stream — the MCP spec default for sub-process child servers. The transport is universal: it accepts an injectable readable: AsyncIterable<Uint8Array> and a writable: UniversalWritable, so callers wire Readable.toWeb(process.stdin), Bun.stdin.stream(), browser ReadableStream, or an in-memory queue without the package depending on any Node-only module.

Features

  • Newline-delimited JSON framing — one message per \n-terminated line.
  • Injectable streams: no node:fs / node:process / node:stream imports.
  • Streaming parser tolerates partial chunks via a buffer + TextDecoder({ stream: true }).
  • Malformed lines are skipped silently; framing recovers on the next newline.
  • Inherits closed guard, listener registry, and idempotent close from BaseTransport.

Usage: Basic Example

import { createStdioTransport } from "@suaveplan/mcp/transport/stdio";
import type { JsonRpcMessage } from "@suaveplan/mcp";

const sentLines: string[] = [];
const inbox = new ReadableStream<Uint8Array>({ start() { /* … */ } });

const transport = createStdioTransport({
    readable: inbox,
    writable: {
        async write(chunk) { sentLines.push(new TextDecoder().decode(chunk)); },
        async close() {},
    },
});

transport.onMessage((m: JsonRpcMessage) => console.log("rx", m));
await transport.send({ jsonrpc: "2.0", id: 1, method: "ping" });

Usage: Advanced Example

import { createStdioTransport } from "@suaveplan/mcp";

// Bridge to a child process spawned by your test harness.
async function bridgeProcess(child: { stdout: AsyncIterable<Uint8Array>; stdin: WritableStream }) {
    const writer = child.stdin.getWriter();
    return createStdioTransport({
        readable: child.stdout,
        writable: {
            async write(chunk) { await writer.write(chunk); },
            async close() { await writer.close(); },
        },
    });
}

API Reference

  • interface StdioTransportOptions — { readable: AsyncIterable<Uint8Array>; writable: UniversalWritable }.
  • class StdioTransport extends BaseTransport — constructor takes StdioTransportOptions; starts reading asynchronously.
  • createStdioTransport(options): StdioTransport — convenience factory mirroring the documented public API.

Implementation Notes

The reader loop maintains an internal text buffer; each chunk is decoded with TextDecoder({ stream: true }) so multi-byte sequences split across reads still decode cleanly. After each newline the line is trim-ed and JSON.parse-d; failures or schema rejections are dropped because line framing makes the next message independent. Send framing writes JSON.stringify(message) + "\n" — JSON.stringify always escapes control characters in strings, so a literal newline in payload text becomes \\n and never breaks the framing. closeInternal flips a stopped flag the reader checks on each iteration and then closes the writable side; calling close while a long read is pending lets the reader exit on its next for await step.