昨天完成了 devbench 的第一版。
它已經會讀檔、列出檔案、執行測試,也能查看 Git 歷史。
但目前所有操作,還是我們自己寫死:
await session.call_tool(
"read_file",
{"path": "pyproject.toml"},
)
也就是說,Tool 已經有了,卻還少一個角色負責判斷:
現在該不該用工具?如果要用,該用哪一個?
這就是從 MCP 往 Agent 走時,第一個需要補上的東西。
今天先不急著裝 Agent Framework,也先不讓模型真的控制工具。
先把每一層到底負責什麼拆清楚,再做一層之後可以重複使用的 MCP Bridge。
安全治理的完整版本會留到 Day 19、20,但從今天開始,交給模型的工具先限制在唯讀範圍。
先把幾個角色分開。
模型
↓
提出下一步要做什麼
Host
↓
保存對話、提供工具、控制執行流程
MCP Client
↓
把工具呼叫送到 Server
MCP Server
↓
真正執行 read_file、list_files...
放進目前的 devbench,流程會變成:
flowchart TD
U[使用者提出開發問題] --> H[Host]
H --> L[LLM]
L --> H
H --> C[MCP Client]
C --> S[devbench MCP Server]
S --> F[專案檔案]
F --> S
S --> C
C --> H
這裡有一個很重要的差別:
模型說它要讀檔,不代表檔案已經被讀取。
模型只能提出:
我要呼叫 read_file
path = src/ironman/llm.py
真正是否執行,仍然要經過 Host,再交給 MCP Client 和 Server。
所以之後看到 Agent 說:
我要執行某個工具。
不能直接把它當成:
工具已經執行成功。
中間還有權限、參數驗證、執行結果和錯誤處理。
Day 10 已經能從 MCP Server 取得 Tools。
今天新增:
src/ironman/mcp_bridge.py
它主要負責兩件事:
MCP Tool
↓
轉成模型看得懂的 ToolSpec
Tool 執行結果
↓
整理成 Host 使用的 ToolObservation
例如把 tools/list 的結果轉成:
specs = [
ToolSpec(
name=t.name,
description=t.description or "",
input_schema=t.input_schema,
)
for t in listed.tools
if t.name in allowed
]
這一層看起來很薄,但之後不管是自己手刻 Agent、LangGraph,還是 Google ADK,都可以共用。
更重要的是這個:
if t.name in allowed
allowed 不是 Server 決定,也不是模型決定。
是 Host 決定哪些工具可以交給模型。
Day 10 的 devbench 提供:
read_file
list_files
run_tests
git_log
但今天只開:
read_file
list_files
也就是:
allowed = {
"read_file",
"list_files",
}
Server 可以提供很多能力,Host 不一定要全部曝光給模型。
這個差別非常重要。
例如 run_tests 看起來只是:
跑測試
但它實際上會啟動:
pytest
而測試程式本身就是可以執行 Python code 的。
所以不能因為 Tool 名字看起來安全,就直接把它當成唯讀能力。
Tool 能不能交給模型,要看它實際會做什麼,不是看名字。
Day 10 已經用 safe_path() 防止:
../../../etc/passwd
但「沒有逃出專案目錄」還不代表模型什麼都應該看。
例如專案裡可能有:
.env
.env.local
credentials.json
private-key.pem
它們都在專案目錄內。
所以今天再加一層限制,只允許這次教學會用到的範圍,例如:
src/
days/
tests/
README.md
pyproject.toml
像:
.env.local
即使真的存在,也不讓模型透過這個 Bridge 讀。
這裡要注意,這只是針對目前教學專案設計的 allowlist。
它不是一套能自動判斷「這是不是公司機密」的萬用安全機制。
如果有人把 Token 直接寫進:
src/config.py
而 src/ 又在允許範圍內,資料還是有可能被讀到。
所以 allowlist 是降低暴露面,不是資料分類系統。
這次的 main.py 很簡單:
import asyncio
from ironman.mcp_bridge import devbench
async def main() -> None:
async with devbench() as tools:
for spec in tools.specs:
print(spec.name)
result = await tools.call(
"read_file",
{
"path": "pyproject.toml",
"max_lines": 8,
},
)
assert not result.is_error
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
今天故意 沒有呼叫模型。
先確認三件事:
MCP Server 能正常啟動
↓
Host 只能取得允許的 Tools
↓
Tool 可以經過 Bridge 正常執行
如果這一層都還不穩,就直接把模型加進來,除錯時很容易搞不清楚到底是哪裡出了問題:
是 Prompt?
是模型選錯 Tool?
是 Tool schema?
是 MCP?
還是 Server 根本沒起來?
先把確定性的部分測好,再加入模型這個不確定因素,問題會單純很多。
除了正常讀取:
pyproject.toml
測試也會故意做幾件不允許的事情:
呼叫 run_tests
讀取 .env.local
傳入不合法的 max_lines
預期結果不是「成功執行」,而是:
拒絕
這也是測試通過。
因為我們要驗證的不只是:
正常功能能不能跑。
還包括:
不該做的事情,有沒有真的做不到。
所以安全相關測試裡:
拒絕成功 = 測試成功
這個觀念後面 Day 19、20 還會再用到。

到這裡,其實已經可以先回答一個之後很常遇到的問題:
LangGraph、Google ADK 這些 Agent Framework 到底在幹嘛?
它們主要幫忙管理:
對話狀態
工具介面
執行流程
事件
節點之間的流轉
重試與停止條件
但 Framework 不會自動把一個設計不好的 Tool 變安全。
如果你把:
delete_database
毫無限制地交給模型,換成哪一套 Agent Framework 都不會突然變成好設計。
同樣地,它也不會自動知道:
.env
不應該交給模型。
這些仍然是 Host 和工具層要負責的事情。
所以我比較想先把底層角色搞清楚,再開始碰框架。
不然很容易看到:
Agent
Tool
Memory
State
Graph
MCP
全部混在一起,最後只剩「框架好像幫我做了很多事」,但不知道事情到底在哪一層發生。
Day 10 做出了:
MCP Server
今天多了一層:
MCP Server
↑
MCP Bridge
↑
Host
它負責把 MCP Tool 轉成上層可以使用的統一格式,同時限制模型真正能碰到的能力。
目前還少最後一塊:
誰來決定下一步?
今天還是我們自己寫:
tools.call("read_file", ...)
明天開始,把這個決定交給模型。
讓模型看著目前的問題與工具清單,自己判斷:
現在要不要使用工具?
↓
要用哪一個?
↓
拿到結果後下一步是什麼?
這就是 ReAct 開始出現的地方。
Day 12:
Agent 如何一邊思考一邊使用工具?認識 ReAct。