部落格技術

手機版 ChatGPT 中的 MCP App:我們的實測心得

我們在 iPhone 模擬器中用 ChatGPT 手機網頁版執行樂譜面板:它的安全區域邊距包含什麼、重建面板時會發生什麼事,以及我們現在如何不用手機就測試這兩件事。

先說結論

在手機上,ChatGPT 會把標題列和輸入框疊在全螢幕的 MCP App 上方,並以安全區域邊距的形式回報,我們實測分別約為 47 px 和 127 px;應用程式開啟期間,底部邊距還會變動。每次進入或離開全螢幕,它也會用對話中最初的工具結果重建我們的面板。因應方式是:把邊距區域留空,但不要再加自己的餘量;每次上下文變化時都重新定位用腳本定位的元素;載入時檢查結果是否已經過時。我們測的是 iPhone 模擬器中的 ChatGPT 手機網頁版,不是原生 App。

在手機版 ChatGPT 中開啟我們的 MCP App,出了哪些問題?

四個問題,都和全螢幕有關。ScoreStarling 的面板會在對話中顯示樂譜(連線方式);全螢幕時可以點選音符,再從選取卡片就這些音符向對話提問,在手機上這張卡片會停靠在播放器上方。2026年10月7日,我們在 iOS 模擬器的 iPhone 17(402 × 874 點)上,用 Safari 開啟 ChatGPT 手機網頁版來執行它。面板所在的 iframe 是跨來源的,所以我們只能從螢幕截圖判讀它的狀態。

在 ChatGPT 手機網頁版上發現的問題(2026年10月7日)
現象原因修正
播放器浮到了螢幕約三分之一高的位置底部邊距(約 127 px)已涵蓋輸入框,我們又在上面固定多留 96 px取邊距和 96 px 中較大的那個
草稿變長或出現 Thinking 狀態列時,播放器往上移了,停靠的卡片卻沒動ChatGPT 改變了邊距;播放器靠 CSS 跟著移動,卡片卻由腳本定位每次上下文變化時重新定位卡片
編輯後進入或離開全螢幕,顯示的是舊樂譜,而且是唯讀ChatGPT 用最初的工具結果建立了新面板結果落後時讀取目前版本
在對話中編輯後,卡片懸在播放器上方約 40 px 處我們的「Updated from the chat」提示移動了卡片定位所參照的區域該區域尺寸改變時重新定位卡片

在手機上,全螢幕的 MCP App 為什麼會被 ChatGPT 的輸入框擋住?

因為 ChatGPT 把輸入框畫在你的 iframe 上方,並透過底部安全區域邊距告訴你它擋住了多少。忽略這個邊距,你的控制項就會被輸入框蓋住;像我們那樣在上面再加一段自己的餘量,控制項又會懸在半空。MCP Apps 規範對 safeAreaInsets 的描述只有一句「Safe area boundaries in pixels」(以像素為單位的安全區域邊界),OpenAI 的文件則說全螢幕時輸入框(composer)仍會疊在上方,但沒有給出尺寸(兩者皆於 2026年10月7日確認)。我們的 47 px 和 127 px 是從螢幕截圖估算的。

10月6日,手機上的一次對話顯示我們的播放器被 ChatGPT 的標題列擋住,於是我們把標頭列移到頂部邊距下方,把播放器移到底部;在底部,我們的 CSS 還在底部邊距之上另外為輸入框留了 96 px。本機用 34 px 邊距測試時看起來沒問題,換成 ChatGPT 的 127 + 96 就不對了。現在面板會取兩者中較大的那個,同時也讀取 ChatGPT 的 window.openai.safeArea:

// panel.js, simplified: the host's inset or 96 px, never both
const bottom = Math.max(context.safeAreaInsets?.bottom || 0, window.openai?.safeArea?.insets?.bottom || 0);
root.style.setProperty('--safe-bottom', `${bottom}px`);
root.style.setProperty('--composer-space', fullscreen ? `${Math.max(0, 96 - bottom)}px` : '0px');
/* panel.css */
.stage > .player { bottom: calc(10px + var(--safe-bottom) + var(--composer-space)); }
我們的面板在 402 × 874 點的手機上(大致依比例),上方是 ChatGPT 的標題列(約 47 px),下方是輸入框(約 127 px)。修正前,面板在邊距之上又多留了 96 px;現在只留出邊距,宿主回報的邊距小於 96 px 時才留 96 px。

手機上全螢幕的 MCP App 開啟期間,會有哪些變化?

邊距會變。我們實測時,草稿增加到兩行,或 Thinking 狀態列取代輸入框時,底部邊緣都會移動。規範允許宿主在任何上下文欄位變化時送出 ui/notifications/host-context-changed,只帶有變化的欄位,由畫面自行合併。

我們的播放器位置綁定在一個 CSS 變數上,跟著變了。選取卡片由腳本定位,留在原地沒動,所以現在每次上下文變化都會重新定位它。我們自己的提示又造成第二種偏移:「Updated from the chat」會在樂譜上方顯示 20 秒,把樂譜區域往下推。卡片的位置是從這個區域的頂端算起,於是滑到了播放器上面;如果這段期間有其他東西重新定位了它,提示消失後它又會懸在播放器上方。現在我們在這個區域加了一個 ResizeObserver,同樣會重新定位卡片,每個動畫影格最多一次:

this.context = {...this.context, ...params};  // host.js, simplified: merge the partial update
function hostContext(next) { applyContext(next); editor.queuePlace(); }
new ResizeObserver(() => { updateClip(); editor.queuePlace(); }).observe($('stage'));

離開全螢幕後,MCP App 為什麼顯示的是舊版本?

因為手機上的 ChatGPT 每次進入或離開全螢幕都會建立新的面板,交給它的是對話中最初的工具結果,也就是在任何編輯之前產生的那一份。規範允許宿主隨時銷毀畫面;而工具結果只記錄了工具執行那一刻的狀態。

我們的工具結果一半是快照、一半是即時資料:頁面圖片和音符對應它自己的修訂版本,另有一個「目前修訂版本」欄位,在面板讀取結果時才取得。重建後的面板畫出的是修訂版本 0,發現樂譜已經到了修訂版本 3,於是變成唯讀。它偵測對話編輯時,是拿伺服器的修訂版本和那個本來就是最新的欄位比較,所以永遠追不上。現在,如果宿主交給面板的結果已經落後於樂譜,面板會讀取一次目前的修訂版本;唯一的例外是結果中帶有等待核准的修改建議,這種結果本來就應該與目前版本不同。

不用手機,要怎麼測試 ChatGPT 在手機上的行為?

在仿真環境中模仿真實宿主,並證明每個測試在拿掉修正後都會失敗。我們用的是 sunpeak,它為 Playwright 測試仿製了 ChatGPT 和 Claude。它的手機版 ChatGPT 介面(0.20.91 版)會畫出標題列和輸入框,並把輸入框回報為 92 px 的邊距。我們為這幾個 bug 寫的三個測試在 402 × 874 的觸控螢幕上執行,補上了其餘部分:它們透過這個介面的沙箱 iframe(它和 ChatGPT 的 iframe 一樣,是面板的上層)依序送出 127、160、70 和 127 px 的新邊距;編輯之後重新載入面板,再把最初的工具結果交給它。另一個測試則像對話那樣,在面板之外編輯樂譜。播放器最後必須位於「邊距與 96 px 中較大者」上方 0–24 px 處,卡片必須位於播放器上方 0–16 px 處。

  1. 執行真實宿主在模擬器中開啟 ChatGPT 手機網頁版
  2. 修正一個原因部署、重新整理應用程式的工具,再看一次
  3. 模仿宿主從仿真環境的 iframe 送出宿主的訊息
  4. 拿掉修正少了這項修正,新測試就必須失敗
手機上的發現如何變成本機測試。
逐一拿掉各項修正的結果(2026年10月7日)
拿掉的修正失敗的檢查
取邊距或 96 px,不再兩者相加輸入框測試:「播放器緊貼輸入框」
上下文變化時重新定位卡片輸入框測試:「卡片不遮住播放器」
重建的面板讀取目前版本重建測試:復原按鈕一直無法使用
區域尺寸改變時重新定位卡片提示測試:「卡片停靠在播放器上」

這三個測試跑一次要 59.9 秒。同一天又加了第四個測試,檢查手機編輯器的單列標頭和播放器;我們的貢獻者規則現在要求,只要改動全螢幕版面、邊距、卡片或結果處理,就必須跑手機測試。這次手機實測還發現:對於「Change this note to C」(把這個音改成 C),ChatGPT 把選取的 B4 改成了低七度的 C4;我們在工具描述中加入「取最近八度」的規則後,用真實模型跑的本機測試從 3 次成功 2 次提升到 6 次全部成功。

還有哪些沒驗證

我們的手機測試都是在 iOS 模擬器和 ChatGPT 手機網頁版上進行:沒有用實機,也沒有測原生 App。尚待釐清的有:

  • ChatGPT 實際送出的邊距數值與時機;我們的數值是估算的,仿真環境沿用了這些估算;
  • Safari 展開的工具列,會擋住一半的輸入框和播放器的下緣;
  • 鍵盤收起後,Safari 留下約 156 點的頁面捲動,跨來源的面板無法把它復原;
  • 在 ChatGPT 還沒回答完時開啟全螢幕,會出現一條最高約 777 點的不透明色帶;
  • ChatGPT 的底部回覆面板,以及它自己銷毀 iframe 的行為,仿真環境都沒有模仿;
  • 電腦上的 ChatGPT:全螢幕是一個側邊面板,輸入框在面板之外;我們沒有讀取它的邊距。

手機版 MCP App 檢查清單

  • 上下兩個邊距都要留空;如果你還為輸入框保留了備用空間,取兩者中較大的那個,絕對不要相加。
  • 合併每一則 host-context-changed 通知,並重新定位所有用腳本定位的元素,每個影格最多一次。
  • 監看你自己的容器,留意會移動它們的提示。
  • 預期宿主會用舊的工具結果重建畫面;載入時比較結果的版本和伺服器上的版本。
  • 讓本機測試宿主使用在真實宿主上量到的邊距。
  • 依 OpenAI 文件的建議,把 UI 資源 URI 當作快取鍵;發布新的 URI 後,在 ChatGPT 中重新整理該應用程式的工具(在外掛程式設定中依序點選 Manage app、Refresh tools;2026年10月7日確認)。
  • 把在真實宿主上的每個發現都變成一個仿真測試,並確保少了修正時它會失敗。

參考資料

  1. SEP-1865:MCP Apps——MCP 的互動式使用者介面(穩定版,2026-01-26) — Model Context Protocol
  2. 為你的 MCP 伺服器加入 UI — OpenAI
  3. UI 設計指南 — OpenAI
  4. 參考文件(window.openai 元件橋接介面)— OpenAI
  5. MCP 測試框架 — sunpeak

常見問題

在手機上,ChatGPT 會把輸入框算進 safeAreaInsets 嗎?

我們實測是會的。在 ChatGPT 手機網頁版上(iPhone 17 模擬器,2026年10月7日),底部邊距約 127 px,涵蓋了輸入框;頂部邊距約 47 px,涵蓋了標題列。MCP Apps 規範沒有規定邊距包含什麼,所以把回報的區域留空就好,不要在上面再加自己的餘量。

全螢幕的應用程式開啟期間,ChatGPT 會改變邊距嗎?

我們實測會:草稿變成兩行,或出現 ChatGPT 的 Thinking 狀態列,都會讓底部邊緣移動。每次收到 ui/notifications/host-context-changed 都要套用新的邊距,把通知中帶的欄位合併進現有的值,並重新定位所有用腳本定位的元素。

為什麼在手機上切換到全螢幕後,MCP App 顯示的是舊資料?

手機上的 ChatGPT 每次進入或離開全螢幕,都會用對話中最初的工具結果建立新的面板,所以我們的面板顯示的是後續編輯之前的樂譜。畫面載入時,把結果中的版本和伺服器上的版本比較一下;如果結果落後了,就讀取一次目前的狀態。

不用手機,可以測試 ChatGPT 在手機上的版面嗎?

可以測一部分。我們用 Playwright 在 sunpeak 仿製的手機版 ChatGPT 介面中重現手機上看到的情況:邊距變化、面板重建、從對話發起的編輯;拿掉對應的修正,每個測試都會失敗。ChatGPT 實際送出的數值、Safari 的工具列、鍵盤、ChatGPT 的底部回覆面板和原生 App,仍然需要實機測試。

帶來任何音樂帶走一份樂譜