MCP GUI Bridge Protocol
Version 0.1.0 · 状态:Proposed · 基于 MCP 构建
一个应用层协议,通过 MCP 将多窗口图形应用的 UI 状态与操作暴露给 AI Agent。Agent 可以读取所有窗口的数据、操作当前可操作的窗口(包括模态对话框),并在窗口与视图之间导航。
规范性约定
MUST(必须)= 强制要求 · SHOULD(应该/建议)= 强烈推荐,偏离时需要给出理由 · MAY(可以)= 可选。
代码标识符始终保持其原始英文形式。JSON 示例中的 // 注释仅用于解释说明,不是消息的一部分。
1 · 概述
1.1 目的
在 GUI 应用与任意 MCP Agent 之间建立一套标准接口,使 Agent 能够像人一样"理解并操作 UI",同时与正在使用该应用的人类用户安全地并发协作。
1.2 角色
| 角色 | 承担者 | 职责 |
|---|---|---|
| Server | GUI 应用本身(或其内嵌组件) | 暴露资源(读)与工具(写),并推送通知 |
| Client | Agent 一侧(例如 Claude) | 读取数据,调用操作 |
| 用户 | 人类 | 与 Agent 并发地操作同一个 UI(见 §7.1、§10.1) |
交互模型:用户与 Agent 对话 → Agent 通过本协议读写 GUI。用户不会直接调用本协议。
1.3 设计原则
- 全局读,栈顶写 —— 资源暴露所有窗口;工具只暴露当前可操作窗口(每条模态链的栈顶)的操作。
- 读写粒度解耦 —— 写一侧止于窗口级(不下钻到控件);读一侧可以细到语义状态,但只描述"某个东西处于什么状态",而不描述"它能如何被操作"。读得细只服务于写的决策。
- 结论集中,拓扑只存一处 —— 可操作性由 Server 计算成唯一权威结论
operableWindowIds;拓扑只存ownerId,不存冗余的派生信息。 - 正确性由运行时校验兜底 —— 动态可见性是"尽力而为"的优化;最终正确性由写操作的运行时校验保证(见 §7)。
- 为 Agent 的可理解性而设计 —— 暴露"LLM 能理解的语义"(
summary+ 语义blocks+ 人类可读的title/label),而不是控件树 / 坐标 / 截图。Server 把 UI 翻译成语义的质量,直接决定 Agent 能否理解它(见 §4.3)。
2 · 术语与约定
窗口(window) —— 一个顶层窗口或一个模态对话框。它是无类型的,可以被打开多次;每个实例有唯一的 id。窗口是最小的可操作单元;本协议不下钻到控件。
窗口的三种状态(由 modal + ownerId 决定):
modal | ownerId | 含义 |
|---|---|---|
false | null | 普通顶层窗口 |
true | 非空 | 窗口级模态(挂在某个父窗口之下) |
true | null | 应用级模态(属于整个应用,没有父窗口) |
模态链(modal chain) —— 窗口级模态沿 ownerId 形成的链(例如 B → D1 → D2)。需要时,Client MUST(必须)自行从 windows[].ownerId 重建它。
栈顶(top) —— 一条模态链上当前唯一可操作的窗口(链的尾端)。
可操作集合(operable set) —— 当不存在应用级模态时,它是 { 每条模态链的栈顶 } ∪ { 非模态的顶层窗口 };当存在应用级模态时,它收缩为 { 应用级模态的栈顶 }(见 §10.2)。最终结论是 operableWindowIds。
Window A (no modal) → operable
Window B → modal D1 → modal D2 → only D2 is operable (B, D1 are blocked)
Window C (no modal) → operable
operable set = { A, D2, C }3 · 传输与初始化
- 传输:任意 MCP 传输(本地应用 SHOULD(应该)使用 stdio)。
- 在
initialize的结果中,Server MUST(必须)声明以下能力:
{ "capabilities": {
"resources": { "subscribe": true },
"tools": { "listChanged": true } } }Server SHOULD 在 instructions 中提供一段概览,说明这唯一一个资源的用途、"操作全部是窗口级的,且列表中只出现可操作窗口的操作",以及"用户可能并发操作,因此写操作应携带 expectedVersion"。完整的推荐文本见 §8.1。
4 · 资源(状态)
4.1 唯一的资源
本协议只有一个资源:app://windows(可以被 subscribe)。它内联了一切 —— 拓扑、可操作性结论,以及每个窗口的内容(summary + blocks)。一次 read 即得到完整快照;任何变化只发出它自己的 updated,Client 重新读取全部内容。在本地 IPC 下数据量不是瓶颈,所以不拆分、不分页、不增量下发(超大数据的处理见 §9)。
4.2 app://windows
它同时提供拓扑、可操作性结论以及每个窗口的内容;一次读取即得到自洽的快照。
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
operableWindowIds | string[] | 是 | 唯一权威结论:此刻可操作的窗口 id。模态链与应用级收缩都已计入其中 |
windows | Window[] | 是 | 所有窗口,每个都内联了自己的内容(见下面的 Window 对象);数组顺序没有语义(它不表示层叠 / z-order,不要依赖顺序) |
Window 对象
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
id | string | 是 | 窗口唯一标识,永不复用(§10.3) |
title | string | 是 | 窗口标题(人类可读) |
modal | boolean | 是 | 是否为模态 |
ownerId | string | null | 是 | 父窗口 id(窗口级模态挂在它之下);null 表示没有父窗口,它与 modal 一起决定窗口的三种状态(见 §2)。用途:Agent 可以沿每个窗口的 ownerId 重建模态链,理解模态的归属层级(哪个对话框属于哪个窗口);可操作性本身由 operableWindowIds 直接给出,不需要从它推断 |
version | 单调递增 int64 | 是 | 该窗口的乐观并发版本号;只要 blocks 或 summary 中任一发生变化,它就递增(见 §7.1) |
summary | string | 建议 | 一句话描述"这个窗口当前正在展示什么" |
blocks | Block[] | 是 | 窗口内容 —— 一个语义内容块序列(见 §4.4);空窗口用 [] |
Client MUST 把 operableWindowIds 当作可操作性的唯一依据。
{
"operableWindowIds": ["w-A"],
"windows": [
{ "id": "w-A", "title": "Order Editor", "modal": false, "ownerId": null,
"version": 42, "summary": "Editing order #12345, recipient not yet filled in",
"blocks": [
{ "type": "fields", "id": "b2", "items": [
{ "label": "Recipient", "value": "" }, { "label": "Amount", "value": "$100.00" } ] },
{ "type": "notice", "id": "b4", "severity": "error", "text": "Recipient cannot be empty" }
] }
]
}4.3 可理解性(让 Agent 易于读懂)
本协议暴露的是"LLM 能理解的语义",而不是控件树 / 坐标 / 截图 —— 这是它与传统 GUI 自动化的根本区别。Agent 能否理解 UI,一半取决于 Server 的"翻译质量"。Server SHOULD(应该):
- 写出真实的
summary:用一句话、人类可读地概述"这个窗口在做什么、它的关键状态是什么"。不要留空,不要塞满机器码 —— 它是 Agent 理解该窗口的入口。 - 优先使用语义原语:如果某个东西能用
fields/table/notice/list表达,就不要用custom;custom是兜底手段,过度使用它会让 Agent 面对一个不透明的 payload。 - 人类可读的 label/title:
fields的label、窗口title以及工具的title/description,都要使用用户能懂的词 —— 不要用内部代号(例如btn_47)。 - 让状态显式化:用
notice(携带severity)明确陈述错误/警告;不要指望 Agent 从字里行间猜测。
一个反例:空的 summary + 全部塞进 custom 的 blocks + 全是代号的 label → Agent 一头雾水。语义化的输入是 LLM 的"母语",远优于无障碍树(accessibility tree)或截图。
4.4 语义内容块(Block)
blocks 是一个扁平数组(不嵌套)。每个 Block 至少包含 type(必需)与 id(建议提供,用于引用)。blocks 由与业务无关的通用原语组装而成,不限制窗口的种类;一个 Block 只描述"展示了什么",而不得携带任何操作。
核心原语(实现 MUST 支持它们):
| type | 用途 | 专属字段 |
|---|---|---|
text | 段落文本 | text: string |
fields | 键值字段组 | items: { label, value, sensitive? }[] |
table | 表格 | columns: string[]、rows: any[][] |
list | 列表 | items: any[]、ordered? |
notice | 状态提示 | severity: info|warn|error|success、text |
media | 图片 / 截图 / 图表 | mimeType: string、url: string、alt? |
custom | 任意结构的兜底 | payload: any |
text · 段落文本
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
text | string | 是 | 要展示的纯文本内容。 |
fields · 键值字段组(表单、属性面板)
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
items | array | 是 | 字段项列表;每项的字段见下。 |
items[].label | string | 是 | 字段名 / 标签,例如"收件人"或"金额"。 |
items[].value | any | 是 | 该字段的当前值;空字符串或 null 表示未填写。 |
items[].sensitive | boolean | 否 | 标记为 true 表示该值已被脱敏(见 §4.5)。 |
table · 表格
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
columns | string[] | 是 | 表头。每个元素是一列的标题,数组顺序即列的从左到右顺序。示例:["Product","Quantity"]。 |
rows | any[][] | 是 | 行数据。每个元素是一行,行内数组与 columns 按位置一一对应 —— rows[i][j] 是第 i 行第 j 列(columns[j])的值。示例:[["A",2],["B",1]] 配合上面的列,表示"产品 A 数量 2,产品 B 数量 1"。 |
list · 列表
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
items | any[] | 是 | 列表项,每项一个值(通常是字符串)。 |
ordered | boolean | 否 | true = 有序(1. 2. 3.);省略或 false = 无序。 |
notice · 状态 / 提示
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
severity | enum | 是 | 提示级别,取 info/warn/error/success 之一,它决定语气与颜色。 |
text | string | 是 | 提示文本,例如"收件人不能为空"。 |
media · 图片 / 截图 / 图表(读一侧的富媒体)
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
mimeType | string | 是 | 媒体 MIME 类型,例如 image/png。 |
url | string | 是 | 从哪里获取:一个 https:// / file:// URL,或一个内联的 data:<mime>;base64,… URL。 |
alt | string | 建议 | 文字描述 —— 语义兜底,使非视觉的 Agent 仍能理解该媒体展示了什么。 |
custom · 兜底
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
payload | any | 是 | 任意结构。当内容无法用上面的原语表达时使用它;Agent 结合窗口的 summary 与上下文来理解它。 |
custom是一个安全阀:无法归类的内容先放在这里;当某一类反复出现时,SHOULD 把它提升为一个正式原语。- 大数据走工具:
state只保存概览 / 重要数据;一个窗口的完整或超大数据不进入blocks,而是通过该窗口的工具按需获取(见 §9)。
候选原语(本版本不要求):section(分区嵌套)、progress(进度)。
{
"version": 42,
"summary": "Editing order #12345, 3 fields still to be filled in",
"blocks": [
{ "type": "text", "id": "b1", "text": "Editing order #12345" },
{ "type": "fields", "id": "b2", "items": [
{ "label": "Recipient", "value": "" },
{ "label": "Amount", "value": "$100.00" },
{ "label": "Payment password", "value": "••••••", "sensitive": true } ] },
{ "type": "table", "id": "b3", "columns": ["Product","Quantity"], "rows": [["A",2],["B",1]] },
{ "type": "notice", "id": "b4", "severity": "error", "text": "Recipient cannot be empty" }
]
}4.5 敏感数据
任何字段或 Block 的值都 MAY(可以)被标记为 sensitive: true。Server MUST 在暴露之前对其脱敏(例如密码显示为 ••••••),使明文密码 / token 不进入 Client 的上下文。
5 · 工具(动作)
5.1 模型
工具是由 Agent 主动触发的动作 —— 它的效果可能是修改(提交)或导航(聚焦),也可能是只读的(例如触发重新加载的刷新);判定标准不是"读还是写",而是"是否为一次主动的动作"(见 §5.6)。
- 操作粒度 = 窗口。 每个可操作窗口暴露一组彼此隔离的独立工具;不同窗口的同名操作 MAY(可以)拥有完全不同的参数与语义。
- 可见即可用:任一时刻
tools/listMUST(必须)只包含此刻可调用的操作。两个层级的隐藏:(1) 窗口被模态阻塞 → 整组隐藏;(2) 某个窗口级操作此刻不可用 → 仅隐藏该操作。 - 任何隐藏 / 恢复都 MUST 触发
tools/list_changed。
5.2 命名
工具的 name MUST 采用 {winId}__{action} 形式。name 只是机器标识;Server SHOULD(应该)提供人类可读的 title 与 description。只允许 [A-Za-z0-9_-];否则需维护一份稳定的句柄映射(§10.4)。
{
"name": "w-A__edit_field",
"title": "Edit field · Order Editor (window w-A)",
"description": "Edit a field in the \"Order Editor\" window",
"inputSchema": {
"type": "object",
"properties": {
"field": { "type": "string" },
"value": {},
"expectedVersion": { "type": "integer", "description": "The state.version it is based on" }
},
"required": ["field", "value"]
}
}5.3 没有保留操作
本协议不保留任何操作名。每一个操作 —— 包括关闭窗口、置顶、切换视图、打开新窗口这类常见操作 —— 都是应用特定的,并作为普通的窗口级 / 应用级动作暴露。不存在被特殊对待的动作;一个实现精确地定义它想要的动作,每个动作有自己的名称、参数与语义。
常见的约定性动作(普通动作,命名由实现自行决定):关闭窗口 w-A__close(关闭一个模态会把栈顶交还给上一层)、把窗口置顶 w-A__focus、切换视图 w-A__switch_tab、打开新窗口 app__new_report —— 最后一个在返回值中返回新窗口的 id(见 §5.4)。当实现提供了聚焦类动作时,Client MAY 在操作某个窗口之前先调用它,以保持人与机器看向同一处。
5.4 返回值与失败
GUI 操作常常触发一条异步的时间线。工具 MUST 只在操作已经生效之后才返回,并 SHOULD 包含最终结果以及后续的 UI 提示。成功时返回以下字段 —— 正常返回本身就表示成功,没有 ok 布尔标志:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
message | string | 建议 | 结果的人类可读描述,例如"已提交;弹出了一个确认对话框" |
openedWindowIds | string[] | 否 | 本次操作打开 / 弹出的新窗口 id(可能是 0 个、1 个或若干个);它们是否为模态由 windows[].modal 给出。没有时省略或使用空数组 |
result | object | 否 | 工具特定的结构化业务结果(例如 { balance, currency });其结构由该工具的 outputSchema 定义,协议不约束其内容 |
失败一律走 isError:任何失败(被模态阻塞、校验失败、版本冲突、窗口不存在……)都通过 MCP 工具结果的 isError:true + { code, message, … } 返回(见 §7.4);不使用返回值中的布尔标志来表达失败。
直接结果放在返回值里;广泛的副作用(其他窗口也发生了变化)依靠资源通知,不塞进返回值。
如何承载:成功字段与失败的错误对象都通过 MCP 的 structuredContent 返回(Server 为该工具声明一个 outputSchema);Agent 做决策时只读取 structuredContent,不解析 content 的自然语言(content 是否携带一份文本镜像由实现决定,见 §12.3,即使携带 Agent 也不依赖它)。至于富数据的分工:富媒体(例如窗口截图)是状态的一个内容块(media 原语,在读一侧);超大结构化数据通过该窗口的工具按需获取(§9)—— 两者都与"通过资源读概览,通过工具取大数据"一致。
5.5 乐观并发
写操作 SHOULD 接受可选参数 expectedVersion —— 即该工具所属窗口(名称中的 winId)当前的 state.version。冲突处理见 §7.1。
5.6 工具语义标注
Server SHOULD 为每个工具附上 MCP 的标准标注,以帮助 Agent 判断"是否需要谨慎、失败后是否可以重试":
| annotation | 含义 | Agent 用途 |
|---|---|---|
readOnlyHint | 只读,不改变任何状态 | 可以自由调用,无需顾虑副作用 |
destructiveHint | 破坏性 / 不可逆(删除、扣款并提交) | 需谨慎;未来可能加入确认(见 §10.6) |
idempotentHint | 重复调用等价于一次 | 结合 stale_state 判断是否可以安全重试(见 §7.1) |
openWorldHint | 与外部世界交互 | 结果可能是不确定的 |
重点是正确标记这两个:destructiveHint(危险操作)与 idempotentHint(重试安全)。
资源即状态,工具即动作
区分标准不是"读还是写",而是"是否为 Agent 主动触发的动作"。可寻址、可订阅的静态状态(存在哪些窗口、某个窗口展示了什么)用资源;只有在 Agent 主动触发时才发生的事情用工具 —— 包括 refresh 或重新获取这类只读动作 —— 并用上面的标注标明其性质。
6 · 通知
Server MUST(必须)只在状态已完全生效之后才发送通知。事件到通知的映射:
| 事件 | 通知 |
|---|---|
| 打开 / 关闭窗口,模态弹出 / 关闭 | app://windows updated;如果可操作集合发生变化 → 额外发送一次 tools/list_changed |
| 窗口内的数据变化 | app://windows updated(该窗口 version++) |
| 某个窗口级操作的可用性变化 | tools/list_changed |
只有两种通知
resources/updated = 唯一资源 app://windows 的内容变了 —— 拓扑 / 可操作性 / 窗口内容,任何变化都走它。
tools/list_changed = 可操作窗口的集合,或某个窗口的可用操作,变了。
收到其中任何一个 → 重新读取 app://windows。
7 · 一致性与错误
7.1 先读后写的一致性(乐观并发)
Agent 与用户(以及其他 Agent)共享同一个 UI,是并发操作方之一。为了防止"基于过期理解的写入":
- 每个窗口携带一个
version,只要它的blocks/summary变化就递增。 - 当写操作携带
expectedVersion时,若当前version不相等,Server MUST(必须)拒绝它,并返回stale_state(包含currentVersion)。 - Client SHOULD(应该)据此重新读取、重新决策,然后重试。默认的冲突仲裁是用户优先。
7.2 运行时校验
动态隐藏是"尽力而为"的优化;由于通知是异步的,Client MAY(可以)仍然调用一个刚刚被阻塞 / 失效的操作。Server MUST 对这类调用执行运行时校验并返回结构化错误 —— 这是正确性的最后一道防线。
7.3 字段级校验错误
当因输入校验失败而被拒绝时,Server MUST 按字段返回错误,field 取对应的工具参数名:
{ "code": "validation_failed", "message": "Submission rejected: 2 fields are invalid",
"fields": [ { "field": "recipient", "error": "Cannot be empty" },
{ "field": "amount", "error": "Must be greater than 0" } ] }7.4 错误对象
工具错误通过 MCP 工具结果的 isError: true 返回,结构为 { code, message, ... }。
| code | 含义 | 额外字段 |
|---|---|---|
stale_state | expectedVersion 与当前值不匹配 | currentVersion |
validation_failed | 输入校验失败 | fields[] |
blocked_by_modal | 目标窗口被模态阻塞(窗口级或应用级) | — |
window_not_found | 窗口已关闭 / id 不存在 | — |
action_not_available | 该窗口级操作当前不可用 | — |
action_timeout | 操作超时且未生效 | — |
8 · 会话生命周期
8.1 初始握手
Server 在 initialize 中返回的 instructions 是 Agent 理解本协议的入口。它 SHOULD(应该)直接采用下面的推荐文本(应用特定的说明可以追加在末尾),以便所有 Agent 都收到一致且充分的指引:
推荐的 instructions 文本
你已连接到一个"多窗口桌面应用" —— 通过下面的接口读取它的 UI,并代表用户操作它。
读取 UI:唯一资源是 app://windows,一次读取即获得全部内容。operableWindowIds = 你此刻可以操作的窗口(唯一权威;只操作这个列表中的窗口);windows[] 的每一项包含 title/modal/ownerId,以及内容 summary(一句话概览)+ blocks(语义化的 UI 内容:文本、字段、表格、提示、媒体……)。先读 summary 理解窗口,再读 blocks 了解细节。
操作:一切都是窗口级工具,命名形如 {winId}__{action};列表中只包含"当前可操作窗口"的操作 —— 你能看到它,就能调用它。如果某个窗口暴露了聚焦类动作,在操作之前先调用它,让用户看到你正在处理哪一个窗口。
保持同步:当你收到 app://windows 的 updated 或工具列表变化通知时,在决策之前重新读取 app://windows。
并发写入:用户可能在同时操作 —— 你不是唯一的操作方。给写操作附上 expectedVersion(你读到的窗口 version);如果返回了 stale_state,说明 UI 已被改动,请重新读取后再决策。
失败:通过 isError 返回,携带一个 code(stale_state/validation_failed/blocked_by_modal……);validation_failed 会逐字段说明问题所在,据此修正后重试。
大数据:窗口的 state 只保存概览;当你需要完整 / 大量数据时,调用该窗口的对应工具来获取(结果在返回的 result 中)。
8.2 断连后的重新同步
app://windows 快照是自包含的,因此重新同步不需要事件重放 —— 重新读取这唯一一个资源就足以重新对齐。
8.3 通知的顺序与原子性
- Server MUST(必须)只在状态已完全生效之后才发送通知。
- 一切(拓扑 / 可操作性 / 窗口内容)都存放在唯一资源
app://windows中,一次读取即得到自洽的快照。 - 收到通知后,Agent 重新读取
app://windows以获得一致的视图,而无需自己推断变化的类型。
9 · 大数据的处理
在本地 IPC 下,在 app://windows 中完整传输普通量级的数据完全没有压力(见 §4),所以本协议不分页、不增量。
对真正超大的数据(例如数万行的表格)该怎么办:不要把它塞进 state。state 只保存概览 / 重要数据(例如前几行 + 一句关于总数的说明);完整数据通过该窗口的工具按需获取 —— 当 Agent 需要它时,调用形如 {winId}__load_… 的动作,大数据就在返回的 result 中(该工具标记为 readOnlyHint,见 §5.6)。
这样 state 就永久保持轻量(全量重传毫无负担),而大数据只在 Agent 主动索取时经由工具传输一次,不进入订阅推送。一句话概括:通过资源读概览,通过工具拉取大数据。
10 · 多会话与实现约定
10.1 多 Client / 多会话
- UI 状态是全局共享的 —— 窗口是真实的、全局唯一的桌面窗口;不存在每个 Client 各自独立的副本。
- 所有 Agent 与用户并发地操作同一组窗口。多个 Agent 只是复用 §7.1 的乐观并发,不引入新机制。
- Server MUST(必须)串行化操作的执行(一次生效一个);通知 MUST 广播给所有连接;订阅关系按连接各自独立。
- 一个 GUI 进程 = 一个 Server 实例,它可以同时服务多个 Client。
10.2 应用级模态
- 定义:一个阻塞整个应用且没有父窗口的模态,在索引中表现为
modal:true+ownerId:null。 - 效果:当存在这样的模态时,可操作性优先收缩 ——
operableWindowIds只保留应用级模态的栈顶,其他一切都不可操作。它们可以嵌套。 - Server 负责把这种收缩计算进
operableWindowIds;协议不需要为它增加任何额外字段。
10.3–10.6 其他约定
| 约定 | 要求 |
|---|---|
| 标识符稳定性 | 窗口 id 全局唯一,block.id 在窗口内唯一;两者都 MUST 稳定且永不复用(单调递增 / UUID) |
| 名称安全字符 | 工具名 {winId}__{action} 只允许 [A-Za-z0-9_-];当 winId/action 含有非法字符时,MUST 维护一份稳定的句柄映射 |
| 数值宽度 | 每个窗口的 version MUST 是 64 位单调递增整数,不做回绕处理 |
| 本版本明确不在范围内 | 权限门控(操作确认 / 授权)、控件级操作(仅窗口级) |
11 · 合规性
一个合规的 Server MUST(必须):
- 声明 §3 列出的能力;
- 提供唯一资源
app://windows(内联窗口内容),字段满足 §4; - 让
operableWindowIds成为可操作性的唯一权威结论,正确反映窗口级 / 应用级模态; - 让
tools/list在任一时刻只包含可操作窗口的可用操作,并在变化时发出tools/list_changed; - 让工具名遵循
{winId}__{action}且字符合规; - 对携带
expectedVersion的冲突写入返回stale_state;对被阻塞 / 已失效的写入执行运行时校验; - 只在状态已生效之后发送通知,并按 §6 映射;
- 串行化操作的执行,并把通知广播给所有连接;
- 在暴露之前对
sensitive值脱敏; - 确保
id/version满足 §10.3 / §10.5。
一个合规的 Client SHOULD(应该):
- 把
operableWindowIds当作可操作性的唯一依据; - 给写操作携带
expectedVersion,并正确处理 §7.4 的错误码; - 收到通知后在决策之前重新读取
app://windows,而不是依据单条中途的通知就行动; - 当实现提供了聚焦类动作时,在操作某个窗口之前先调用它。
12 · 实现映射(到 MCP 方法)
本节把前面的数据模型落到 MCP 的具体方法上,以消除实现上的歧义 —— 只有据此实现的 Server 才能相互兼容。
12.1 发现资源
| MCP 方法 | 返回 |
|---|---|
resources/list | 只列出一个:app://windows(固定,可 subscribe)。不存在动态增删,因此不需要 resources/list_changed |
12.2 resources/read 的返回包装
每次 resources/read 都把对应的 JSON 作为单个 application/json 文本内容返回:
{
"contents": [
{ "uri": "app://windows",
"mimeType": "application/json",
"text": "{ ...the app://windows JSON defined in §4.2, inlining all windows... }" }
]
}只有这一个资源、这一种包装(text 中保存 §4.2 定义的完整 JSON)。
12.3 tools/call 的返回包装
- 成功:
structuredContent= §5.4 的 ToolResult 对象;isError省略或为 false。 - 失败:
isError:true,并把 §7.4 的错误对象放进structuredContent。 content:本协议不依赖它;SHOULD(应该)放入一段文本(重复message或错误消息),以满足 MCP 关于"content 至少提供一些内容"的约定,但 Agent 做决策时只读取structuredContent。- 每一个有返回内容的工具都 MUST(必须)声明
outputSchema。
outputSchema 示例(协议的公共基础字段 + 该工具 result 的结构):
{
"type": "object",
"properties": {
"message": { "type": "string" },
"openedWindowIds": { "type": "array", "items": { "type": "string" } },
"result": {
"type": "object",
"properties": { "balance": { "type": "number" }, "currency": { "type": "string" } }
}
}
}12.4 winId 在所有接口中保持一致
winId 出现在 operableWindowIds、ownerId、openedWindowIds 以及工具名 {winId}__{action} 中 —— 它们必须是同一个值,这样 Agent 才能跨字段与工具做关联(给定一个 id → 匹配工具、查找窗口)。
- 对外的
winIdMUST 是名称安全的([A-Za-z0-9_-]),并在所有接口中保持一致。 - 当内部窗口 id 不满足这一点时(含空格 / 中文字符等),Server MUST 全程使用一个稳定的映射句柄作为对外的
winId—— 不是只在工具名里做映射;每一个 id 字段与工具名都使用同一个句柄。
12.5 杂项约定
- 关闭一个窗口 → 只有
app://windows的内容发生变化(该窗口从windows[]中移除),发出updated;它不涉及资源的增删。 - 空集合是合法的:
operableWindowIds为空(全部被应用级模态阻塞)与tools/list为空,都是正常状态。
附录 A · 完整示例
场景:窗口 A(Order Editor)与 C(Report Details)已打开且可操作;A 提交之后,弹出确认模态 D1。
附录 B · 类型参考
资源 · 被读取的状态
Windows —— app://windows
| 字段 | 类型 | 备注 |
|---|---|---|
operableWindowIds | string[] | 可操作窗口 · 唯一权威结论 |
windows | Window[] | 所有窗口 · 内联内容 · 顺序无语义 |
Window —— windows[] 元素
| 字段 | 类型 | 备注 |
|---|---|---|
id | string | 全局唯一 · 永不复用 |
title | string | 窗口标题 |
modal | boolean | 是否为模态 |
ownerId | string | null | 父窗口;null = 顶层 / 应用级模态 |
version | int64 | 内容版本 · 写一致性 |
summary | string? | 一句话描述 |
blocks | Block[] | 内联内容 · 语义内容块序列 |
Block —— 七者之一 · 都包含 type + id?
| type | 结构 |
|---|---|
text | { text } |
fields | { items: { label, value, sensitive? }[] } |
table | { columns: string[], rows: any[][], offset?, total? } |
list | { items: any[], ordered?, offset?, total? } |
notice | { severity: "info"|"warn"|"error"|"success", text } |
media | { mimeType: string, url: string, alt? } |
custom | { payload: any } |
对 table / list:截断时 total 是必需的;offset 默认为 0。
工具 · 被发起的动作
调用参数 —— 除每个工具自身的参数之外
| 字段 | 类型 | 备注 |
|---|---|---|
expectedVersion | int? | 所属窗口的 state.version · 乐观并发 |
ToolResult —— 成功 · 没有 ok 标志
| 字段 | 类型 | 备注 |
|---|---|---|
message | string? | 人类可读的结果 |
openedWindowIds | string[] | 打开 / 弹出的新窗口 · 可能是若干个 |
result | object? | 工具特定的业务结果 · 结构由 outputSchema 定义 |
ToolError —— isError = true
| 字段 | 类型 | 备注 |
|---|---|---|
code | string | 错误码 · 见下 |
message | string | 人类可读 |
currentVersion | int? | 当 code = stale_state 时 |
fields | {field,error}[]? | 当 code = validation_failed 时 |
code ∈ stale_state · validation_failed · blocked_by_modal · window_not_found · action_not_available · action_timeout