Architecture at a glance
Three processes
Section titled “Three processes”- 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/viteplugin (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.
- Browser apps get that endpoint for free by adding the
- 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.
Two protocols
Section titled “Two protocols”| Hop | Protocol | Transport |
|---|---|---|
| app ↔ gateway | Tesseron JSON-RPC 2.0 (custom) | WebSocket or UDS (per binding) |
| gateway ↔ agent | Model Context Protocol | stdio |
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.
What travels on each hop
Section titled “What travels on each hop”App → Gateway (you send these):
tesseron/hello- register app, actions, resources, capabilities.actions/invokeresponse - 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/listwith entries named<app_id>__<action_name>.tools/callresults, streamed vianotifications/progresswhere available.resources/list,resources/read,resources/subscribe.
Why an MCP gateway?
Section titled “Why an MCP gateway?”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.idsoshop__addItemandadmin__banUsernever collide.
Discovery, not binding
Section titled “Discovery, not binding”There is exactly one discovery mechanism and it works the same for every runtime:
- Your app binds an endpoint locally - WebSocket on loopback, or a Unix domain socket. Whichever binding fits the runtime.
- It writes
~/.tesseron/instances/<instanceId>.jsonwith a{ kind, url | path }spec. - The gateway (watching that directory) picks up the file and dials the advertised endpoint via the matching dialer.
- Once connected, the app sends
tesseron/helloand 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.