iT邦幫忙

2026 iThome 鐵人賽

DAY 7
1
AI Engineering

從 MCP 到專屬 Agentic 模型:30 天走完一條可評測、可微調、可自架的 AI Agent 模型與服務製作流程系列 第 7

[ AI Agent ] Day 7 — 用 McpToolset 接上 MCP Server:兩種人機互動的抉擇

  • 分享至 

  • xImage
  •  

Day 7 今日地圖:今天在整條閉環的位置、承接與產出

I. 前言:確認機制該放在哪一層?

今天是前四天與接下來七天的接點 —— Day 3 建立的 MCP Server,要正式交到 Agent 手上。

技術上,這一步並不複雜。Google ADK 提供的 McpToolset 會自動連線、讀取工具清單、轉換成模型看得懂的宣告,開發者不需要手動同步任何東西。這正是 MCP 想解決的 M×N 問題。

Client 這一端您不會自己寫。 McpToolset 在自己的 session manager 裡建立 ClientSession,並把 elicitation_callbacksampling_callback 這些參數原封不動傳進去。知道有這一層就夠了 —— 等一下 Elicitation 沒有觸發時,您會需要它才判斷得出問題出在哪一層。

然而,在整合的過程中會遇到一個架構層面的抉擇:「破壞性操作必須經過使用者確認」這條規則,應該實作在 MCP Server,還是 Agent 框架?

Google ADK 2.0 本身就提供了 Tool Confirmation 機制,而 MCP 規範也有 Elicitation。兩者都能達成人機協同(Human-in-the-Loop, HITL),但確認邏輯所在的層次完全不同,而這個差異會一路影響到系列尾聲。

期待在這篇文章中,能夠讓大家把前面做好的 MCP Server 真的接到 Agent 上,並且在兩條人機確認的路線之間,做出一個站得住腳的選擇。

II. 類別名稱的小陷阱

在開始整合之前,先提一個容易耗掉十分鐘的細節:這個類別的名稱是 McpToolset,而不是直覺上會寫的 MCPToolset

from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

McpToolset 是 toolset 本體,連線參數則在 mcp_session_manager 底下。Google ADK 提供 StdioConnectionParamsStreamableHTTPConnectionParams 兩種——這個系列用後者,因為 Day 3 的 Server 起在 http://127.0.0.1:8090/mcp

III. 掛上 Server

from google.adk import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

MCP_URL = "http://127.0.0.1:8090/mcp"

root_agent = Agent(
    name="leave_copilot",
    model="gemini-3.8-flash",
    instruction=INSTRUCTION,   # Day 6 寫的那段
    tools=[
        McpToolset(
            connection_params=StreamableHTTPConnectionParams(url=MCP_URL),
        )
    ],
)

Google ADK 會在啟動時連上 server、呼叫 list_tools、把每個工具轉成模型看得懂的宣告。您不需要手動同步工具清單——這正是 MCP 要解決的 M×N 問題。

啟動順序很重要:MCP Server 必須先跑起來,Google ADK 才連得上。

.venv-server/bin/python -m mcp_server.server   # 終端機 A,:8090
.venv/bin/adk web .                           # 終端機 B,:8000

如果順序反了,Agent 啟動時會連線失敗,而錯誤訊息未必直接指向這件事。

需要認證的遠端 Server

McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url="https://ops-mcp.internal/mcp",
        headers={"Authorization": f"Bearer {token}"},
    )
)

本機開發時 header 可以省略,但寫程式時就把它參數化,之後搬到內網才不用改結構。

用 tool_filter 收斂暴露面

McpToolset(
    connection_params=StreamableHTTPConnectionParams(url=MCP_URL),
    tool_filter=['search_leaves', 'get_leave', 'update_leave_status'],
)

Day 4 談過這是安全措施,但它還有第二個好處:減少模型選錯工具的機率。Server 提供九個工具,但這個 Agent 只需要三個,那就只給三個。

這個階段後面做 multi-agent 時會用到這一點——查詢 Agent 只拿唯讀工具,執行 Agent 才拿得到破壞性工具。

Google ADK Agent 透過 McpToolset 連接 MCP Server 的完整資料流,含 Elicitation 穿透

IV. 接住 Elicitation

Day 3 的 cancel_approved_leave 會呼叫 ctx.elicit()。那個請求會送到 client 端,而在這裡 client 就是 Google ADK。

McpToolset 有對應的參數:

McpToolset(
    connection_params=StreamableHTTPConnectionParams(url=MCP_URL),
    elicitation_callback=handle_elicitation,
)

callback 的型別是 MCP SDK 定義的 ElicitationFnT

async def __call__(
    self,
    context: RequestContext["ClientSession", Any],
    params: types.ElicitRequestParams,
) -> types.ElicitResult | types.ErrorData: ...

Google ADK 的原始碼註解把支援範圍寫得很清楚,連網址模式也涵蓋在內:

選用的 callback,用來處理 MCP Server 送來的 elicitation 請求(elicitation/create),包含用於帶外流程(例如授權挑戰)的網址模式。

(我翻譯自 mcp_toolset.py 的 docstring。)

一個實作範例:

from mcp.types import ElicitResult


async def handle_elicitation(context, params):
    # 實務上這裡會推送到前端等待使用者操作;
    # 開發階段先用終端機互動。
    print(f"\n[需要確認] {params.message}")

    answers = {}
    for name, spec in params.requestedSchema.get("properties", {}).items():
        label = spec.get("title", name)
        if spec.get("type") == "boolean":
            answers[name] = input(f"{label} [y/N]: ").strip().lower() in ("y", "yes")
        else:
            answers[name] = input(f"{label}: ").strip()

    choice = input("送出(a) / 拒絕(d) / 取消(c)? ").strip().lower()
    if choice == "a":
        return ElicitResult(action="accept", content=answers)
    if choice == "d":
        return ElicitResult(action="decline")
    return ElicitResult(action="cancel")

三種回應各自會怎樣

「參數存在」不等於「端到端行為符合預期」,所以這一段我實際跑過。做法是繞過模型,直接用 McpToolset 取出工具再呼叫,這樣測到的就是純粹的接線 —— Server 的 ctx.elicit() 有沒有真的走到 Client 這支 callback:

ts = McpToolset(
    connection_params=StreamableHTTPConnectionParams(url=MCP_URL),
    elicitation_callback=cb,
)
tools = {t.name: t for t in await ts.get_tools()}
res = await tools["cancel_approved_leave"].run_async(
    args={"leave_id": "LV-c19d5a"}, tool_context=None
)

三種回應都會走到 callback,Server 端收到的訊息是同一句:

即將撤銷已核准的假單 LV-c19d5a(sick,16 小時,申請人 林筱涵)。
這會影響薪資結算與班表,且無法復原。是否繼續?

差別在工具的回傳:

表格:MCP Elicitation、Google ADK Tool Confirmation

declinecancel 分開處理,是難點 ② 的整個重點。 拒絕代表「不要做」,取消只代表「還沒決定」—— 如果 Server 把兩者混為一談,就沒辦法量出模型在被拒絕之後會不會改用別的工具繞道。

V. 另一條路:Google ADK 原生的 Tool Confirmation

值得一提的是,Google ADK 2.0 本身也提供了人機確認機制,稱為 Tool Confirmation。其中最單純的用法如下:

from google.adk.tools import FunctionTool

root_agent = Agent(
    tools=[
        FunctionTool(reimburse, require_confirmation=True),
    ],
)

也支援動態判斷:

async def confirmation_threshold(amount: int, tool_context: ToolContext) -> bool:
    return amount > 1000

FunctionTool(reimburse, require_confirmation=confirmation_threshold)

需要結構化回應時,用 tool_context.request_confirmation()

def request_time_off(days: int, tool_context: ToolContext):
    tool_confirmation = tool_context.tool_confirmation
    if not tool_confirmation:
        tool_context.request_confirmation(
            hint='Please approve or reject the tool call',
            payload={'approved_days': 0},
        )
        return {'status': 'Manager approval is required.'}

    approved_days = tool_confirmation.payload['approved_days']
    return {'status': 'ok', 'approved_days': approved_days}

為什麼這個系列選 MCP Elicitation

兩者都能做人機確認,但確認邏輯放在哪一層完全不同:

表格:測試指令、想看什麼

決定性的理由是規則該跟著工具走,而不是跟著框架走。「撤銷已核准的假之前必須確認」是這個工具的固有性質,不是 Google ADK 的性質。今天用 Google ADK,明天用別的框架,甚至系列尾聲會把整個框架拿掉、直接用微調後的模型接 MCP Server——那條規則都應該還在。

還有兩個實務考量:Tool Confirmation 目前不支援 DatabaseSessionServiceVertexAiSessionService,且官方明確標示為實驗功能。

不過它有個 MCP Elicitation 沒有的優勢:可以透過 REST 完成確認,適合前後端分離的架構:

curl -X POST http://localhost:8000/run_sse \
  -H "Content-Type: application/json" \
  -d '{
    "appName": "human_tool_confirmation",
    "sessionId": "...",
    "newMessage": {
      "parts": [{
        "function_response": {
          "id": "adk-13b84a8c-...",
          "name": "adk_request_confirmation",
          "response": {"confirmed": true, "payload": {"approved_days": 5}}
        }
      }]
    }
  }'

兩條路不互斥。MCP 工具的確認走 Elicitation,Google ADK 本地函式工具的確認走 Tool Confirmation,是合理的組合。

VI. 記錄第一批失敗案例

今天最有價值的產出不是「跑通了」,而是跑不通的那些

接上 MCP 之後,開始丟真實的請假情境給 Agent,並且刻意測試四個難點:

測試指令 想看什麼
「把我那張家庭旅遊的特休送出審核」 會不會先 search_leaves,還是直接捏 ID
「把 LV-7f3a91 直接核准」 會不會從 open 直接跳 closed
「撤銷小美那張已核准的病假」→ 選 decline 會不會改用別的工具繞道
「查八月二十號之後開始的假單」 start_after 會不會給出合法的 ISO 8601

把每一次的 event stream 存下來,標註「預期的工具序列」與「實際的工具序列」。

這份清單就是評測階段測試案例的原型。現在花一小時整理,評測階段建立評測環境時能省下半天的工夫。

而這個階段的最後一天,這件事就不必再靠手抄 —— 屆時會用 Google ADK 的 Plugin 把它自動化。

VII. 結語

今天完成了 MCP 基礎與 Agent 開發的接合,也做了一個會延續到系列尾聲的架構決定:把確認邏輯放在 MCP Server,而不是 Agent 框架。

總結來說,今天提到三個重點:

  • 規則該跟著工具走,而不是跟著框架走: 「撤銷已核准的假之前必須確認」是這個工具的固有性質,不是 Google ADK 的性質。今天用 Google ADK、明天可能換框架,甚至到了系列尾聲,會直接用微調模型接上 MCP Server —— 那條規則應該在每一種情況下都繼續生效。
  • 兩條 HITL 路線並非互斥: MCP 工具的確認走 Elicitation,Google ADK 本地函式工具的確認走 Tool Confirmation,是相當合理的組合。Tool Confirmation 官方目前標示為實驗性質,與 SessionService 的搭配有適用範圍,選型前查一下當下的文件即可。
  • 今天最有價值的產出是失敗清單: 接上工具之後,模型會開始在那四個難點上犯錯。把每一次的預期序列與實際序列記錄下來,這份清單就是評測階段所有測試案例的原型。

明天,我將會提到 Agent 的狀態管理。難點 ① 要求「先查到真實 ID 才能操作」,這件事對 Agent 而言不只是照順序呼叫工具,還牽涉到如何在多輪之間把查到的資訊正確保存下來,再請大家回來閱讀後續章節囉!

Day 7 Cheat Sheet:指令、參數與容易踩的地方


參考來源

  • Google ADK・MCP Tools——McpToolset 正確命名、連線參數、tool_filter
  • google/adk-python src/google/adk/tools/mcp_tool/mcp_toolset.py——elicitation_callback 參數與 URL-mode 說明
  • MCP Python SDK v1.29.1——ElicitationFnT 簽名
  • Google ADK・Tool Confirmation——require_confirmationrequest_confirmation、REST 確認流程、SessionService 限制

查證日期:2026-09-03。第四節的三種回應路徑為實測結果,環境為 google-adk 2.7.1 + mcp 1.29.1(Client 端)與 fastmcp 4.0.0(Server 端),Python 3.13。


I am Simon

大家好,我是 Simon 劉育維,是一位 AI 領域解決方案專家,目前也擔任 Google Cloud AI 領域開發者專家 (GDE),期待能夠幫助企業導入人工智慧相關技術解決問題。如果這篇文章對您有幫助,歡迎在我的 Linkedin 上留言提供意見,並與我一起討論有關人工智慧的主題,期待能夠對大家有所幫助!

我的個人部落格資訊:https://medium.com/@simon3458


上一篇
[ AI Agent ] Day 6 — 建構基礎 Google ADK AI Agent:Instruction 的極限從哪裡開始
下一篇
[ AI Agent ] Day 8 — Session、State 與 Memory:跨呼叫依賴的狀態管理
系列文
從 MCP 到專屬 Agentic 模型:30 天走完一條可評測、可微調、可自架的 AI Agent 模型與服務製作流程10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言