@nodetool-ai/sdk is the public TypeScript client for a NodeTool server. It has two parts: a typed tRPC client that covers the server’s AppRouter, and ChatSocket, an event-emitter wrapper around the chat WebSocket. The source is in packages/sdk/src/.

Install

npm install @nodetool-ai/sdk

The package is ESM only and declares engines.node as >=22.0.0. ws is an optional peer dependency (^8.18.0). Install it when you use the chat socket in Node:

npm install ws

Inside the NodeTool monorepo, depend on it as @nodetool-ai/sdk: "*".

Exports

The package has one entry point, @nodetool-ai/sdk.

Export Kind Purpose
createNodetoolClient function Builds a NodetoolClient from a base URL and options.
ChatSocket class Chat WebSocket client. You normally get one from client.chat().
NodetoolClient, CreateNodetoolClientOptions types Client and option shapes.
ChatEvent, ChatChunkEvent, ChatMessageEvent, ChatToolCallEvent, ChatErrorEvent, ChatGenerationStoppedEvent, ChatThreadUpdateEvent, ChatRawEvent types Inbound frame types. ChatEvent is the union, and it also includes PlanningUpdate and TaskUpdate.
SendChatMessageOptions, ChatSocketOptions, ConnectionState, WebSocketCtor, ChatFrameFields, ChatFrameValue types Socket options and wire-level value types.
AppRouter type The server’s tRPC router type, for your own typed wrappers.
Chunk, ContentType, JobStatus, TaskUpdate, PlanningUpdate, ToolCallUpdate, ErrorMessage types Re-exported from @nodetool-ai/protocol.

Create a client

import { createNodetoolClient } from "@nodetool-ai/sdk";

const nodetool = createNodetoolClient({
  baseUrl: "http://localhost:7777",
  authToken: process.env.NODETOOL_TOKEN ?? null
});

CreateNodetoolClientOptions:

Option Type Behavior
baseUrl string Required. A trailing slash is removed. tRPC calls go to <baseUrl>/trpc and the chat socket to <baseUrl>/ws, with http mapped to ws and https to wss.
authToken string \| null Optional bearer token. Sent as Authorization: Bearer <token> on tRPC calls and as a ?token= query parameter on the chat WebSocket.
fetch typeof fetch Optional override. Defaults to the global fetch, and the client throws if there is none.
WebSocket WebSocketCtor Constructor for environments without a global WebSocket.

The returned NodetoolClient has baseUrl, authToken, trpc, chat() and listLanguageModels(). tRPC calls are batched over HTTP, with each batch sent as a POST.

Call the typed API

client.trpc is a CreateTRPCClient<AppRouter>. Routers include threads, messages, models, workflows, jobs and assets, among others. Inputs and outputs are typed from the server.

const { workflows, next } = await nodetool.trpc.workflows.list.query({
  limit: 20
});
for (const w of workflows) {
  console.log(w.id, w.name);
}

// Page forward with the cursor from the previous response.
if (next) {
  await nodetool.trpc.workflows.list.query({ limit: 20, cursor: next });
}

jobs.list takes limit, workflow_id, project_id, include_outputs and start_key, and returns jobs plus next_start_key.

The SDK does not wrap workflow execution or job progress streaming. To run a workflow, use the REST and WebSocket endpoints in the API Reference. The SDK only lets you read jobs through trpc.jobs.*.

listLanguageModels() returns { id, name, provider }[] for every configured provider that advertises the generate_message capability. A provider whose model list fails to load is skipped.

const models = await nodetool.listLanguageModels();

Chat over WebSocket

client.chat() returns a new ChatSocket. Open one socket for each active thread if you need concurrent streams.

const thread = await nodetool.trpc.threads.create.mutate({ title: "Hello" });
const socket = nodetool.chat();

socket.on("chunk", (c) => process.stdout.write(c.content));
socket.on("message", (m) => console.log("persisted:", m.id));
socket.on("error", (e) => console.error(e.message));
socket.on("state", (s) => console.log("socket state:", s));

socket.connect();

send() and stop() throw WebSocket is not connected unless the socket is open. Wait for the state event to report "connected" before sending:

socket.on("state", (s) => {
  if (s === "connected") {
    socket.send({
      threadId: thread.id,
      text: "Summarize the last run.",
      provider: "openai",
      model: "gpt-5.4-mini"
    });
  }
});

Methods

Method Behavior
connect() Opens the socket. Does nothing if it is already open or connecting.
disconnect() Closes the socket and cancels a pending reconnect.
send(opts) Sends a chat_message command.
stop(threadId) Sends a stop command for the thread.
resume(threadId, lastSeq) Sends resume_chat. The server replays frames it stamped after lastSeq.
on(event, handler) Subscribes and returns an unsubscribe function.
getState() Returns the current ConnectionState.

SendChatMessageOptions has threadId and text (required) and the optional fields model, provider, agentMode, tools, collections and permissionMode ("plan", "default" or "auto"). The server default is "default", which parks tool actions on an approval request, so a script with no approval handler should set permissionMode: "auto" deliberately.

Events

on() accepts chunk, message, tool_call, error, generation_stopped, thread_update, planning_update, task_update, raw and state. raw fires for every decoded frame, including frame types with no typed handler. state carries a ConnectionState: idle, connecting, connected, disconnected, reconnecting or error.

Wire format and reconnects

Outbound commands are encoded with msgpack. If encoding fails, the socket sends JSON text instead. Inbound binary frames are decoded as msgpack and text frames as JSON. Frames that fail to decode are logged and dropped.

After an unintentional close the socket reconnects with a linear backoff of reconnectDelayMs * attempt, starting at 1500 ms and capped at 15 s. It gives up after maxReconnect attempts (default 10) and sets the state to error. Set maxReconnect to 0 to disable reconnecting. These two options exist on ChatSocketOptions, and client.chat() does not pass them, so construct ChatSocket directly to change them:

import { ChatSocket } from "@nodetool-ai/sdk";

const socket = new ChatSocket({
  url: "ws://localhost:7777/ws",
  authToken: process.env.NODETOOL_TOKEN,
  maxReconnect: 3
});

The protocol itself is described in the Chat API.

Use in Node

Node has a global fetch but no global WebSocket. Pass the constructor from ws, or chat() and new ChatSocket() throw No WebSocket constructor available:

import { WebSocket } from "ws";
import { createNodetoolClient } from "@nodetool-ai/sdk";

const nodetool = createNodetoolClient({
  baseUrl: "http://localhost:7777",
  WebSocket: WebSocket as unknown as typeof globalThis.WebSocket
});

The cast is only needed if your TypeScript configuration reports a type mismatch between ws and the DOM WebSocket type. Browsers need no constructor. The fetch and WebSocket options are the places to inject a wrapped fetch or a test double.

See examples/chat_app/ in the repository for a complete chat client built on the SDK.