博客技术

手机版 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,仍然需要真机测试。

带来任何音乐带走一份乐谱