Skip to content

Architecture at a glance

WebSocket MCP stdio USER human at the keyboard YOUR APP WS server + tab file MCP GATEWAY WS client + MCP AGENT Claude Code, Cursor, Desktop
Three processes, two protocols. Your app hosts a WebSocket endpoint and announces itself; the gateway dials in and speaks MCP stdio to the agent.
  • Your app - browser tab, React / Svelte / Vue / vanilla-TS, or a Node / desktop process. Hosts the action handlers and the real state they mutate. Also hosts a local endpoint the gateway dials into:
    • Browser apps get that endpoint for free by adding the @tesseron/vite plugin (or the equivalent for your dev server). The binding is WebSocket.
    • Node apps get it via @tesseron/server, which by default binds a loopback WebSocket; pass { transport: 'uds' } for a Unix domain socket on Linux/macOS.
    • Anything else (Electron main, .NET, Python, Go, Rust, ...) can follow the same pattern: bind whichever transport binding makes sense, drop an instance manifest at ~/.tesseron/instances/<instanceId>.json, speak the protocol.
  • The MCP gateway - a small Node process (@tesseron/mcp) bundled into the Claude Code plugin. Runs on stdio for the agent. Watches ~/.tesseron/instances/ and dials each app it finds via the binding the manifest advertises. The gateway never binds a port of its own.
  • The agent - Claude Code, Claude Desktop, Cursor, or any other MCP client. Doesn't know or care about transport bindings - it only sees standard MCP tools.
HopProtocolTransport
app ↔ gatewayTesseron JSON-RPC 2.0 (custom)WebSocket or UDS (per binding)
gateway ↔ agentModel Context Protocolstdio

The gateway is the only place that knows both dialects. Everything else is clean on each side: your app speaks one flavour of JSON-RPC, the agent speaks MCP.

App → Gateway (you send these):

  • tesseron/hello - register app, actions, resources, capabilities.
  • actions/invoke response - the return value of an invoked action.
  • actions/progress - streaming progress updates.
  • resources/updated - push notifications for subscribed resources.
  • sampling/request, elicitation/request - ask the agent or user something mid-handler.

Gateway → App (you handle these):

  • actions/invoke - the agent called one of your actions.
  • actions/cancel - the agent cancelled a running invocation.
  • resources/read, resources/subscribe, resources/unsubscribe - resource I/O.

Gateway → Agent (abstracted - the SDK takes care of MCP framing):

  • tools/list with entries named <app_id>__<action_name>.
  • tools/call results, streamed via notifications/progress where available.
  • resources/list, resources/read, resources/subscribe.

Because MCP doesn't run over WebSocket, and JSON-RPC-over-stdio doesn't work from a browser tab. The gateway reconciles the two, plus:

  • Session claiming. A 6-character code (AB3X-7K) that the user pastes into the agent binds one tab to one agent session. Keeps strangers out.
  • Local-only. Apps bind to loopback (TCP 127.0.0.1) or a private Unix socket; the gateway never binds a port. Nothing leaks off the machine.
  • Multi-app fan-in. You can run several apps at once; tools are namespaced by app.id so shop__addItem and admin__banUser never collide.

There is exactly one discovery mechanism and it works the same for every runtime:

  1. Your app binds an endpoint locally - WebSocket on loopback, or a Unix domain socket. Whichever binding fits the runtime.
  2. It writes ~/.tesseron/instances/<instanceId>.json with a { kind, url | path } spec.
  3. The gateway (watching that directory) picks up the file and dials the advertised endpoint via the matching dialer.
  4. Once connected, the app sends tesseron/hello and the normal protocol takes over.

No fixed ports. No environment variables. No "which gateway do I connect to". Just bind, announce, serve. If you want to port Tesseron to a new language, that's the entire runtime contract - everything else is libraries of your language's choosing.

Next: the 5-minute quickstart walks through getting this running end-to-end.