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:streamimports. - Streaming parser tolerates partial chunks via a buffer +
TextDecoder({ stream: true }). - Malformed lines are skipped silently; framing recovers on the next newline.
- Inherits
closedguard, listener registry, and idempotent close fromBaseTransport.
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 takesStdioTransportOptions; 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.