@tesseron/server
@tesseron/server is what you use in a Node process - an Express server, a NestJS app, a CLI tool, a background worker, an Electron main process. The builder API is identical to @tesseron/web; the transport is what's different.
How it connects
Section titled “How it connects”Unlike the browser SDK, Node can host its own listener. @tesseron/server ships two transport bindings and picks one based on connect() options:
- WebSocket on loopback (default). Binds
127.0.0.1on an OS-picked port. - Unix domain socket, opt-in via
tesseron.connect({ transport: 'uds' }). Linux + macOS only; falls back to WS on Windows.
Either way the connection flow is the same:
- On
tesseron.connect()the SDK creates the host endpoint. - Writes
~/.tesseron/instances/<instanceId>.jsonwith a{ kind, url | path }spec. - Waits for the gateway to dial in.
- On the first and only accepted connection, sends
tesseron/helloand runs the normal Tesseron handshake.
No environment variables, no fixed ports, no client URL. The instance manifest does everything.
When to use server vs web
Section titled “When to use server vs web”| Use server when | Use web when |
|---|---|
| The handler's work lives on the backend (DB writes, queue jobs, filesystem). | The handler's work needs DOM or browser APIs. |
| You don't need the user's tab to be open. | The agent should only work while the user is viewing the page. |
| You want a headless service that Claude can drive. | You want Claude to drive the UI the user is already looking at. |
Both can run at the same time against the same MCP gateway - multi-app coexistence is first-class.
Exports
Section titled “Exports”import { // Singleton client - pre-constructed, use directly. tesseron, // Class (if you need multiple clients per process). ServerTesseronClient, // WS-binding transport — WS server + manifest writer. NodeWebSocketServerTransport, type NodeWebSocketServerTransportOptions, // UDS-binding transport — net server + manifest writer. UnixSocketServerTransport, type UnixSocketServerTransportOptions,} from '@tesseron/server';Typical process layout
Section titled “Typical process layout”import { tesseron } from '@tesseron/server';import { z } from 'zod';
tesseron.app({ id: 'notes_api', name: 'Notes API', description: 'CRUD over the notes store',});
tesseron .action('createNote') .input(z.object({ title: z.string(), body: z.string() })) .handler(async ({ title, body }) => { return db.notes.insert({ title, body }); });
tesseron.resource('noteCount').read(() => db.notes.count());
async function main() { const welcome = await tesseron.connect(); console.log(`Tesseron ready. Claim code: ${welcome.claimCode}`);}
main().catch((err) => { console.error(err); process.exit(1);});
async function shutdown() { await tesseron.disconnect(); process.exit(0);}process.on('SIGINT', shutdown);process.on('SIGTERM', shutdown);Customising the bind
Section titled “Customising the bind”WebSocket binding (default)
Section titled “WebSocket binding (default)”await tesseron.connect({ appName: 'notes_api', host: '127.0.0.1', port: 0 });appName- stamped into the instance manifest so the gateway log names your app usefully. Defaults to'node'.host- always127.0.0.1in practice; exposed for tests that need::1.port-0(OS picks) is almost always what you want. Setting a fixed port only matters if you're reverse-tunnelling the transport.
UDS binding
Section titled “UDS binding”await tesseron.connect({ transport: 'uds', appName: 'notes_api' });// or, override the socket path:await tesseron.connect({ transport: 'uds', path: '/tmp/notes.sock' });appName- same as WS.path- omit to let the SDK create a per-process 0700 temp dir underos.tmpdir()and bind<dir>/sockinside (recommended; the parent dir is the access gate). Pin a path only if you need to coordinate with another process that expects it.
Pass a Transport instead to bypass bind-and-announce entirely - useful in tests or when you're piping frames through some other channel.
Express example
Section titled “Express example”The express-prompts example shows the canonical "HTTP + Tesseron on one Node process" pattern. Keep the shared state outside both entry points; each channel calls the same functions:
const prompts = new Map<string, Prompt>();
// REST surfaceapp.post('/prompts', (req, res) => { const p = createPrompt(prompts, req.body); res.status(201).json(p);});
// Tesseron surface - same underlying functiontesseron.action('addPrompt') .input(z.object({ name: z.string(), template: z.string() })) .handler((input) => createPrompt(prompts, input));Transport details
Section titled “Transport details”NodeWebSocketServerTransport (WS binding)
Section titled “NodeWebSocketServerTransport (WS binding)”Wraps the ws npm package (v8). It:
- Binds a WebSocket server via Node's built-in
http.createServer. - Accepts exactly one upgrade request that advertises the
tesseron-gatewaysubprotocol; every other upgrade attempt is destroyed. - Tolerates every frame shape
wshands back -string,Buffer,Buffer[],ArrayBuffer- and coerces to UTF-8 before parsing. - Writes its instance manifest on
listen()and deletes it onclose().
See the WebSocket binding spec for the wire-level rules.
UnixSocketServerTransport (UDS binding)
Section titled “UnixSocketServerTransport (UDS binding)”Wraps Node's net module. It:
- Creates a private (mode
0700) directory underos.tmpdir()and binds a socket inside it (or uses the path you supplied). chmod 0600s the socket file after bind, so the inode rejects connect attempts from other UIDs.- Accepts exactly one connection; rejects subsequent connect attempts.
- Frames messages as NDJSON:
JSON.stringify(msg) + '\n'per outbound,\n-split on inbound. - Writes its instance manifest on bind and deletes it (plus the temp dir) on
close().
See the UDS binding spec for the wire-level rules and the Windows limitation.
Running under Docker / systemd
Section titled “Running under Docker / systemd”Two things to get right:
- Same HOME dir as the gateway. The gateway reads
~/.tesseron/instances/; your Node process has to write there. In containers, mount~/.tesseroninto the container's$HOME. - Signal handling.
process.on('SIGTERM', …)to calltesseron.disconnect()before exit cleans up the manifest and gives the gateway a clean close (code 1001 on WS, normal'close'on UDS) so the agent doesn't see abrupt tool failures.
Claim codes surface on stdout/stderr of your Node process, not the gateway's. Plan how you expose them to humans - a web UI endpoint, a file you rotate, whatever fits.
Capabilities
Section titled “Capabilities”Server handlers get the same ActionContext as browser handlers. Two differences to know:
ctx.client.origin- fabricated. Typically the string"node:<app.id>"or similar. Don't use it for auth.ctx.client.route- alwaysundefined. There's no "current route" on the server.
Everything else - progress, sample, elicit, log, signal - behaves the same.