$ mix dialup.new my_app → Get Started!

WebSocket-first
Elixir Web Framework

Dialup はサーバー上の GenServer がブラウザの状態を持ち続ける WebSocket ファーストの Elixir フレームワークです。

app/counter/page.ex
defmodule Dialup.App.Counter.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

Web UI と MCP Tools が自動で対応

<.dialup_action> で Web UI を宣言すると、同じ操作が MCP ツールとして tools/list に掲載されます。別途 REST を設計する必要はありません。

1

Web UI を宣言する

<.dialup_action command={...} desc="Add a line item">
  Add item
</.dialup_action>
2

MCP ツールが自動生成される

POST /mcp  Authorization: Bearer TOKEN
{"method":"tools/list"}
→ add_item, read_scene, …
3

同じセッションを更新する

{"method":"tools/call",
 "name":"add_item",
 "arguments":{"sku":"SKU-1","_version":3}}

Dialup の特徴

1 tab = 1 process で、全てのページにまたがってセッションが継続。
状態はサーバー上に存在し、WebSocket で同期される。

🤖

UI → HTTP MCP

action / region 宣言から tools/listtools/call を自動生成。人間は WebSocket、AI は JSON-RPC。

1 tab = 1 process

各セッションが独立した GenServer を持つ。状態はサーバー上に存在し、WebSocket で同期される。

🏗️

Command 駆動の DDD

<.dialup_action command={...}>Commanded の command を dispatch。UI と MCP は read model を投影し、変更は集約コンテキストへ一本化。

🔄

session / assigns の分離

session はナビゲーションをまたいで持続。assigns はページ遷移でリセット。ログイン状態が自然に保たれる。

🧩

クライアント側 DOM モーフィング

サーバーは HTML を送るだけ。差分適用は idiomorph が担う。プロトコルが単純でデバッグしやすい。

🪶

軽量な依存関係

Phoenix 本体は不使用。Bandit + HEEx + idiomorph のみ。クライアント JS は自作 ~100 行 + idiomorph 5KB。

実際に動かしてみる

MCP のライブデモと、カウンター・フォームの UI デモを用意しています。