Skip to content

Security model

Tesseron's security model is local-first, user-authorised. The MCP gateway binds to localhost and won't expose any action until a human types a short code out-of-band. These are the two gates.

Origin: localhost Origin: evil.com allowlist match otherwise YOUR APP http://localhost:3000 ATTACKER https://evil.com MCP GATEWAY verifyClient() ACCEPT -> tesseron/hello REJECT 403, close 1008
The MCP gateway inspects Origin at the upgrade handshake. Non-localhost origins are rejected unless explicitly allowed.

The WebSocket upgrade is accepted when:

  • Origin starts with http://localhost: or http://127.0.0.1:, or
  • Origin appears verbatim in TESSERON_ORIGIN_ALLOWLIST (comma-separated).

Anything else returns HTTP 403. This is defence-in-depth - it prevents a drive-by page on evil.com from spraying tesseron/hello messages at your gateway and enumerating your app's surface.

Even from a permitted origin, the session is inert until claimed. The flow:

  1. App connects and sends tesseron/hello.
  2. MCP gateway generates a random 6-char code (format XXXX-YY, excludes visually confusing characters) and returns it in welcome.
  3. The code is displayed out-of-band: gateway stderr, and wherever your app chooses to render it.
  4. The user types the code into the agent ("connect Tesseron session AB3X-7K").
  5. Agent calls tesseron__claim_session. If the code matches, the session transitions to Claimed and tools/list_changed fires.

The claim code is never sent on the WebSocket from the gateway to the agent - the user carries it across. That's the whole point: it's a human-performed authorisation gesture, not an electronic one.

Strength: ~1.5 billion possible codes (6 positions × ~32 unambiguous alphanumerics). A brute-force attacker would need ~750M tries for a 50% hit rate. Codes are single-use; a failed match does not retry.

hello [shop actions] hello [admin actions] tools/list tools/call shop__addItem actions/invoke tools/call admin__banUser actions/invoke SHOP APP app.id = 'shop' searchProducts, addItem ADMIN APP app.id = 'admin' listUsers, banUser MCP GATEWAY routes by prefix AGENT shop__* | admin__*
Two apps, one gateway. Tools are namespaced by app.id; actions route back to the declaring session.

Multiple apps can be connected at once. Every MCP tool is prefixed with the app.id, so shop__addItem and admin__banUser never collide. Internally the gateway routes tools/call name=shop__addItem to the session whose app.id === "shop" - if that session has disconnected, the call errors with -32003 ActionNotFound.

This means you can, without coordinating, keep your dashboard and your customer app both connected to the same agent. Each has its own claim code, its own origin check, its own session.

  • Malicious code running in your app's process. If an attacker already executes JS in your tab or on your Node server, they can call your SDK and declare whatever actions they want. Tesseron is no worse - and no better - than the process it's embedded in.
  • Malicious MCP clients on the same machine. Any local process can open a WebSocket to ws://127.0.0.1:7475 and send tesseron/hello. The origin check only fires if the client sends an Origin header; non-browser clients may not. The claim code is the second gate, and it requires human cooperation.
  • Prompt injection. If your handler's description or inputs are attacker-controlled, they can manipulate the agent's plans. Sanitise descriptions you show to the agent the same way you would sanitise HTML you show to users.
  • Exfiltration via resources. Anything you expose as a resource is readable by the claimed agent. Don't expose credentials, session tokens, or PII you haven't decided the user is okay sharing with Claude.
  • Never expand TESSERON_ORIGIN_ALLOWLIST by default. Add origins only for specific agent integrations that need them.
  • Treat the claim code as short-lived. Don't render it persistently in the UI after the session is claimed.
  • Log the agent.id that claimed the session - useful for auditing which agent actually ran which action.
  • For production tools, use per-user app IDs. shop_kenny vs shop_sarah prevents one user's agent from driving another user's tab, even if both are on the same machine.

That's the end of the Protocol section. The SDK section picks up from here - how to speak this protocol from TypeScript today and from other languages later.