# Dialup (English)
**WebSocket-first apps** and **auto-generated HTTP MCP APIs** in one file-based Elixir framework.
[日本語](./README.md)
## Links
- [Official website](https://dialup-framework.org/)
- [Getting Started guide](https://dialup.hexdocs.pm/getting-started.html)
## Overview
Dialup is an Elixir framework for building live applications with a Next.js-like developer
experience. It has two equal promises: the human UI is WebSocket-first from the first render, and
the HTTP MCP API is generated from the same page declarations. Each page is one supervised
server-side actor. You write the UI once with `<.dialup_action>` and `<.dialup_region>`; Dialup
derives `tools/list`, `tools/call`, and `read_scene` from those declarations. No duplicate REST
layer. No hand-written OpenAPI for agent tools.
```
Human browser ──WebSocket──► UserSessionProcess ◄──HTTP JSON-RPC── AI agent
│
command / set / navigate / handle_event
│
declare_action / dialup_action
```
### Features
- **WebSocket-first human UI** — live DOM updates over `/ws` with idiomorph
- **Auto-generated HTTP MCP API** — actions and regions become agent tools automatically
- **One event path** — browser events and agent `tools/call` serialize through the same `UserSessionProcess` (`command` / `set` / `navigate` / `handle_event`)
- **HTTP MCP request-response** — `initialize`, `tools/list`, `tools/call` at `POST /mcp` (Bearer token; `POST /agent/:token` is legacy)
- **Agent discovery** — `/.well-known/dialup-agent`, embedded page context, `/llms.txt`
- **Scoped session tokens** — least-privilege grants with expiry and projection control
- **File-based routing** — file placement maps directly to URLs
- **Server-side state** — one tab = one `UserSessionProcess`
- **Colocated CSS** — `.css` next to `.ex`, auto-scoped at compile time
## Quick Start
### 1. Install the generator
```bash
mix archive.install hex dialup_new
```
### 2. Create a new project
```bash
mix dialup.new my_app
cd my_app
mix deps.get
mix run --no-halt
```
Then visit http://localhost:4000
### Agent-ready page in 30 lines
```elixir
defmodule Dialup.App.Page do
use Dialup.Page
declare_action name: :increment, desc: "Increment counter", params: %{}
def mount(_params, assigns), do: {:ok, Map.put(assigns, :count, 0)}
def agent_state(assigns), do: %{count: assigns.count}
def handle_event(:increment, _, assigns) do
{:update, Map.update!(assigns, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<p>Count: {@count}</p>
<.dialup_action name={:increment}>+1</.dialup_action>
"""
end
end
```
The same `<.dialup_action>` is a WebSocket-backed browser button and a generated MCP tool. Use the
page from the browser, or grant a session token and call the API:
```bash
# After obtaining a token (see guides/mcp-api.md)
curl -X POST http://localhost:4000/mcp \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer TOKEN' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_scene","arguments":{}}}'
```
## Documentation
Run `mix docs --open` to browse the full documentation locally.
- [Events](./guides/events.md) — **WebSocket-first UI events and updates**
- [HTTP MCP API](./guides/mcp-api.md) — **auto-generated tools, discovery, versioning**
- [Building agent-native apps](./guides/agent-native-app-development.md) — implementation workflow
- [Session tokens](./guides/agent-handoff.md) — reaching a live browser session
- [Authentication](./guides/authentication.md) — HTTP auth, MCP grants, [sequence diagrams](./guides/authentication.md#sequence-diagrams)
- [Getting Started](./guides/getting-started.md) — installation and basics
- [Fullstack Example](./guides/fullstack-example.md) — Ecto and PubSub
See `guides/` for routing, state, lifecycle, events, deployment, and more.
## Architecture
Every live tab runs one `UserSessionProcess`. The server-side session actor is the **single source
of truth** for page state, declared actions, and agent grants. Human operators and AI agents reach
the same actor through different transports — WebSocket for the browser, HTTP JSON-RPC for MCP.
### System overview
```mermaid
flowchart LR
humanBrowser["Browser (human)"] -->|"WebSocket via dialup.js"| userSessionProcess["UserSessionProcess"]
aiAgent["AI agent (MCP client)"] -->|"HTTP JSON-RPC (POST /mcp)"| userSessionProcess
dialupAction["dialup_action / declare_action"] --> toolsList["tools/list"]
dialupRegion["dialup_region / declare_region"] --> toolsList
toolsList --> toolsCall["tools/call"]
toolsCall --> userSessionProcess
userSessionProcess --> render["render/1 + agent_state/1"]
render --> humanUI["idiomorph DOM update"]
render --> agentJSON["JSON response (no push)"]
```
`POST /agent/:token` is a legacy path-token transport to the same handler.
### Human interaction (WebSocket)
```mermaid
sequenceDiagram
participant Browser as User / browser
participant JS as dialup.js
participant USP as UserSessionProcess
Browser->>JS: click / input
JS->>USP: WebSocket event
USP->>USP: command / set / navigate / handle_event
USP->>USP: version++
USP->>JS: render diff
JS->>Browser: idiomorph morph (DOM update)
```
### Agent interaction (HTTP MCP)
```mermaid
sequenceDiagram
participant Agent as AI agent
participant MCP as POST /mcp
participant USP as UserSessionProcess
Agent->>MCP: initialize
MCP->>Agent: server capabilities
Agent->>MCP: tools/list
MCP->>Agent: declared actions + built-ins
Agent->>MCP: tools/call read_scene
MCP->>USP: read actor state
USP->>Agent: structuredContent + currentVersion
Agent->>MCP: tools/call mutation + _version
alt version stale
MCP->>Agent: isError
Agent->>MCP: tools/call read_scene
Note over Agent: re-fetch, then retry with fresh _version
else version current
MCP->>USP: apply mutation
USP->>Agent: updated structuredContent
end
```
Agents have **no WebSocket transport** and **no server push** — poll with `read_scene`.
### Agent-first browser handoff
When an agent starts a headless session and a human joins later, joining is a **three-phase**
handshake. Opening the browser URL alone does not complete the join.
```mermaid
sequenceDiagram
participant Agent as AI agent
participant API as Dialup HTTP
participant USP as UserSessionProcess
participant Browser as Human browser
Agent->>API: POST /_dialup/agent-session
API->>USP: start session + grant token
Agent->>API: tools/call issue_browser_url
API->>Agent: browserUrl
Browser->>API: WebSocket attach (tab_id + join_token)
Note over Browser,USP: cookie not set yet
Browser->>API: POST /_dialup/finalize-join
Note over Browser,USP: cookie set, join token consumed
Browser->>USP: __reconnect
Note over Agent,Browser: human and agent share one UserSessionProcess
```
See [Session tokens](./guides/agent-handoff.md) for token issuance patterns.
### Declaration boundary
- Only `<.dialup_action>` / `declare_action/1` and `<.dialup_region>` / `declare_region/1` become
MCP tools. Raw `ws-event`, `ws-href`, and other ad-hoc WebSocket hooks are **not** auto-exposed to
agents.
- Actions marked `confirm: :human` are available in the human UI only; HTTP MCP calls return an
isError tool result.
- Agent-visible state is an allowlisted projection from `agent_state/1`, not a dump of all assigns.
## License
MIT