MCP GUI Bridge Protocol
版本 0.1.0 · 狀態:提案中 · 基於 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 | 非 null | 視窗層級模態(附著於某個父視窗之下) |
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 為空 + blocks 全部塞進 custom + label 全是代碼 → Agent 只能一片摸黑。語義化的輸入是 LLM 的「母語」,遠遠優於無障礙樹或螢幕擷圖。
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 在暴露之前將它遮蔽(例如密碼顯示為 ••••••),使明文的密碼/權杖不會進入 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 判斷「是否需要謹慎,以及失敗是否可以重試」:
| 標註 | 含義 | 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 文字
You are connected to a "multi-window desktop application" — read its UI and operate it on the user's behalf through the interface below.
Reading the UI: the single resource is app://windows, and one read gets everything. operableWindowIds = the windows you can operate at this moment (the sole authority; only operate the ones in this list); each of windows[] contains title/modal/ownerId, plus the content summary (a one-sentence overview) + blocks (semantic UI content: text, fields, tables, notices, media…). Read the summary first to understand the window, then the blocks for details.
Operating: everything is a window-level tool, named like {winId}__{action}; the list contains only the operations of "currently operable windows" — if you can see it, you can call it. If a window exposes a focus-style action, call it before acting so the user sees which one you are working on.
Staying in sync: when you receive an updated for app://windows or a tools-list-changed notification, re-read app://windows before deciding.
Concurrent writes: the user may be operating at the same time — you are not the only operating party. Attach expectedVersion (the window version you read) to write operations; if stale_state is returned, the UI has been changed, so re-read before deciding.
Failure: returned via isError, carrying a code (stale_state/validation_failed/blocked_by_modal…); validation_failed explains what is wrong per field, so correct accordingly and retry.
Large data: a window's state holds only the overview; when you need complete/large data, call the corresponding tool of that window to fetch it (the result is in the returned 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(訂單編輯器)與 C(報表詳情)已開啟且可操作;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