MCP Apps 的一個 MIME type,13 天改了三版:SEP-1865 從草案走到 stable
2025 年 11 月 21 日,一份剛開出來的規格草案裡同時住著兩種寫法。能力協商那段寫 text/vnd.mcp.ui+html,其他段落寫 text/html。ext-apps repo 當天的 commit 訊息講得很直白:規格早就在協商段定義了前者,卻在別處用了後者,所以全篇改成一致。
同一天又有一個 commit 把它換掉,這次換成 text/html+mcp,理由列了四條,頭一條是「這是未註冊的 subtype,私人使用是允許的」。
一個字串一天內改兩次,看起來像手滑。但留言串裡作者講過它的分量:這個 MIME type 是當識別字用的,拿來觸發 MCP Apps 協定。我的理解是,client 看到它才知道這份 HTML 不是普通網頁,要走 postMessage 那一套。暗號寫錯,兩邊就對不上。
這篇照時序走一遍 MCP Apps(規格編號 SEP-1865),主線是這個字串怎麼在 13 天內改到第三版,旁邊還有哪些東西在同一段時間被磨掉。我的立場先放在這裡:這份規格最有參考價值的地方,是每個讓步發生在誰身上、讓了多少,都留在公開紀錄裡。要採用的人可以直接照紀錄分辨哪些是底線、哪些可以商量。
草案之前,各家已經各做各的
MCP Apps 的前身是 MCP-UI。它的 repo 在 2025 年 5 月 13 日有了第一個 commit,5 月 16 日 v1.0.1 到 v1.1.0 全部在同一天發完。規格的 Motivation 段自己寫了脈絡:MCP-UI 已經有 Postman、HuggingFace、Shopify、Goose、ElevenLabs 這些採用者,而 OpenAI 的 Apps SDK 在 2025 年 11 月上線(確切日期我沒查,只引規格那一句),進一步證明對話介面裡的互動 UI 有需求。
麻煩在於兩邊並存。規格原文說,開發者得為不同 host 維護各自的實作或轉接層,例子就是 MCP-UI 對 Apps SDK。
SEP-1865 的 PR 在 2025 年 11 月 21 日 17:39(UTC)開出來,規格抬頭列了九位作者。它要做的事很具體:MCP server 用 ui:// 這種資源位址預先宣告介面,client 把它放進 iframe 渲染,兩邊走 MCP 自己的 JSON-RPC over postMessage 溝通。草案一落地,頭一件事就是上面那個字串。
留言串裡的那則批評
草案開出四天後的 2025 年 11 月 25 日,一位自稱是 IANA media type 審查者之一的 darrelmiller 在 PR 留言:
+mcp is not itself a format with any kind of syntax and semantics. It is a protocol.
他的論點,我用白話打個比方:+json、+xml 這種後綴,是在標示「底下是什麼格式」,像包裝盒上寫「內容物:玻璃」。+mcp 寫的卻比較像「處理流程:走 MCP」,是另一種資訊。他還加了一句,如果哪天想註冊這個 media type,「highly unlikely that the registration would be allowed」。他的建議是 text/html;profile=mcp,引的是 RFC 6906 對 profile 的定義:原本 media type 的語意與處理模型仍然適用。
作者 antonpk1 當天的回覆值得整段讀。他先承認:text/html+mcp 語意上不正確,選它是務實考量,因為 MIME type 在這裡只是觸發協定的識別字。然後他說 profile 寫法會增加解析複雜度,得把 text/html;profile=mcp、text/html; profile=mcp、text/html;profile="mcp"、text/html ; profile=mcp 這些合法排列通通正規化,只為了一個本質上是查表用的 key。
審查者再回,說自己「contractually bound」得講未註冊又結構無效的 media type 不該用,但也理解不想增加複雜度的顧慮。(他後面接著提了兩項緩解辦法,原文我這邊沒讀全,不引。)
結局在 12 月 4 日,ext-apps 的 PR #75 把字串改成 text/html;profile=mcp-app。commit 訊息引的就是 RFC 6838 對自訂 media type 的建議,結論跟審查者一致:MCP Apps 沒有定義新的語法,只是 HTML 的一種 profile。profile 的值則不是審查者提的 mcp,是同一串裡 glen-84 建議的 mcp-app,理由是「更具體」。從留言到改完 9 天,從草案到改完 13 天。
作者嫌的那個解析成本,後來規格怎麼處理,我沒讀到。stable 規格裡能看到的只有結果:client 在 initialize 時宣告 "mimeTypes": ["text/html;profile=mcp-app"],server 的 UI 資源 mimeType 則是 SHOULD 用同一個值。
MIME type 的三個版本
2025-11-21 第一版
text/vnd.mcp.ui+html。協商段已定義,其他段落卻用 text/html,同日 commit 統一。
2025-11-21 第二版
text/html+mcp。同一天稍晚改的,commit 訊息稱它是未註冊 subtype、私人使用允許。
2025-11-25 審查者留言
IANA 審查者指出 +mcp 不是格式後綴,建議改用 profile 參數。
2025-12-04 第三版
text/html;profile=mcp-app,PR #75 合併。
規格邊實作邊修
MIME type 吵得最兇,但不是唯一被磨掉的。同一個月的 commit 還有這些:
- 12 月 1 日,displayMode 拿掉
"carousel"。 - 同一天,theme 拿掉
"system",commit 的理由是 host 應該自己把 system 解成 light 或 dark 再交給 app,app 只需要知道最後結果。 - 12 月 10 日,PR #126「host may request user consent」。
最後這一條有出處可追。11 月 23 日有人在 SEP 留言質疑 ui/message:讓 server 在沒有使用者同意的情況下往對話裡塞任意訊息,是個有問題的模式。作者當時回應值得往 #ui-cwg 另開討論串(那個頻道裡講了什麼,我沒讀到)。到了 12 月 10 日的 commit,規格文字長成:「Host SHOULD add the message to the conversation context … Host MAY request user consent.」質疑沒有被整個接受,但規格留了一個口子。
小修之外,我更在意設計上那幾條「沒做」。規格的 Rationale 攤得很清楚:
- 介面資源要預先宣告,登記在 tool 的 metadata 裡,而不是像 MCP-UI 那樣隨 tool result 內嵌回傳。理由是 host 可以在 tool 執行前預取模板、也能先審查模板。內嵌寫起來對 server 開發者方便,規格說它因為效能優化的缺口與審查流程的困難,先擱置了。
- 外部 URL 不進 MVP,理由是 model 看不到內容、沒辦法截圖,審查也有困難。
- 「全域 API 物件」被否決,因為它需要 host 專屬的注入,遇到外部 iframe 來源就行不通。
- 溝通用 MCP 自己的 JSON-RPC,不另訂訊息格式。握手也跟著改成類似 MCP 的
ui/initialize、ui/notifications/initialized,取代 MCP-UI 的iframe-ready。
第一條在留言串裡沒吵完。2025 年 12 月 18 日有人質疑,所有 UI 頁面都得先定義成資源,不適合 AI 動態生成介面。另一位回:資源可以動態產生,即使 URI 是靜態的。2026 年 1 月 8 日又有人補一句,至少 ChatGPT 會快取這個資源,所以乾脆靜態也無妨。這個爭點在串裡我沒看到定論,只能照實說:規格選了預先宣告,動態生成能不能順手,兩邊各有一個說法。
安全那一側也有一筆。2026 年 1 月 7 日合併的 PR #208 把 PostMessageTransport 的 eventSource 參數改成必填,目的是強制驗證訊息來源,擋跨 app 的訊息注入,並補了 14 個安全測試。規格對 web 型 host 的要求也寫得硬:必須經由中介的 Sandbox proxy,host 與 sandbox 必須不同 origin,sandbox 必須有 allow-scripts 與 allow-same-origin。
44 天沒人理,然後合併
stable 在 2026 年 1 月 26 日發出:規格目錄 specification/2026-01-26,加上 SDK v1.0.0,v1.0.0 的 release 內文只有一行。那個目錄的 git 歷史只有一個 commit,就是這次發版。
兩天後的 1 月 28 日,SEP-1865 的 PR 才合併,距離開出來 68 天。中間有兩個小插曲:1 月 16 日 sep-automation-bot 留言提醒,被指派的 sponsor 已經 44 天沒有動作;1 月 23 日維護者 localden 問可不可以直接關掉這個 PR,因為更通用的 extension 定義另有 PR 在追。作者 idosal 回得很實際:合併可以保留這串討論累積的脈絡,而且大家都連到這個 PR,標成 Closed 會造成混淆。stable 與合併之間差兩天,哪一邊先做的決定,留言裡沒有說明。
比這更有意思的是 2026 年 2 月 12 日合併的 PR #435,距 stable 17 天,作者是 mstoiber-oai。內文大意是:ChatGPT 有自己的 sandbox 與 host 溝通協定,名稱和規格用 MUST 強制的那一套對不上,所以把那幾則來回訊息從 MUST 改成 SHOULD,並說這是 ChatGPT Apps SDK 從一開始的運作方式。diff 很乾淨:「Sandbox 必須送出 ui/notifications/sandbox-proxy-ready」變成 SHOULD,「Host 必須把原始 HTML 資源送進去」也變成 SHOULD。同一段的第 1、2 條,不同 origin 與 allow-scripts、allow-same-origin 兩項權限,維持 MUST。
規格的 Motivation 段把 MCP-UI 與 Apps SDK 當成兩個既有實作並列,stable 之後,Apps SDK 那一側(ChatGPT)又提了鬆綁。鬆的是訊息名稱,沒鬆的是隔離。
stable 不等於凍結
stable 之後 draft 繼續長。2026 年 4 月 21 日,規格加進「App 可以註冊自己的 tool」,PR 內文說這是 pull 式的,和既有 push 式的 ui/update-model-context 互補。9 月 8 日 ext-apps v2.0.0 出來,release 內文特別寫:wire protocol 沒變,2.x 的 View 能在 1.x 的 host 跑,反之亦然,還有測試拿已發布的 1.7.5 對這個版本雙向跑。會壞的是依賴與 TypeScript API:改用 MCP TypeScript SDK 2.0 的拆分套件、zod 4、Node.js 20 以上。
10 月 27 日又有一筆:PR #829 讓 SDK 在 App 註冊的 tool 與 WebMCP 之間雙向橋接,內文說「沒有新的 wire message、沒有 host 端 API 變更、沒有棄用」。草案頭部現在寫的是 Draft,stable 檔案寫的是 Stable,兩份已經分岔。
這個擴充的識別字是 io.modelcontextprotocol/ui,我在 9 月 9 日那篇《握手拆掉之後,你的 MCP client 怎麼判斷對面是新是舊》只用一句話帶過它屬於選配擴充。
這篇的範圍:我只讀了規格、ext-apps 的 commit 與 release notes、SEP 的留言串,沒有實際跑過任何 MCP Apps server 或 host。README 寫它能在 Claude、ChatGPT 與其他相容 client 裡內嵌渲染,各家實際支援到什麼程度,我沒驗證,官方 client-matrix 頁面也沒打開。
我會怎麼用這段紀錄
如果要做 host,我會把兩件事當底線:MIME type 就照 stable 的 text/html;profile=mcp-app,以及 sandbox 與 host 不同 origin、權限寫死那兩條。訊息名稱那幾條,已經有人拿到了 SHOULD,我會當可以商量。理由是這兩條在紀錄裡都有被挑戰過:MIME type 挨了審查者的批評、改到現在這一版,stable 寫的就是它;sandbox 的不同 origin 與權限,在 ChatGPT 要求鬆綁的那個 PR 裡還是留成 MUST。
什麼會讓我改變想法?第一,如果 draft 之後動到了 wire 層,而 stable 與 draft 的差距擴大到 host 得同時支援兩邊,那「照 stable 做」就不再安全。第二,如果我之後讀到實際 host 並沒有照 stable 的 MIME type 與 sandbox 條件在跑,那這份紀錄上的底線就只是紙上的底線。這兩件事我目前都沒有證據,所以現在只能說到這裡。
stable 的檔案停在 1 月 26 日,draft 的 commit 紀錄到 10 月 27 日還有新的。










