iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
AI Engineering

打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計系列 第 21 篇

【Day 21】把 REST API 包成 MCP server:5 個工具 3,347 byte,描述佔五成

  • 分享至 

  • xImage
  •  

昨天比的是三條取得途徑,封裝對象在內網時只剩自建與自架兩條。今天走自建這條:把一個 IoT 專案既有的四支 REST 端點包成 MCP server,掛進 agent,然後量三件事——工具的 schema 佔多少、確認閘門擋不擋得住、這個 server 實際講的是哪一版協定。


為什麼多包一層

那四支端點本身就是 HTTP,理論上給模型一個發請求的工具就能用。多包一層是為了把四件事固定在協定層:

  • 工具邊界:模型看得到的操作就是列出來的那幾個,沒有「順手打別的端點」這條路
  • 參數驗證:合法值的範圍在工具裡判定,不合法的請求不會送到 API
  • 授權範圍:哪些操作要先取得同意,寫成工具的回傳值而非提示裡的一條規則
  • 稽核點:每一次呼叫都經過同一個函式,要加紀錄時只有一個地方要改

四件事的共通點是它們都需要一個確定會被執行的位置。寫在 prompt 裡的規則由模型決定要不要遵守,寫在工具實作裡的規則由程式決定。


五個模組各管一件事

模組 負責 不碰
config.py API 位址、監聽埠、溫度政策、方向定義 邏輯與 I/O
api_client.py 對四支端點發 HTTP,封裝 payload 格式 業務與安全判定
safety.py 純判定函式,回傳 (allowed, message) 任何 I/O
tools.py 參數檢查、呼叫 safety、呼叫 client、整理回傳 判定規則本身
server.py 建 client、註冊工具、掛 health、啟動 上面四項的內容

safety.py 沒有 I/O 是刻意的,判定規則因此可以用單元測試蓋滿,不必起 server 也不必連 API。tools.py 以 register_tools(mcp, client) 注入依賴,client 在模組層沒有綁死的實例,測試時換成替身即可。

被封裝的四支端點是三個 POST 加一個 GET,一支讀感測值、三支下控制指令。


語意層工具與低階參數工具並存

同一組硬體有兩個工具可以操作。低階那個 control_fans 收設備編號與兩顆風扇各自的方向,語意層那個 control_door_fans 收一個位置名稱與 on/off,內部自己查對照表、自己決定方向、自己對多台設備各發一次請求。

兩者的 schema 大小反過來:

工具 schema 合計 其中 description 其中 inputSchema
get_environment_status 402 180 62
control_fans 940 550 230
control_door_fans 912 601 142
control_ac 499 190 155
set_ac_temp 594 291 152
合計 3,347 1,812 741

低階工具的參數多,inputSchema 230 byte 是全組最大的一個。語意層工具的參數只有兩個,inputSchema 142 byte,它的 description 反而是全組最長的 601 byte,因為要寫清楚哪些說法對應到哪一個位置、on 實際會做什麼。

三個數字合起來是同一件事的兩面:參數從 schema 移到描述裡,總量沒有省下來,省的是模型要做的推理。 低階工具要模型自己查對照、自己決定方向、自己拆成兩次呼叫,語意層工具把這三步收進實作。

description 全組 1,812 byte,佔 3,347 的 54%,加上工具名與 title 之後 schema 的其餘部分才 794 byte。寫工具描述的成本與寫 schema 的成本不在同一個量級。


三種閘門,各自的回傳形狀不同

判定規則分三類,實測各跑一次。需確認與不合法這兩類在判定階段就回傳,實際的 HTTP 請求沒有送出,因此以本機的替身 API 執行不影響結論:

呼叫 回傳
set_ac_temp(25) ok: true,指令送出
set_ac_temp(20) ok: false、needs_confirmation: true,訊息說明超出自主範圍並要求複述後帶 confirm=true
set_ac_temp(35) ok: false、error: true,訊息說明超出 API 合法範圍,已拒絕
control_ac("OFF") ok: false、needs_confirmation: true,訊息要求複述「將關閉冷氣」
control_ac("OFF", confirm=True) ok: true,指令送出
control_fans(device_id=9, ...) ok: false、error,訊息說明編號只能是那兩個值

三種形狀對應三種語意:

  • ok: true — 判定通過,API 收到請求
  • needs_confirmation: true — 值合法但超出 agent 的自主範圍,實作沒有送出請求
  • error — 值本身不合法,實作沒有送出請求

第二類與第三類的差別在有沒有回頭路。合法但需確認的,模型複述給使用者、得到同意、帶 confirm=true 再呼叫一次就會執行。不合法的帶什麼旗標都不會執行。

溫度那個工具同時有兩條界線,API 的合法範圍是一組數字,agent 可以自己決定的範圍是更窄的一組。自主範圍與合法範圍分開寫,是為了讓「拒絕」與「要問一下」變成兩種不同的回傳。

判定結果裡寫明下一步,模型才接得下去


掛上去:連上之後才寫進組態

hermes mcp add 只給 --url,它先連線、列出探索到的工具、問要不要全部啟用:

Connecting to http://127.0.0.1:9100/mcp
Does this server require authentication? [Y/n]:
Connecting to 'env-control'...

✓ Connected! Found 5 tool(s) from 'env-control':
  ...
Enable all 5 tools? [Y/n/select]:
✓ Saved 'env-control' to .../config.yaml (5/5 tools enabled)
Start a new session to use these tools.

hermes mcp test 另外報連線耗時:

Testing 'env-control'...
Transport: HTTP → http://127.0.0.1:9100/mcp
Auth: none
✓ Connected (1531ms)
✓ Tools discovered: 5

兩個指令合起來決定了三件事的先後:

  • 先連線再寫組態:探索失敗時組態維持原狀,不會留下一個連不上的項目
  • 工具的啟用範圍在加入時就決定,[Y/n/select] 那一問可以只挑其中幾個
  • 既有的 session 沿用舊的工具陣列,最後一行提示要開新 session 才用得到,這與身分那一層的行為一致

prompt-size 量不到 MCP 的工具

前幾天量 context 用的是 hermes prompt-size,把這個 server 掛上去之後再量一次,工具數與 byte 數沒有變:

狀態 工具數 工具 schema byte
掛上 5 個 MCP 工具 19 33,227
移除之後 19 33,227

原因在 hermes_cli/prompt_size.py 的 _build_inspection_agent()。它建的是一個離線的 agent,api_key 與 base_url 都填假值以強制走直接建構的路徑,工具集只從組態裡的 toolset 清單來。註解寫明這樣做是為了不呼叫網路,MCP 的工具要連上 server 才拿得到,離線的量測自然看不到它們。

所以 MCP 那一側的 schema 成本要自己量,方法是直接對 server 發一次 tools/list,把回傳的每個工具序列化成 JSON 再數 byte。上面那張 3,347 的表就是這樣來的。

把兩個數字放在一起:原有的 33,227 加上這 3,347,MCP 這五個工具讓工具 schema 的總量多了 10%。


它實際講的是 2025-11-25

這個 server 建在 FastMCP 上,組態以環境變數 FASTMCP_STATELESS_HTTP=true 開啟無狀態模式,容器回報的 serverInfo 版本是 3.4.4。

四個探測的結果:

探測 結果
不先握手直接發 tools/list 正常回傳五個工具
initialize 時宣告 protocolVersion: 2026-07-28 回傳 protocolVersion: 2025-11-25
呼叫 server/discover -32602 Invalid request parameters
tools/list 回傳的鍵 只有 tools

server/discover 是新版要求 server MUST 實作的探索方法,這裡回的錯誤碼是參數無效。tools/list 那一列對應的是新版要求列表結果帶上 ttlMs 與 cacheScope,回傳裡兩個都沒有。

capabilities 裡倒是出現了 extensions 欄位,內容是 io.modelcontextprotocol/ui。新版加的那個宣告位置已經在了,協定版本本身還停在上一版。

因此這個 server 的「無狀態」要講清楚是哪一種:

  • 已經成立:不維持連線層的 session,不先握手也拿得到工具清單
  • 尚未成立:協定版本停在上一版,新版的 server/discover 與列表結果的快取欄位都還沒有

前者是 FastMCP 自己的選項帶來的,後者要等上游跟上新版才有。組態旗標的名字與規範條文的名字剛好撞在一起,兩者指的不是同一件事。


心得

那個 10% 是這次唯一讓我意外的數字。掛上去之前預期它會很明顯,五個工具、每個描述都寫得很長,結果是原本就在的 33,227 把它稀釋掉了。原生的工具集有十九個,MCP 這五個是加在一個已經不小的基數上。

順著這個數字往回看,之前為了省 byte 去刪工具描述的那些字,省的比例比想像中小。真正決定總量的是工具的個數,不是每個工具寫得多詳細。

另一件是量到一半才發現的。原本打算用 prompt-size 直接看掛上去前後的差,掛了、量了、數字一模一樣,第一反應是組態沒存進去,去翻 config.yaml 才確認存了,再去翻那個指令的實作才知道它根本不連 MCP。

一個量測工具涵蓋的範圍,寫在它的實作裡


明天

控制指令送出之後要等一段時間才會反映在感測值上,同步的回應表達不了「已受理但還沒完成」。


上一篇
【Day 20】要接一個 MCP:自建、官方託管、開源自架的取捨
下一篇
【Day 22】已受理但還沒完成:長時操作在無狀態協定下怎麼做
系列文
打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言