主題: AI agents

MCP Apps 把 UI 放進對話,但不是每個 Tool 都要變成小網站

MCP Apps 能在 AI 對話裡顯示 dashboard、表單與視覺工具。真正好用的邊界是:tool 執行、View 呈現、host 管理信任。

動態迷因(展開/收合)
需要排序、篩選與比較資料時,硬塞成段落只會製造更多 prompt。 · 來源:GIPHY

MCP tool 很會回傳文字與 structured data。遇到使用者需要檢查 200 筆資料、調整六個互相影響的選項、在 canvas 拖動物件,或逐段確認 visual diff 時,就沒那麼好用了。

2026 年 1 月,MCP Apps 成為第一個正式 MCP extension。Tool 現在可以指向一份互動式 HTML resource,讓支援的 host 直接在對話裡顯示。daily.dev 的整理舉了 dashboard、diff viewer 與設定精靈等例子,背後則透過 JSON-RPC bridge 溝通。

看起來很像「把 web app 塞進聊天室」,這句話不算錯,但少了最重要的邊界:

Tool 負責執行 domain work,View 負責呈現互動,host 負責管理兩者之間的信任邊界。

少了這層分工,MCP App 很快就會變成一個 state 不清楚、API 重複、權限也說不明白的脆弱小網站。

MCP Apps 不是每次都讓模型生成 UI

MCP App 通常不是請模型替每次回應臨時發明一份 HTML。它的介面是 MCP server 預先宣告、自己維護並提供的 resource。

穩定版 MCP Apps 規格定義了四個主要部分:

  1. Tool 透過 _meta.ui.resourceUri 宣告 View。
  2. URI 使用 ui:// scheme,指向一份 HTML resource。
  3. Host 讀取 resource,放進 sandboxed iframe 顯示。
  4. Iframe 與 host 透過 postMessage 傳遞 JSON-RPC 訊息。

最小的 tool 宣告大概長這樣:

{
  "name": "visualize_orders",
  "description": "Show orders for a selected period",
  "inputSchema": {
    "type": "object",
    "properties": {
      "from": { "type": "string", "format": "date" },
      "to": { "type": "string", "format": "date" }
    }
  },
  "_meta": {
    "ui": {
      "resourceUri": "ui://orders/dashboard"
    }
  }
}

Server 再另外註冊 ui://orders/dashboard,並使用 MCP Apps 的 HTML MIME type。官方 quickstart 的總結很精準:MCP App = Tool + UI Resource

這個拆分很重要。Dashboard 不是訂單服務本身,只是 tool result 的一種呈現方式。

Host 不是只負責把 iframe 貼上去

Host 做的事比在 chat message 裡插入 iframe 多很多。

它要確認雙方是否支援 io.modelcontextprotocol/ui extension、讀取指定 resource、套用 sandbox 與 Content Security Policy、把 tool result 傳給 View,再把允許的呼叫代理回 MCP server。

整個流程可以簡化成:

Model 或使用者
    ↓ 選擇操作
Server 上的 MCP tool
    ↓ 回傳文字/structured data
Host
    ↓ 透過可稽核的 bridge 傳遞結果
Sandboxed View
    ↓ 使用者篩選、編輯、確認或要求下一步
Host
    ↓ 驗證並代理允許的 tool call
MCP server

每一層只負責一件事:

層級 應該負責 不該負責
MCP tool 驗證、授權、domain action、權威結果 排版與 click state
View 畫面、區域互動、accessible controls secrets 或最終授權
Host capability negotiation、隔離、同意、訊息仲介 產品 business rules

如果按鈕的意思是「刪除正式環境資料」,View 不會因為畫出了按鈕就自動取得權限。Server 仍要驗證請求,host 也可以在轉送前要求使用者明確同意。

直接操作比再問一句 prompt 更快時,UI 才值得

官方 overview提到複雜資料探索、相互依賴的設定、視覺創作、文件審查與 multi-step workflow。它們有一個共同點:使用者需要操作或檢查 state,不只是讀一句話。

適合 MCP Apps 的情況包括:

  • 能篩選、比較、drill down 的圖表。
  • 後續欄位會隨 environment、region 改變的部署表單。
  • 能逐段核准的 PDF 或 code diff。
  • 位置本身有意義的 canvas、地圖、播放器或 timeline。
  • 有上一筆、下一筆、接受與拒絕的批次 review queue。

不太需要 MCP Apps 的情況包括:

  • 回傳目前時間。
  • 說明 deployment 是否成功。
  • 做一次搜尋並顯示三筆簡短結果。
  • 詢問單純的 yes/no confirmation。
  • 顯示原本用純文字更好複製、引用與搜尋的內容。

第二份清單不會因為多了 card、gradient 與 loading spinner 就比較好用。普通 MCP tool 開發更快、測試更簡單,也能在更多 host 運作。

動態迷因(展開/收合)
Tool 只回一句話時,替它蓋一座迷你 dashboard 真的完全沒必要。 · 來源:GIPHY

先設計文字 fallback,再做 iframe

MCP Apps 是 optional extension。不同 host 的支援程度不一,穩定版規格要求先做 capability negotiation,不能假設每個 client 都能顯示 View。

它的 graceful degradation 規則很實用:帶 UI 的 tool 仍應回傳有意義的文字內容。Host 不支援 Apps 時,它就退回普通 tool,而不是只留下一句「請開啟 widget 查看結果」。

以訂單 dashboard 為例,result 可以包含:

{
  "content": [
    {
      "type": "text",
      "text": "42 orders, NT$183,400 total; 3 require review."
    }
  ],
  "structuredContent": {
    "count": 42,
    "total": 183400,
    "needsReview": 3,
    "orders": []
  }
}

文字讓 model 與不支援 UI 的 client 仍有可用答案;structured data 則給 View 一份穩定的輸入。兩邊都不需要把 prose 重新 parse 回 application state。

測試也會簡單很多:先驗證 tool contract,再拿已知 result 驗證 View。

Sandbox 限制破壞範圍,不代表陌生程式碼自動可信

MCP Apps 在 sandboxed iframe 裡執行。Host 會阻止 View 讀取 parent DOM、cookies 與 storage,通訊則經過可稽核的訊息。這是一條真的安全邊界,不只是宣傳用語。

但 server 送來的仍是可執行 HTML、CSS 與 JavaScript。規格的 security model要求 host 檢查預先宣告的 resource、驗證訊息並強制套用 CSP。

實作時可以從這些預設開始:

  1. connectDomainsresourceDomains 只宣告必要來源;沒寫的 domain 維持封鎖。
  2. Camera、microphone、geolocation 與 clipboard permission 只在功能真的需要時申請。
  3. Credentials 與授權決策留在 server。
  4. 每個 View 發起的 tool call 都當成不可信輸入。
  5. Destructive action 必須能被 host 看見並要求同意。

規格也能透過 UI visibility 定義 app-only tool。例如 refresh 或 local form submit helper 可以只讓 View 呼叫,不必塞滿 model 的 tool list。不過它仍不能繞過 server authorization。

權威 state 留在 server

2026 年 1 月的穩定版規格仍把 state persistence 與 restoration 列為未來功能;不同 client 提供的 App capabilities 也不完全相同。近期 Reddit 實作討論裡,開發者就提到把 state 存在 server,再同步回 iframe,以避開 host 行為差異。

因此可以採用一條很無聊、但可靠的規則:View 可以暫存互動 state,server 才擁有可復原的 state。

如果關閉對話再打開就會遺失重要決策,那份決策從一開始就不該只住在 iframe memory。透過 tool 保存 domain change,再讓 View 從新 result 重建畫面。

這條邊界也讓同一個 MCP server 能服務 text-only client、未來 host 與獨立 web interface。UI 可以替換,business state 不行。

我會照這個順序做

要替既有 MCP server 加入 Apps,我的最小路徑是:

  1. 先讓底層 MCP tool 沒有 View 也實用。
  2. 找出一個目前需要多輪 prompt,或會失去視覺脈絡的互動。
  3. 同時回傳簡潔文字與穩定 structured data。
  4. 只為這個互動加入一份 ui:// resource。
  5. 縮小 View-side call;純 UI helper 適合時隱藏於 model。
  6. 宣告最小 CSP 與 permission set。
  7. 同時測試支援的 host 與 text-only fallback。
  8. 測試 keyboard、窄 container、loading、empty 與 error state。
  9. 像普通 API 一樣記錄並檢查每個 View 發起的 mutation。

不要從「把整個產品改成 MCP App」開始。一個 tool 加上一個高摩擦 workflow,已足夠判斷 embedded UI 值不值得長期維護。

六月一篇討論 MCP Apps 為何還沒像 MCP 與 Skills 一樣普及的 Reddit thread 正好呈現兩面:有人在圖表與確認流程得到好結果,也有人遇到不同 surface 行為不一致。這不是否定標準的理由,而是保留 fallback、測試使用者真正採用 host 的理由。

結論:增加一個 View,不要增加第二份真相

MCP Apps 解決了真的介面問題。純文字不適合直接操作;要使用者用一輪又一輪 prompt 代替篩選表格、審查文件或編輯視覺物件,也很彆扭。

它最好用的時候,邊界反而很無聊:

  • Tool 執行並驗證。
  • View 呈現並收集互動。
  • Host 隔離、仲介並取得同意。
  • Server 保留權威 state。
  • Text fallback 持續可用。

互動本身有意義時,用 MCP Apps。只要一句話就能完成工作時,讓普通 tool 繼續普通。


外部參考資料