infra-mcp
給 AI Agent 的基礎設施資源層——分配機器、port 和名字,讓 AI 不只會寫程式,還能真的把東西部署出去。
核心問題
AI coding agent 現在寫程式跟測試都做得不錯。但要部署的時候會撞牆:哪個 port 是空的?hostname 該叫什麼?tunnel 怎麼註冊?這些問題的答案不在程式碼裡,在基礎設施的狀態裡——而 AI 看不到那個狀態。
常見的作法是讓 AI 直接 ssh 進去 netstat 一下、翻 Caddy config、猜一個沒人用的 port。這能動,但每次都是重新推理一遍,而且沒有任何東西記得它做過什麼。下次要收回資源時,沒人知道 8043 是誰的。
infra-mcp 是一台 MCP server,把這些基礎設施原語變成有狀態、可查詢、可回收的工具:port 分配、DNS 管理、Cloudflare Tunnel 註冊、服務部署、Gitea repo 管理。可以想成 AI agent 跟基礎設施之間的資源排程層。
42 個工具
| 分類 | 工具 |
|---|---|
| Ports | allocate_port release_port reconcile_ports check_listening_ports |
| Services | register_service record_service update_service deploy_service restart_service stop_service purge_service upgrade_service get_service_info get_service_logs check_service_health get_caddy_config |
| Security | audit_all_services validate_service_security check_firewall |
| Tunnels | register_main_tunnel list_main_tunnels create_cloudflare_tunnel delete_cloudflare_tunnel list_cloudflare_tunnels get_tunnel_config get_tunnel_token list_public_hostnames add_public_hostname remove_public_hostname |
| DNS | create_dns_record update_dns_record delete_dns_record list_dns_records |
| Access | create_access_application delete_access_application list_access_applications list_access_policies |
| Git | create_gitea_repo list_gitea_repos get_gitea_repo delete_gitea_repo |
| Inventory | list_resources |
兩次改版,修的是同一件事
v1.0.0 在 2026 年 5 月上線,38 個工具,功能表看起來很完整。接下來兩個月它一直在跑,我也一直在用。
然後 7 月做了兩次改版,一天一次。事後回頭看,這兩次修的其實是同一類問題,而且不是「功能不夠」——是工具說的話不能信,但它看起來完全正常在運作。這比壞掉危險得多:壞掉會停,說謊會繼續。
v1.1.0 沒人信的稽核工具,保護不了任何東西
安全稽核工具從 v1.0.0 就在了。我沒在用它,而且它值得被忽略:每一個服務都評 0.0 分。
原因有兩個,都很蠢。它推導 Caddy config 檔名的規則是 {project}-{service}.caddy,但真實檔名是照 hostname 命名的,所以永遠找不到檔案;然後 check_listening_ports 把 0.0.0.0 判定得比 Tailscale 位址還不嚴重。
代價
六個服務在公網 IP 上開著大約三個月沒人發現。它們不在那兩個「有通過篩選」的服務裡——因為舊版只稽核標記為 deployed 的記錄,其他的根本沒掃。
一個滿口 0.0 分的工具,跟一個關掉的工具,防護效果一模一樣。而且前者更糟,因為它讓人以為有在看。
v1.1.0 把稽核重寫成可信的:
- 三種結果,不是兩種——
SECURE/VULNERABLE/UNVERIFIED。「我判斷不出 bind 位址」既不是健康也不是漏洞,分數只計算真的判斷得出來的部分 - 用找的,不要用推的——Caddy site 靠 hostname、port、static root 去找;systemd unit 靠
WorkingDirectory和ExecStart去找。真實的 unit 是照它跑什麼命名的,不是照{project}-{service} - 位址用解析的,不要用字串比對——loopback、Tailscale(
100.64.0.0/10、fd7a:115c:a1e0::/48)、私有與公開網段,依「誰搆得到」分級 - 快也是正確性的一部分——每台機器抓一份
ServerSnapshot,不再每個檢查往返一次。全機隊稽核從 3–4 分鐘變成約 11 秒。慢的稽核跟吵的稽核一樣,都會被跳過
同一版還修了 purge_service——它從來沒有一次成功回傳過。標記記錄為已清除之後,它用一個「排除已清除列」的查詢再讀一次,拿到 None,然後拋錯,對一件已經做完的工作回報 PURGE_FAILED。看到這個訊息最直覺的兩個反應——重試、或手動去清——都是錯的。
反過來它也可能在網站還在服務的時候回報成功:Caddy 移除和檔案刪除的失敗被丟掉不看。現在任何一步失敗都回 PURGE_INCOMPLETE 並指名留下了什麼。
另外新增了 check_firewall,它不相信防火牆對自己的說法。一個處於 rc 狀態的 ufw 套件,會讓 /etc/ufw/ufw.conf 寫著 ENABLED=yes、systemctl is-enabled ufw 回答 enabled,而實際上一條過濾規則都沒有。所以它去看活的 iptables/ip6tables 規則、套件狀態和持久化設定——IPv6 分開檢查,因為清掉 ufw 可能讓 v6 鏈停在 ACCEPT 而 v4 看起來一切正常。
v2.0.0 猜出來的路徑,被當成事實記了兩個月
v1.1.0 上線隔天,我做了一次全機隊普查,把 registry 裡每一筆路徑拿去跟真實檔案系統對。結果是 34 筆資料/設定路徑裡有 32 筆指向不存在的目錄,6 筆 static 路徑全錯。
根因是結構性的,不是 bug:register_service 會替每一個服務推導出五個子路徑(app、static、data、config、log)。但我的機隊裡多數服務根本不是這台 server 部署的——它們是我以前手動架的,只是被登記進來當作清單。對這些服務,推導出來的路徑就是虛構。
虛構為什麼危險
purge_service 會拿這些路徑來問我「要不要刪這些目錄」。稽核報告會把它們當成真實資源列出來。整個系統對這些路徑的態度,跟對真實路徑一模一樣——因為 schema 裡它們就是同一個欄位。
問題不在於某一筆記錯了,而在於 schema 讓「猜測」跟「事實」無法區分。
所以 v2.0.0 不是修那 32 筆,是消滅這一類記錄存在的可能性。
資源模型:從猜路徑到 layer + roots + overrides
五個路徑欄位被砍掉,換成:
layer——standard(我部署的)或nonstandard(我只是觀察到的)project_root/deploy_root/workspace_url——根,不是子路徑path_overrides(JSON)——只存偏離慣例的例外
子路徑改由單一 resolver 依慣例推導。關鍵在最後那一段判斷:
這樣一來,虛構路徑不是「比較少見」,是寫不出來。要有子路徑,得先有一個我確實分配過的根。
分配 vs 記錄:新工具 record_service
把兩件事拆開之後,職責也跟著清楚了:
register_service | record_service(新) | |
|---|---|---|
| 用途 | 分配資源給新服務 | 登記已存在、非我部署的服務 |
| layer | standard | nonstandard |
| 路徑 | 配發根,子路徑依慣例推導 | 零預設值,不知道的就留 null |
| deploy | 消費這些根去部署 | 拒絕執行 |
| purge | 可刪它分配的目錄 | 永不刪除任何目錄 |
「不知道」現在是一個可以被表達的狀態。以前不行——schema 逼你填一個值,所以填進去的是猜的。
其他一併修掉的
- health check 現在會探 deployments table。以前
/health只探 port 分配,schema 壞掉它照樣回報健康——而 v1.1.0 加的 pull-based 自動部署就是靠這個 health check 決定要不要 rollback。等於自動回滾裝了但抓不到最該抓的那種故障 - 兩階段 migration script(
scripts/migrate_resource_model.py):先加欄位+回填(舊程式還在跑)→ 新程式上線後才 drop。預設 dry-run,逐筆報告,可重跑,對未遷移的 DB 拒絕 drop 欄位 - 測試從 0 個到 50 個(pytest + pytest-asyncio),涵蓋模型、路徑推導、兩種註冊模式、deploy/purge 的守門條件,以及 migration script 對 fixture DB 的行為
技術選擇
- Python 3.11 + FastAPI(
mcpSDK)——遠端 MCP server,HTTP endpoint 掛在/mcp,不是 stdio。多台機器、多個 client 共用同一份 registry,狀態才有意義 - SQLAlchemy + aiosqlite——registry 是單一 SQLite 檔,可備份、可直接查。規模是幾十個服務不是幾萬個,用 Postgres 只是多一個會壞的東西
- 單一 resolver——路徑推導只有一個實作,所有消費者(deploy、purge、audit、info、upgrade)都走它。慣例改了只要改一處,而且不可能有某個工具還在用舊慣例
- SSH 走既有 config——不自己管金鑰也不自己開連線,服務記錄裡的
server就是~/.ssh/config的 host alias,認證的事留給 ssh agent - 公開 repo 的資訊衛生——
.gitignore擋configs/整個目錄而不只是*.db。因為*.db沒擋到resources.db.bak-<date>,那份完整的基礎設施清單一度是「沒被追蹤但可以被 stage」的狀態,躺在一個 public repo 裡
我從這個專案學到的
做 AI agent 用的工具,最大的風險不是功能不夠,是它在錯的時候還很有說服力。
人類用錯工具會起疑:分數全都是 0.0,我會覺得怪。但 agent 不會——它拿到什麼就信什麼,然後基於那個往下做決定。一個推導出來的路徑,跟一個真的路徑,在 JSON response 裡長得完全一樣。
所以這兩版的修法有個共同形狀,而且我覺得它比任何單一功能都重要:讓系統有能力說「我不知道」。稽核多了 UNVERIFIED;註冊多了 nonstandard 這一層;purge 多了 PURGE_INCOMPLETE。三個都不是新功能,是把原本被迫二選一的地方開出第三個誠實的選項。
下一步是把這個模型推到 deploy_service 的完整生命週期,還有讓 registry 能主動偵測漂移——不是等我想到要做普查才發現 32 筆是假的。
相關專案
- Enterprise AI Ops Stack——infra-mcp 是這個三層架構的基礎設施層,上面還有身分層與終端層
- cred-mcp——憑證管理層,讓密碼明文永遠不進 LLM context
- pty-mcp——互動式終端 MCP server,需要真 TTY 的操作走它