AIに作業を任せるほど、ターミナル・ブラウザ・各種ツールを行き来する手間が増えていく。「今どのエージェントが何をしているのか」を見失う。その散らかりを、ひとつのデスクトップアプリに畳み込めないかと考えた。
目次
このガイドで作るもの
複数のAIエージェントの状態・会話・成果物を一画面で扱うデスクトップ司令塔「Claude Mission Control」。チャット、メディア生成(Studio)、ワークスペース把握、ノートブックを5つのタブに統合し、外部ツールは MCP(Model Context Protocol)で繋ぐ。1つのアプリで「投げて・見て・受け取る」が完結する構成だ。

前提環境:最初に道具を揃える
このアプリは3言語(TypeScript / Rust / Python)と外部CLIが噛み合う。動かす前に、土台を先に入れておく。ここを飛ばすと最初の pnpm tauri dev でつまずく。
- Node.js 20+ と pnpm … フロント。依存に
link:プロトコルがあるためnpmは不可。npm i -g pnpmで入れる。 - Rust 安定版(rustup)+ Tauri 2 のビルド要件 … デスクトップの殻。Windows なら Microsoft C++ Build Tools と WebView2 が要る。
- Python 3.12+ と uv … サイドカーの依存管理(pip より速い)。
- Hermes Agent … Hermes エージェントを使うなら必須(後述)。
- Docker Desktop(任意) … OpenClaw アダプタを使うときだけ。
- npx / Windows Terminal(
wt) / Obsidian Local REST API … それぞれ Notebook タブ・Workspace タブ・Obsidian 連携で使う。
土台が入ったら、セットアップはこれだけだ。
# フロント
pnpm install
# Python サイドカー
cd sidecar
uv sync --extra dev
cd ..
# 開発起動(Rust が Python サイドカーを自動 spawn する)
pnpm tauri dev
設計の起点:なぜ Tauri + Python サイドカーなのか
UI は軽く配布しやすくしたい、でも AI/ツール連携は Python の資産を使いたい。そこでフロントを Tauri 2 + SvelteKit、バックを Python FastAPI のサイドカーとして同梱し、両者をローカルで通信させる構成にした。Electron より軽量な配布物にしつつ、Python 側で重い処理や外部 API を担わせられる。
バイブコーディングで作る順番:大きく作らず、小さく積む
「複数AIの司令塔」と聞くと身構えるが、いきなり全部は作れない。バイブコーディングの5原則そのままに、動く最小単位を1つずつ積む。実際このアプリも Phase 0→8 の小さな段で組み上げた。各段で「AIに何を頼むか」をプロンプト例で示す。
- Phase 0:殻だけ出す。まず空のウィンドウが1枚。
プロンプト例:「Tauri 2 + SvelteKit(static adapter)の最小構成を作って。空ウィンドウが1枚出るところまで。ロジックはまだ要らない」 - Phase 1:サイドカーを1本だけ立てる。UI とは別プロセスの Python を起こし、ループバックWSで握手する。
「Python(FastAPI)でws://127.0.0.1:7878/wsを1本立てて。起動時に渡す--auth-tokenと一致しないWSは即閉じ、通ったら{"type":"ready"}を返すだけ」 - 最初のエージェントを1種だけ。共通インターフェース(ABC)を決め、まず Claude を pty で動かす1実装にする。
「AgentAdapterという抽象基底を作って。start/stop/status/send(prompt)の4つだけ。最初は claude を pty で駆動する実装をadapters/claude.pyに」 - 横断ライブを足す。「今どのエージェントが何をしているか」を EventBus で配る。
「pub/sub の EventBus をbus.pyに。アダプタは publish するだけ、WS は subscribe してそのまま UI に流すだけ」 - 外部ツールは MCP に寄せる。個別アダプタを書かず「同じ作法」に統一する。
「NotebookLM 連携をnpx -y notebooklm-mcpの stdio JSON-RPC で。専用クライアントは作らず MCP 1本に寄せて」 - 2種類目以降のエージェントを増やす。ABC があるので registry に足すだけ。ここで Hermes を入れる(次章)。
- 品質ゲートを「最初から」回す。3言語が絡むと手作業確認は必ず崩れる。
「pytest-asyncio と Vitest を最初から書いて。CI で pnpm / uv / cargo の3系統を全緑にしないとマージしない運用に」
コツは「1段ずつ動かして確かめてから次へ」。1段の中でも、AIの出力(特に層の境界をまたぐコード)は読んでから受け取る。丸呑みで積むと、後で必ず崩れる。
作り方の要点
1. タブで「役割」を分ける
機能を1画面に詰め込むと破綻する。Chat(会話・LIVEストリーム・スキルタイムライン)、Studio(画像/音声/動画/検索)、Workspace(プロジェクト把握)、Notebook の4系統にタブで分割し、それぞれが独立して動くようにした。「1タブ=1関心事」が、後からの拡張を楽にする。

2. 外部ツールは MCP で繋ぐ
各ツールに個別アダプタを書くと保守が地獄になる。Notebook 連携は MCP クライアント(Python)経由に寄せ、ツール側を差し替え可能にした。MCP に統一しておくと、新しいツールを「同じ作法」で足せる。
3. 品質ゲートを最初から回す
複数言語・複数プロセスのアプリは、手で確認していると必ず崩れる。pytest・vitest・E2E・CI を最初から全緑で維持する運用にした。「動いているはず」を「動いていると測れる」に変えるのが、この規模では生命線になる。
3層分離で「壊れない」司令塔を組む
ここからは、実際の Claude Mission Control がどう組まれているかを、稼働中の実装に沿って開く。肝は「3つの層に責務を厳格分離する」一点に尽きる。

レイヤー責務:ロジックを置く場所を1か所に固定する
3言語(TypeScript / Rust / Python)が混ざるアプリで最初にやるべきは、「どの層に何を書くか」を決め切ることだ。本アプリはこう固定した。
- Svelte 5 UI(
src/)… 描画と状態表示のみ。ロジックはゼロ。Tauri invoke/event のラッパ(lib/ipc.ts)と Svelte ストア(agents / connection / conversation)だけを持つ。 - Rust コア(
src-tauri/)… 薄く保つ。ウィンドウ/トレイ/blur/グローバルホットキー、Python サイドカーの spawn・健康監視・再起動、UI↔WS ブリッジ。秘密情報は OS の keyring に置き、.env に平文で置かない。 - Python サイドカー(
sidecar/)… 統合ロジックの本拠地。エージェントを知っているのはここだけ。FastAPI+WebSocket、AgentRegistry、EventBus、aiosqlite 短期ストア、各 MCP クライアント。
「UI にロジックを書かない/Rust を太らせない/統合は全部 Python」を徹底すると、どこを直せばいいか迷わなくなる。
Rust ↔ Python は loopback WebSocket + トークン認証
フロントとサイドカーは ws://127.0.0.1:PORT のループバックで繋ぐ。HTTP の単発リクエストではなく WebSocket にするのは、エージェントの進捗をサーバ側から押し出す(イベント駆動)必要があるからだ。127.0.0.1 限定+起動時に発行するトークンで認証し、ローカルの他プロセスから叩かれない様にする。Rust は Python を spawn したら健康監視し、落ちたら再起動する「番人」に徹する。
サイドカーは起動時に --auth-token を受け取り(既定 127.0.0.1:7878 で待受)、WS はこのトークンが一致しないと 1008 で即切断する。Authorization: Bearer ヘッダを優先し、無ければ後方互換で ?token= も見る。
# sidecar.py(要点)
@app.websocket("/ws")
async def ws(websocket: WebSocket) -> None:
token = _ws_token(websocket) # Bearer ヘッダ優先 / ?token= も可
if not verify_token(expected=auth_token, presented=token):
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
await websocket.accept()
await websocket.send_json({"type": "ready"}) # 握手完了
# 以降: EventBus を WS へ流す forwarder + 受信コマンドの dispatcher
Agent アダプタ:共通 ABC で「同じ作法」に揃える
拡張性の核がこれだ。エージェント種別ごとの差異(起動方法・通信方法)を共通の抽象基底(ABC)の裏に隠し、adapters/ に1種別=1実装で置く。
共通の抽象基底はこれだけ。たった4メソッドに揃える。
# adapters/base.py(要点)
class AgentAdapter(ABC):
name: str
type: str
@abstractmethod
async def start(self) -> None: ...
@abstractmethod
async def stop(self) -> None: ...
@abstractmethod
async def status(self) -> AgentStatus: ...
@abstractmethod
def send(self, prompt: str) -> AsyncIterator[Event]: ... # token/skill/error... を流す
この裏に、起動方法がまるで違う種別を adapters/ に「1種別=1実装」で隠す。本アプリは4種を持つ。
claude_code… pty(擬似端末)で対話プロセスを駆動openclaw_docker… Docker コンテナ制御hermes_daemon… Hermes のローカル HTTP ゲートウェイを叩く(次章で詳説)antigravity… Antigravity CLI を駆動
起動方法が違う4種でも、UI からは AgentRegistry を通して同じインターフェースで扱える。サイドカーは agents.toml の type を見て対応アダプタを差し込むだけ。新しいエージェントは「ABC を実装して registry に登録」すれば増える。個別 if 分岐を UI やコアに撒かないのが、保守を生かすか殺すかの分かれ目だ。

EventBus と短期/長期メモリの分離
横断ライブストリーム(どのエージェントが今何をしているか)は bus.py の pub/sub EventBus で配る。各アダプタはイベントを publish するだけ、UI は subscribe するだけ。状態は2層に分ける。短期は aiosqlite のローカルストア、長期記憶は Obsidian Vault へ MCP クライアント経由で永続化する。NotebookLM 連携は npx notebooklm-mcp を stdio で起動し JSON-RPC 2.0 で話す。すべて「MCP という同じ作法」に寄せてある。
3言語を全緑で回す品質ゲート
前半で「品質ゲートが生命線」と書いた、その実体。UI は Vitest + Testing Library、サイドカーは pytest-asyncio。パッケージは pnpm(依存に link: プロトコルがあるため npm は不可)・uv(Python)・cargo(Rust)の3系統。3言語が絡むと「片方を直して片方が壊れる」が日常になるので、CI 全緑を「進む条件」にしておく。
Hermes を「ローカルLLMゲートウェイ」として組み込む
ここが本ガイドで追記した肝だ。Hermes アダプタは、他の2種(pty・Docker)とは毛色が違う。Hermes 自身を OpenAI 互換の API サーバとしてローカルに立て、サイドカーが HTTP で叩く。これで grok / claude / gpt / llama を「同じ口」で差し替えられる。
仕組み:アプリが hermes gateway を spawn する
アダプタは Hermes Agent のゲートウェイを子プロセスで起こし、環境変数でローカル API サーバを有効化する。
# adapters/hermes.py(起動の要点)
env["API_SERVER_ENABLED"] = "true"
env["API_SERVER_PORT"] = "7777"
env["API_SERVER_KEY"] = "mission_control_key"
proc = await asyncio.create_subprocess_exec(
"hermes", "gateway", "run", "--replace", env=env, ...)
# 生存確認: GET http://127.0.0.1:7777/health
# 送信: POST http://127.0.0.1:7777/v1/chat/completions
# headers = {"Authorization": "Bearer mission_control_key"}
# json = {"model": "hermes-agent", "messages": [...], "stream": False}
OpenAI 互換なので、messages 配列に「system(過去インサイト)+直近履歴+今回の user」を積んで投げ、choices[0].message.content を受け取るだけ。長期記憶は Obsidian の Knowledge/hermes-insights.md 末尾を system に前置きする作りにしてある。
設定:agents.toml に1ブロック足す
Mission Control の設定は ~/.claude-mission-control/agents.toml。Hermes を1エージェントとして宣言する。
[agents.hermes]
type = "hermes_daemon"
binary = "hermes-agent.exe" # 起動時に gateway 用の hermes(.exe) へ自動補正
gateway_port = 7777
[obsidian]
vault_path = "C:/path/to/MyVault" # 長期記憶(insights)の置き場
Hermes 側の認証:どのモデルで喋らせるか
ゲートウェイが「誰に問い合わせるか」は Hermes 側の ~/.hermes/config.yaml が決める。アプリのモデル切替UIは、この model: ブロックを書き換えているだけだ。選べる代表は次の通り。
grok-4.3(provider: xai-oauth) … X の SuperGrok / Premium+ サブスクで認証。API キー不要。※この OAuth 経由の利用は xAI の利用規約の解釈・変更に依存する(公式アプリ外からの利用が制限される可能性がある)。確実に使うなら正規の xAI API キーを推奨。各自で現行規約を確認のこと。claude-3.5-sonnet/gpt-4o/llama-3.3-70b(provider: openrouter) … OpenRouter の API キーで認証。1キーで複数モデルに届く。
初回はこの2手だけ通せばいい。OAuth ログインの詳しい流れはXメディア生成パイプラインの作り方の「Hermes セットアップ」と同じだ。
# 1. Hermes を入れる(依存込み)
iex (irm https://hermes-agent.nousresearch.com/install.ps1) # Windows
# 2. プロバイダ認証(どちらか)
hermes login --provider xai-oauth # SuperGrok(ブラウザで X にサインイン)
# または hermes setup で OpenRouter キーを登録
# 3. 確認
hermes status # Provider が出れば認証済み
つまずきどころ:①ゲートウェイの実体は hermes.exe(hermes-agent.exe を指定しても自動補正される)。②xai-oauth のトークンは約6時間で切れ、リフレッシュトークンが失効すると生成が全停止する(その時は hermes login --provider xai-oauth で入れ直す)。③ポート 7777 が他プロセスと衝突するなら gateway_port を変える。
正直なまとめ
このアプリの価値は派手なUIではなく、「UIは描画だけ・Rustは番人・Pythonが統合・拡張はアダプタ」という責務分離にある。ここさえ守れば、エージェントもツールも後から増やせる。Hermes をローカルゲートウェイにすれば、grok / claude / gpt / llama を同じ口で差し替えられるのも、この「アダプタに寄せる」設計の果実だ。動かすには SuperGrok / OpenRouter の認証や Docker(OpenClaw 使用時)など環境準備が要るが、最初から全部を作ろうとせず、Phase 0 の空ウィンドウから小さく積めば、未経験でもここへ辿り着ける。
この制作で使ったもの PR
🖥 サーバ/ドメイン
🤖 使ったAIツール(公式)
📚 技術書(Amazon)



