Askr documentation
Server & APIs

Stdio Transport

Stdio Transport at the Askr server boundary, with validated input and explicit failure states.

Example

Create one MCP server, register typed primitives on it, and let the selected transport supply auth, cancellation, sessions, and progress.

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 buffers bounded UTF-8 lines, handles LF and CRLF input across chunk boundaries, rejects malformed or oversized lines, 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 detaches the input/output transport hooks; 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 after input EOF or close.

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.