Stdio Transport
Use stdio transport with the current published Askr packages.
@askrjs/askr0.0.59
How to use stdio transport
Create one MCP server, register typed primitives on it, and let the selected transport supply auth, cancellation, sessions, and progress.
- Import the published @askrjs/askr entrypoint.
- Keep stdio transport configuration next to the component, route, or server composition root that owns it.
- Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { schema } from '@askrjs/schema';
import { createMcpServer } from '@askrjs/server/mcp';
export const mcp = createMcpServer({
name: 'project-tools',
version: '1.0.0',
}).tool(
'lookup-project',
{ input: schema.object({ id: schema.uuid() }) },
async (context, { id }) => ({
content: [{ type: 'text', text: await lookup(id, context.signal) }],
})
);Node adapter
`connectMcpStdio(mcp, options)` from `@askrjs/node/mcp` bridges a `McpServer` to newline-delimited JSON-RPC over `process.stdin`/`process.stdout` (or any `Readable`/`Writable` you pass via `options.input`/`options.output`), the transport most MCP clients that launch a local subprocess expect. It reads lines with Node's `readline` interface (`crlfDelay: Infinity` so it handles both LF and CRLF input), parses each as JSON, and forwards valid messages to `mcp.handle(...)` with a `transport: "stdio"` environment.
Input and output
Each line in is expected to be a single JSON-RPC message; each response or notification out is written as one JSON object followed by `\n` via `output.write`. `options.auth` can be a static `AuthContext` or a function `(environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>` — useful for deriving auth from an environment variable the launching client sets — and defaults to an anonymous, unauthenticated context if omitted. Because `supportsPush` is always `true` for stdio, `context.progress` and `context.log` work on every call without any extra setup, unlike the HTTP transport where push requires an open SSE channel.
Process lifecycle
The connection exposes `closed` (a promise that resolves once the stream ends) and `close()` to shut it down deliberately. Closing aborts every in-flight request's `AbortController`, calls `mcp.terminateSession(sessionId)` on the underlying server, and closes the `readline` interface; passing `options.signal` lets an external `AbortController` trigger the same teardown, which is the natural way to tie stdio lifetime to your process's own shutdown signal. If the input stream simply closes (the client exits or pipes EOF) without an explicit `close()` call, the adapter still resolves `closed` from the `readline` `'close'` event.
Diagnostics
Errors thrown while handling a line — including anything a tool handler throws that isn't caught by the tool-dispatch layer — are caught and written to `options.diagnostics` (defaulting to `process.stderr`) as `MCP stdio error: <message>`, rather than crashing the process or corrupting stdout. Keeping diagnostics on stderr matters because stdout is reserved for JSON-RPC frames; anything a client reads from stdout that isn't valid, complete JSON will break the protocol, so avoid `console.log` and similar calls anywhere in a stdio-connected process.