Skip to content

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 角色

角色承擔者職責
ServerGUI 應用程式本身(或其內嵌元件)暴露資源(讀取)與工具(寫入),並推送通知
ClientAgent 一側(例如 Claude)讀取資料、呼叫操作
使用者人類與 Agent 並行操作同一個 UI(見 §7.1、§10.1)

互動模型:使用者與 Agent 對話 → Agent 透過本協定讀寫 GUI。使用者不會直接呼叫本協定。

1.3 設計原則

  • 全域讀取,堆疊頂端寫入 — 資源暴露所有視窗;工具只暴露當下可操作視窗(每條模態鏈的堆疊頂端)的操作。
  • 讀寫粒度解耦 — 寫入側止於視窗層級(不深入到控制項);讀取側可以細到語義狀態,但只描述「某物處於什麼狀態」,而不描述「它可以怎樣被操作」。讀得詳細只是為了服務寫入的決策。
  • 結論集中,拓樸只存一處 — 可操作性由 Server 計算成唯一權威結論 operableWindowIds;拓樸只儲存 ownerId,不含任何冗餘的衍生資訊。
  • 正確性由執行期驗證保障 — 動態可見性是一種「盡力而為」的最佳化;最終的正確性由寫入操作的執行期驗證保證(見 §7)。
  • 為 Agent 的可理解性而設計 — 暴露「LLM 能理解的語義」(summary + 語義 blocks + 人類可讀的 titlelabel),而不是控制項樹/座標/螢幕擷圖。Server 把 UI 翻譯成語義的品質,直接決定 Agent 能否理解它(見 §4.3)。

2 · 術語與約定

視窗(window) — 一個頂層視窗或一個模態對話框。它沒有型別區分,並且可以被開啟多次;每個實例都有唯一的 id。視窗是最小的可操作單位;本協定不深入到控制項。

視窗的三種狀態(由 modalownerId 決定):

modalownerId含義
falsenull普通的頂層視窗
true非 null視窗層級模態(附著於某個父視窗之下)
truenull應用層級模態(屬於整個應用程式,沒有父視窗)

模態鏈(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(必須)宣告以下能力:
json
{ "capabilities": {
    "resources": { "subscribe": true },
    "tools":     { "listChanged": true } } }

Server SHOULDinstructions 中提供一份概觀,說明這個唯一資源的用途、說明「操作全部都是視窗層級的,而且清單中只會出現可操作視窗的操作」,以及說明「使用者可能並行操作,所以寫入操作應該攜帶 expectedVersion」。完整的建議文字見 §8.1。

4 · 資源(狀態)

4.1 唯一的資源

本協定只有一個資源:app://windows(可被 subscribe)。它把所有東西都內嵌其中 — 拓樸、可操作性結論,以及每個視窗的內容(summaryblocks)。單次 read 即可得到完整快照;任何變更只會發出自己的 updated,然後由 Client 重新讀取全部內容。在本機 IPC 之下,資料量並不是瓶頸,因此不切分、不分頁、也不做增量傳遞(超大資料的處理見 §9)。

4.2 app://windows

它同時提供拓樸、可操作性結論,以及每個視窗的內容;單次讀取即可得到一份自我一致的快照。

欄位型別必需說明
operableWindowIdsstring[]唯一權威結論:此刻可操作視窗的 id。模態鏈與應用層級收束都已經計入其中
windowsWindow[]所有視窗,每個都內嵌其內容(見下方的 Window 物件);陣列順序沒有語義(它不代表堆疊/z-order,不要依賴這個順序)

Window 物件

欄位型別必需說明
idstring視窗的唯一識別碼,永不重用(§10.3)
titlestring視窗標題(人類可讀)
modalboolean是否為模態
ownerIdstring | null父視窗 id(視窗層級模態附著於它之下);null 表示沒有父視窗,並與 modal 一起決定視窗的三種狀態(見 §2)。用途:Agent 可以沿著各視窗的 ownerId 重建模態鏈,理解模態的歸屬層級(哪個對話框屬於哪個視窗);可操作性本身由 operableWindowIds 直接給出,不需要從它推斷
version單調遞增 int64該視窗的樂觀並行版本號;只要 blockssummary 任一發生變更,它就會遞增(見 §7.1)
summarystring建議用一句話描述「這個視窗目前顯示什麼」
blocksBlock[]視窗內容 — 一串語義內容區塊(見 §4.4);空視窗使用 []

Client MUST(必須)把 operableWindowIds 當作可操作性的唯一依據。

json
{
  "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 理解該視窗的入口。
  • 優先使用語義基本型別:如果某樣東西可以用 fieldstablenoticelist 表達,就不要用 customcustom 是後備手段,過度使用它會讓 Agent 面對一團不透明的 payload。
  • 人類可讀的 label/titlefieldslabel、視窗 title,以及工具的 titledescription,都要用使用者能理解的詞語 — 不要用內部代碼(例如 btn_47)。
  • 把狀態明示出來:用 notice(攜帶 severity)明確陳述錯誤/警告;不要期望 Agent 從字裡行間猜測。

一個反例:summary 為空 + blocks 全部塞進 customlabel 全是代碼 → 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|successtext
media圖片/螢幕擷圖/圖表mimeType: stringurl: stringalt?
custom任意結構的後備方案payload: any

text · 段落文字

欄位型別必需說明
textstring要顯示的純文字內容。

fields · 鍵值欄位群組(表單、屬性面板)

欄位型別必需說明
itemsarray欄位項目的清單;每個項目的欄位見下方。
items[].labelstring欄位的名稱/標籤,例如「收件人」或「金額」。
items[].valueany該欄位的目前值;空字串或 null 表示尚未填寫。
items[].sensitiveboolean標記為 true 表示該值已被遮蔽(見 §4.5)。

table · 表格

欄位型別必需說明
columnsstring[]表頭。每個元素是一欄的標題,陣列順序就是各欄由左至右的順序。範例:["Product","Quantity"]
rowsany[][]列資料。每個元素是一列,列內的陣列與 columns 按位置一一對應 — rows[i][j] 就是第 i 列、第 j 欄(columns[j])的值。範例:[["A",2],["B",1]] 搭配上面的欄位,表示「產品 A 數量 2、產品 B 數量 1」。

list · 清單

欄位型別必需說明
itemsany[]清單項目,每個項目一個值(通常是字串)。
orderedbooleantrue =有序(1. 2. 3.);省略或 false =無序。

notice · 狀態/通知

欄位型別必需說明
severityenum通知層級,為 infowarnerrorsuccess 之一,它決定語氣與顏色。
textstring通知文字,例如「收件人不能為空」。

media · 圖片/螢幕擷圖/圖表(讀取側的富媒體)

欄位型別必需說明
mimeTypestring媒體的 MIME 型別,例如 image/png
urlstring從何處取得它:一個 https://file:// URL,或一個內嵌的 data:<mime>;base64,… URL。
altstring建議一段文字描述 — 語義後備,讓非視覺的 Agent 仍然能理解該媒體顯示的是什麼。

custom · 後備方案

欄位型別必需說明
payloadany任意結構。當內容無法用上述基本型別表達時使用它;Agent 會結合該視窗的 summary 與上下文來理解它。
  • custom 是一個安全閥:無法歸類的內容先放在這裡;當某一類反覆出現時,它 SHOULD 被提升為正式的基本型別。
  • 大量資料走工具:state 只保留概觀/重要資料;一個視窗的完整或超大資料不進入 blocks,而是透過該視窗的工具按需取得(見 §9)。

候選基本型別(本版本不要求):section(分區嵌套)、progress(進度)。

json
{
  "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/list MUST(必須)只包含該時刻可呼叫的操作。兩個層級的隱藏:(1)視窗被模態阻擋 → 整組隱藏;(2)某個視窗層級的操作在該時刻不可用 → 只隱藏該操作。
  • 任何隱藏/恢復都 MUST 觸發 tools/list_changed

5.2 命名

工具的 name MUST 採用 {winId}__{action} 的形式。name 只是機器識別項;Server SHOULD(應該)提供人類可讀的 titledescription。只允許 [A-Za-z0-9_-];否則必須維護一份穩定的控制代碼對映(§10.4)。

json
{
  "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 布林旗標:

欄位型別必需說明
messagestring建議對結果的人類可讀描述,例如「已提交;彈出了一個確認對話框」
openedWindowIdsstring[]本次操作所開啟/彈出的新視窗 id(可能是 0、1 或數個);它們是否為模態由 windows[].modal 給出。沒有時可省略或使用空陣列
resultobject該工具特定的結構化業務結果(例如 { 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,只要其 blockssummary 發生變更它就遞增。
  • 當寫入操作攜帶 expectedVersion 時,若當前 version 不相等,Server MUST(必須)拒絕它,並回傳 stale_state(其中包含 currentVersion)。
  • Client SHOULD(應該)據此重新讀取、重新決策,然後再重試。衝突仲裁的預設原則是使用者優先。

7.2 執行期驗證

動態隱藏是一種「盡力而為」的最佳化;由於通知是非同步的,Client MAY(可以)仍然呼叫一個剛剛被阻擋/失效的操作。Server MUST 對這類呼叫執行執行期驗證,並回傳結構化錯誤 — 這是正確性的最後一道防線。

7.3 欄位層級的驗證錯誤

當因為輸入驗證失敗而被拒絕時,Server MUST 按欄位回傳錯誤,其中 field 取對應的工具參數名稱:

json
{ "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_stateexpectedVersion 與當前的不一致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).

① initializeread instructions② read app://windowswindows + operability conclusion③ subscribeapp://windows

8.2 斷線後的重新同步

app://windows 快照是自我完備的,因此重新同步不需要事件重播 — 重新讀取這個唯一資源就足以重新對齊。

① Reconnectrecover from disconnection② Re-readapp://windows③ Resubscribeapp://windows④ Compare versionlocate the changed windows

8.3 通知的順序與原子性

  • Server MUST(必須)只在狀態完全生效之後才發送通知。
  • 所有東西(拓樸/可操作性/視窗內容)都存在於唯一資源 app://windows 之中,單次讀取即可得到一份自我一致的快照。
  • 收到通知之後,Agent 重新讀取 app://windows 以取得一致的檢視,而不必自行推斷變更的類型。

9 · 大量資料的處理

在本機 IPC 之下,把一般資料量完整地放在 app://windows 中傳輸完全沒有壓力(見 §4),因此本協定不分頁、也不做增量。

真正超大的資料(例如數萬列的表格)該怎麼辦:不要把它塞進 statestate 只保留概觀/重要資料(例如前幾列 + 一句關於總數的說明);完整資料透過該視窗的工具按需取得 — 當 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:trueownerId:null
  • 效果:當存在這樣的模態時,可操作性會優先收束 — operableWindowIds 只保留該應用模態的堆疊頂端,其他一切都不可操作。它們可以嵌套。
  • Server 負責把這種收束計算進 operableWindowIds;本協定不需要為它增加任何額外欄位。

10.3–10.6 其他約定

約定要求
識別碼穩定性視窗 id 全域唯一,而 block.id 在同一視窗內唯一;兩者都 MUST 穩定且永不重用(單調遞增/UUID)
名稱安全字元工具名稱 {winId}__{action} 只允許 [A-Za-z0-9_-];當 winIdaction 含有非法字元時,MUST 維護一份穩定的控制代碼對映
數值寬度每個視窗的 version MUST 是 64 位元單調遞增整數,不做繞回處理
本版本明確不在範圍內權限閘門(操作確認/授權)、控制項層級的操作(僅到視窗層級)

11 · 合規性

一個合規的 Server MUST(必須):

  • 宣告 §3 所列的能力;
  • 提供唯一資源 app://windows(內嵌視窗內容),且各欄位滿足 §4;
  • operableWindowIds 成為可操作性的唯一權威結論,正確反映視窗層級/應用層級模態;
  • tools/list 在任何時刻都只包含可操作視窗的可用操作,並在變化時發出 tools/list_changed
  • 讓工具名稱遵循 {winId}__{action} 且字元合規;
  • 對攜帶 expectedVersion 的衝突寫入回傳 stale_state;對被阻擋/已失效的寫入執行執行期驗證;
  • 只在狀態生效之後才發送通知,並按 §6 對映;
  • 序列化操作的執行,並把通知廣播給所有連線;
  • 在暴露 sensitive 值之前將其遮蔽;
  • 確保 idversion 滿足 §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 文字內容回傳:

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 的結構):

json
{
  "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 出現在 operableWindowIdsownerIdopenedWindowIds 以及工具名稱 {winId}__{action} 之中 — 它們必須是同一個值,這樣 Agent 才能跨欄位與跨工具建立關聯(拿到一個 id → 就能對應到工具、查到視窗)。

  • 對外的 winId MUST 是名稱安全的([A-Za-z0-9_-]),並且在所有介面上保持一致。
  • 當內部的視窗 id 不滿足這一點時(含有空格/中文字元等),Server MUST 全程使用一個穩定對映後的控制代碼作為對外的 winId — 不能只在工具名稱中對映;每一個 id 欄位以及工具名稱都要使用同一個控制代碼。

12.5 雜項約定

  • 關閉視窗 → 只有 app://windows 的內容發生變化(該視窗從 windows[] 中移除),並發出 updated;它不涉及資源的新增/移除。
  • 空集合是合法的:operableWindowIds 為空(全部被應用層級模態阻擋)以及 tools/list 為空,兩者都是正常狀態。

附錄 A · 完整範例

場景:視窗 A(訂單編輯器)與 C(報表詳情)已開啟且可操作;A 提交之後,彈出確認模態 D1。

AgentServer · GUI1read app://windows{ operableWindowIds, windows[…inlined content] }2call w-A__submit(expectedVersion=42){ openedWindowIds:[w-D1], message }tools/list_changed · w-A__* → w-D1__*app://windows updated · operable→[w-D1,w-C]3call w-D1__confirmD1 closedtools/list_changed · restoredapp://windows updated · operable→[w-A,w-C]RequestResponseNotification (async broadcast)

附錄 B · 型別參考

資源 · 被讀取的狀態

Windowsapp://windows

欄位型別備註
operableWindowIdsstring[]可操作視窗 · 唯一權威結論
windowsWindow[]所有視窗 · 內嵌內容 · 順序沒有語義

Windowwindows[] 的元素

欄位型別備註
idstring全域唯一 · 永不重用
titlestring視窗標題
modalboolean是否為模態
ownerIdstring | null父視窗;null =頂層/應用層級模態
versionint64內容版本 · 寫入一致性
summarystring?一句話描述
blocksBlock[]內嵌內容 · 一串語義內容區塊

Block — 七種之一 · 全部包含 typeid?

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 }

對於 tablelist:截斷時必須提供 totaloffset 預設為 0

工具 · 主動發起的動作

呼叫參數 — 除了各工具自身的參數之外

欄位型別備註
expectedVersionint?所屬視窗的 state.version · 樂觀並行

ToolResult — 成功 · 沒有 ok 旗標

欄位型別備註
messagestring?人類可讀的結果
openedWindowIdsstring[]開啟/彈出的新視窗 · 可能有數個
resultobject?該工具特定的業務結果 · 結構由 outputSchema 定義

ToolErrorisError = true

欄位型別備註
codestring錯誤碼 · 見下方
messagestring人類可讀
currentVersionint?code = stale_state
fields{field,error}[]?code = validation_failed

codestale_state · validation_failed · blocked_by_modal · window_not_found · action_not_available · action_timeout