Skip to main content

README.en.md

# 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