WebSocket-first のライブ UI自動生成される HTTP MCP API を備えた、ファイルベースルーティングの Elixir フレームワーク。

English

リンク

概要

Dialup は Next.js のような開発体験でライブアプリを構築できる Elixir フレームワークです。 中核は二つあります。人間向け UI は最初から WebSocket-first で動き、HTTP MCP API は同じページ宣言から自動生成されます。 各ページは 1 つの監視されたサーバー側アクターです。UI を <.dialup_action><.dialup_region> で書くと、Dialup が tools/listtools/callread_scene を導出します。 REST API の二重実装は不要です。

人間ブラウザ  WebSocket  UserSessionProcess  HTTP JSON-RPC  AI エージェント
                                      
                               command / set / navigate / handle_event
                                      
                          declare_action / dialup_action

特徴

  • WebSocket-first の人間 UI/ws と idiomorph によるライブ DOM 更新
  • 自動生成される HTTP MCP API — action / region 宣言がそのままエージェントツールになる
  • 同じイベント経路 — ブラウザ操作とエージェントの tools/call が同一 UserSessionProcess で直列化(command / set / navigate / handle_event
  • HTTP MCPPOST /mcp(Bearer token)で initialize / tools/list / tools/callPOST /agent/:token は legacy)
  • エージェント向け discovery/.well-known/dialup-agent、ページ埋め込み、/llms.txt
  • スコープ付きセッショントークン — 有効期限・投影・ capability を細かく制御
  • ファイルベースルーティング — ファイル配置がそのまま URL になる
  • サーバーサイドステート — 1 タブ = 1 UserSessionProcess
  • コロケーション CSS.ex の隣に .css、コンパイル時に自動スコープ化

クイックスタート

1. ジェネレータのインストール

mix archive.install hex dialup_new

2. 新規プロジェクト作成

mix dialup.new my_app
cd my_app
mix deps.get
mix run --no-halt

http://localhost:4000 にアクセス

エージェント対応ページの最小例

defmodule Dialup.App.Page do
  use Dialup.Page

  declare_action name: :increment, desc: "カウンタを増やす", 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

同じ <.dialup_action> が、ブラウザでは WebSocket 経由のボタンになり、エージェントには生成された MCP ツールとして見えます。 ブラウザで操作することも、トークンを取得して API を呼び出すこともできます(詳細は HTTP MCP API):

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":{}}}'

ドキュメント

mix docs --open で全文を参照できます。

その他は guides/ を参照(ルーティング、状態管理、デプロイなど)。

アーキテクチャ

1 タブにつき 1 つの UserSessionProcess が動きます。サーバー側セッションアクターがページ状態・ 宣言済み action・エージェント grant の single source of truth です。人間と AI エージェントは 異なるトランスポート(ブラウザは WebSocket、エージェントは HTTP JSON-RPC)から同じアクターに 到達します。

全体像

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 は同じハンドラへの legacy な path-token トランスポートです。

人間操作(WebSocket)

sequenceDiagram
  participant Browser as ユーザー / ブラウザ
  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 更新)

エージェント操作(HTTP MCP)

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: 再取得してから fresh _version で retry
  else version current
    MCP->>USP: apply mutation
    USP->>Agent: updated structuredContent
  end

エージェントには WebSocket トランスポートも server push もありませんread_scene で ポーリングします。

Agent-first browser handoff

エージェントが headless セッションを開始し、後から人間が参加する場合、join は 3 フェーズの ハンドシェイクです。ブラウザ URL を開いただけでは join は完了しません。

sequenceDiagram
  participant Agent as AI agent
  participant API as Dialup HTTP
  participant USP as UserSessionProcess
  participant Browser as 人間ブラウザ

  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 未設定
  Browser->>API: POST /_dialup/finalize-join
  Note over Browser,USP: cookie 設定、join token 消費
  Browser->>USP: __reconnect
  Note over Agent,Browser: 人間と agent が同一 UserSessionProcess を共有

トークン発行パターンは セッショントークン を参照してください。

宣言境界(declaration boundary)

  • MCP tool になるのは <.dialup_action> / declare_action/1<.dialup_region> / declare_region/1 のみです。生の ws-eventws-href などの ad-hoc WebSocket フックは エージェントへ 自動公開されません
  • confirm: :human の action は人間 UI のみで利用可能です。HTTP MCP から呼ぶと isError tool result が返ります。
  • エージェントが見える state は agent_state/1 による allowlist 投影であり、assigns 全体の dump ではありません。

ライセンス

MIT