WebSocket protocol
Clients talk to TnsServer over one WebSocket endpoint, /chat. This page covers protocol version 1: the events in each direction, sessions and their agents, MCP tool proxying and the session knowledge base. To start a server, see Running the server.
Connection lifecycle
- The client opens a WebSocket connection to
/chat. When the server has a bearer token (TNSAI_TOKENorAuthConfig.withToken), the upgrade must carryAuthorization: Bearer <token>; a browserWebSocketcannot set that header. See Server hardening. - When the upgrade URL carries a
sessionId, the server checks that session's capability token on upgrade. See Server hardening. - The first event binds the connection to its
sessionId. A later event with anothersessionIdgets aFORBIDDEN_SESSIONerror and is dropped. - The first event for a new
sessionIdcreates the session with oneassistantagent. Several connections can share one session. - When any connection of a session closes, the server cancels that session's pending approvals and running chat tasks.
- When the last connection of a session closes, the server removes the session. Its agents, audit trail and knowledge base are discarded, and a later event with the same
sessionIdstarts a new, empty session.
Every event must carry the protocol version (v: 1) and a sessionId. An event with another version gets a PROTOCOL_MISMATCH error. A plain ping text frame is ignored, so clients can use it as a keepalive.
Protocol v1 events
Client to server
| Type | Fields | Description |
|---|---|---|
chat | message | Send a user message to the session's active agent |
agent_add | role, provider?, model?, goal?, domain?, name?, roles? | Add a new agent to the session |
agent_remove | agentId | Remove an agent from the session |
tool_approve | toolCallId, decision | Respond to a tool approval request (approve, reject, always) |
cancel | None | Cancel the running chat task and pending approvals |
mcp_tools | tools: McpToolDef[] | Register MCP server tools for the session |
mcp_result | toolCallId, result?, error? | Return the result of an MCP proxy tool call |
set_autonomy | level | Change the autonomy level (FULL_AUTO, SUPERVISED, CAUTIOUS, MANUAL) |
kb_add | content, metadata? | Add a document to the session knowledge base |
kb_search | query, limit?, threshold? | Search the knowledge base |
kb_list | None | List all documents in the knowledge base |
kb_remove | documentId | Remove a document from the knowledge base |
Server to client
| Type | Fields | Description |
|---|---|---|
token | agentId, content | A streamed content token from the LLM |
tool_start | agentId, toolCallId, tool, args, risk | Tool execution has begun |
tool_approve_request | agentId, toolCallId, tool, risk, summary | User approval required for a tool call |
tool_result | agentId, toolCallId, tool, duration, result | Tool execution completed |
agent_state | agentId, status, role, provider?, model? | Agent status change (thinking, idle) |
team_update | agents: AgentInfo[], strategy | Full roster update after agent add/remove |
done | usage: {tokens, cost} | LLM response stream completed. tokens is the number of streamed content chunks, not model tokens; cost is always 0.0 |
error | message, code? | Error event with optional error code |
mcp_call | toolCallId, server, tool, args | Request the client to execute an MCP tool |
index_start | path, totalFiles | Directory indexing started |
index_progress | indexedFiles, totalFiles, currentFile | Indexing progress update |
index_done | fileCount, chunkCount | Indexing completed |
autonomy_changed | level | Autonomy level was updated |
audit_entry | toolCallId, agentId, tool, risk, decision, timestamp, summary | Audit trail entry |
kb_added | documentId | Document added to knowledge base |
kb_search_result | results: KbSearchHit[] | Knowledge base search results |
kb_list_result | documents: KbDocInfo[], totalCount | Knowledge base document listing |
kb_removed | documentId, found | Document removal result |
Sessions
Each session has its own agents, audit trail, autonomy level and knowledge base. The autonomy level starts at SUPERVISED; set_autonomy changes it, and Tool approval explains what each level asks for.
Multiple agents
A session can contain multiple agents with different roles and LLM configurations. You can add and remove agents dynamically at runtime using WebSocket events, which is useful for building team-based workflows where different agents handle different parts of a task.
// Add an agent using the server's configured default LLM supplier
{
"v": 1,
"type": "agent_add",
"sessionId": "sess-1",
"role": "reviewer",
"name": "Code Reviewer",
"goal": "Review code for security issues"
}Agent IDs are generated as {name}-{8hexchars} (e.g., Code Reviewer-abcd1234). After each add/remove, the server broadcasts a team_update event with the full agent roster, including each agent's tools, roles, and LLM configuration.
The example omits provider, so the new agent uses the server's llmClientSupplier. The only provider you can name is ollama, which uses model, or glm-5:cloud when model is missing. Any other provider gets an error event with code AGENT_ADD_FAILED, and no agent is added. To give agents a cloud model, set it as the server's supplier and omit provider.
After a successful add, the agent_state event reports the provider and model of the agent's actual client, not the request fields.
Multi-role agents can be created by providing a roles array instead of a single goal/domain:
{
"v": 1,
"type": "agent_add",
"sessionId": "sess-1",
"role": "fullstack",
"roles": [
{"name": "frontend", "goal": "Build React UIs", "domain": "web"},
{"name": "backend", "goal": "Build REST APIs", "domain": "server"}
]
}MCP tools
If your frontend application connects to MCP servers (like a filesystem or database server), you can register those tools with the TnsAI server session. The server wraps each tool as a McpProxyTool and injects them into all agents, enabling a proxy pattern where the server orchestrates and the client executes.
// Client sends mcp_tools event
{
"v": 1,
"type": "mcp_tools",
"sessionId": "sess-1",
"tools": [
{
"server": "filesystem",
"name": "read_file",
"description": "Read a file",
"inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}}
}
]
}When the LLM invokes an MCP tool, the server sends an mcp_call event to the client. The client executes the tool via its local MCP connection and returns the result via mcp_result:
Server -> Client: mcp_call {toolCallId: "abc", server: "filesystem", tool: "read_file", args: {path: "/src/App.tsx"}}
Client -> Server: mcp_result {toolCallId: "abc", result: "import React from 'react';..."}The McpProxyTool uses a CompletableFuture to block the agent's virtual thread until the client responds.
Knowledge base
Each session has its own knowledge base. The kb_* events add, search, list and remove documents in it, and POST /api/index indexes a whole directory into it. The session's agent searches it on every chat turn.
// Add a document
{"v": 1, "type": "kb_add", "sessionId": "s1",
"content": "React hooks must follow the rules of hooks...",
"metadata": {"source": "docs", "topic": "react"}}
// Search
{"v": 1, "type": "kb_search", "sessionId": "s1",
"query": "rules of hooks", "limit": 5, "threshold": 0.3}
// List all documents
{"v": 1, "type": "kb_list", "sessionId": "s1"}
// Remove a document
{"v": 1, "type": "kb_remove", "sessionId": "s1", "documentId": "doc-abc123"}Search results include documentId, content, score, and metadata for each hit. The kb_list_result event returns document previews (first 100 chars) and content lengths.
How chat runs
- A
chatmessage goes to the first agent in the session. The agent runsstreamChatWithTools, which handles the whole model, tool, model loop. - Before each turn, the handler binds the session's knowledge base to the agent, so the agent's knowledge search reads the top 5 results.
- Chat work runs on virtual threads, off the WebSocket thread.
cancelinterrupts the running chat task and cancels every pending approval.
HTTP endpoints
The server also serves health, index, search and audit endpoints over HTTP; see Endpoints.
Connection example
This JavaScript client sends one chat message, prints the streamed tokens and approves every tool call. It sends no bearer token, so it only works against a server without a bearer token.
const ws = new WebSocket("ws://localhost:7777/chat");
ws.onopen = () => {
// Send a chat message
ws.send(JSON.stringify({
v: 1,
type: "chat",
sessionId: "my-session",
message: "Explain the WebSocket protocol"
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case "token":
process.stdout.write(data.content);
break;
case "tool_approve_request":
// Auto-approve for this example
ws.send(JSON.stringify({
v: 1,
type: "tool_approve",
sessionId: data.sessionId,
toolCallId: data.toolCallId,
decision: "approve"
}));
break;
case "done":
console.log("\n[Done]", data.usage);
break;
case "error":
console.error("[Error]", data.message);
break;
}
};Related
- Running the server: start the server and see the tools each agent gets.
- Tool approval: risk levels, autonomy levels and approval requests.
- Server hardening: tokens, origins and binding.