重點摘要(一分鐘版)
- 導向型 MCP Gateway = 一台無狀態路由 server,擺在既有 API 目錄前面:本身零資料、零快取、零登入狀態。
- 只做兩件事:引導(告訴 Agent「這類問題查哪個資料集」)+轉拋(把查詢即時打到上游、回傳當下結果)。
- 一句話心法:「Agent 問你,你身上沒資料,但你知道資料在哪、幫他導過去查。」
當企業內部有上千個各自獨立、各自授權的資料 API時,AI Agent 的最大問題不是「查不到」,而是根本不知道有什麼、也不知道該問誰。這篇拆解一個實際上線的解法——一台導向型 MCP Gateway:它不持有資料,只讓 Agent 找對資料集、把查詢即時轉拋過去。
| 面向 | 🚫 直覺做法:持有資料 | ✅ 這套做法:導向資料 |
|---|---|---|
| 角色 | 資料倉儲 / 快取層 | 路由器 / 導向器 |
| 狀態 | 有 DB、有快取、要同步 | 無狀態,每次獨立 |
| 真相 | 兩份會分歧 | 上游唯一真相 |
| 風險 | PII 落地、同步負擔 | 資料不落地 |
問題:Agent 面對上千個 API 的「無知」
大型組織的資料通常不是乾淨的 data lake,而是幾百上千個各自獨立的 API——各自授權、各自欄位規格。人靠 portal 與口耳相傳慢慢摸;Agent 沒有背景,兩件事都卡:不知道有什麼可查、就算知道也不知道端點長怎樣。導向型 Gateway 的答案是:上游已是權威來源,我不再存一份,只讓 Agent 找得到、問得到。
四件套工具 = Agent 的認知流程
整台 server 對 Agent 只暴露四個工具,剛好走完「探索 → 理解 → 定位 → 查詢」:
① 探索
list_datasets()
「這裡有哪些資料集?」
→
② 理解
describe_dataset()
「有哪些欄位、要不要身分?」
→
③ 定位
find_dataset()
「用問題找資料集」=導向核心
→
④ 查詢
query_dataset()
「真的去查」,回真資料、不落地
八個核心設計決策(為什麼這樣做)
每個決策都是岔路 → 選擇 → 帶走的通則。一表看完:
| 面向 | 我們選了什麼 | 🎯 帶走的通則 |
|---|---|---|
| A 定位 | 純導向、零落地 | 上游是權威來源時,gateway 的職責是降低發現與存取摩擦,不是複製資料。先問「這份狀態非存不可嗎?」 |
| B 範圍 | 明列「不做什麼」:不落地、不寫入 | YAGNI 要「寫下來」才守得住。範圍靠主動排除守住,不是被動不做。 |
| C 實證 | 拿能跑的真實請求逆推契約 | 逆向對接以 known-good 呼叫為錨點;宣稱可取的都要一次實際 HTTP 200 背書。 |
| D 推導 | 目錄只存代碼,URL 用規則推導 | 能由規則生成的存規則不存結果;新增資料集=丟一個代碼。 |
| E 授權 | MCP 只轉拋,永不自己湊身分 | 畫清「誰負責解析身分」的邊界;寧可回一個明確的「我需要身分」,也不要默默用錯身分。 |
| F 無狀態 | 身分每次當參數帶入,不記 session | gateway 盡量無狀態;上下文由呼叫方每次帶進來 → 可擴充、可重試。 |
| G 誠實 | 不做假資料 fallback | 誠實失敗 > 似是而非;錯誤結構化、給可行動欄位。一次假資料毀掉全部信任。 |
| H 遷就 | 上游無欄位過濾→本機比對補償 | 先摸清上游不能做什麼,再決定自己補什麼(補時標成本 + 設上限)。 |
面向 D 實作(推導取代儲存)——URL 由代碼長出來,catalog 不存 URL:
面向 E 深潛:授權模型的三版演進
這段最值得學——想法愈想愈乾淨:
v1 · 單層
app 級金鑰 + 固定服務身分。夠用但沒分敏感度。
→
v2 · 敏感度驅動
低敏感度帶服務身分;高敏感度要求呼叫端提供身分,缺則回錯誤。
→
v3 · 定調
MCP 只是轉接器,永不湊身分;身分解析是呼叫端(Agent / SSO)的責任,AD 永遠留在 client 那側。
身分該從哪來?依資料集敏感度決定——這是 query 打網路前的決策樹:
① 這個資料集是什麼敏感度?
低敏感度 → 預設帶服務身分(呼叫端帶齊三欄則覆蓋)
高敏感度 → 要求呼叫端提供身分
高敏感度 → 要求呼叫端提供身分
├ 三欄齊全 → 用呼叫端身分查
└ 缺身分 → 擋下,回 identity_required
└ 缺身分 → 擋下,回 identity_required
方法論:由內而外的 checkpoint
大任務切成「離線可測 → 對外可連」,每關綠燈才前進——先讓核心邏輯能離線 mock 測,最後才碰網路與部署,失敗定位最快:
CP1 核心(離線)
代碼推 URL、組 body、分頁、catalog。全 mock 測。
→
CP2 工具層
四工具契約 + 錯誤轉譯(mock client)。
→
CP3 整合起服務
live 測(opt-in)對齊 total;MCP client 連得上。
→
CP4 部署
反代 + systemd 常駐,對外可連。
| 面向 | 做法 |
|---|---|
| 測試分層 | 離線 mock 為主(預設全綠、CI 友善);真實網路測試 RUN_LIVE=1 才跑。鐵律:測試 / log 不得出現真實 PII 或金鑰。 |
| 部署 | backend 綁 localhost、對外走反代;systemd 常駐(Restart=on-failure + 開機自啟);機密走環境變數,不進版控。 |
| 單一真相 | design 寫「該長怎樣」、git log 寫「怎麼變成的」、留一節「接手看這裡」。三者不重複。 |
踩過的坑(帶走這些,少走冤枉路)
| 坑 | 解法 |
|---|---|
| 反代後 DNS-rebinding 421 | MCP SDK 擋非白名單 Host。反代後 backend 看到公開域名,ALLOWED_HOSTS 必須含正式域名(+ localhost),否則 initialize 回 421。 |
| SSE 卡住 | streamable HTTP 走 SSE,nginx 要 proxy_buffering off。 |
| 金鑰 scope 陷阱 | 「拿不到」先確認是不是金鑰 scope 太窄,不是程式錯。跨模組要用 scope 較廣那把。 |
| 上游沒有欄位過濾 | 別假設上游能依任意欄位過濾——實測只認日期。要條件查就整表拉下來本機比對(標成本、設上限)。 |
怎麼複製這套模式到別的資料來源
| # | 步驟 |
|---|---|
| 1 | 找 known-good 呼叫當契約錨點,實測 host / auth / body / 回傳 / 分頁。 |
| 2 | 抽最小事實 + 推導規則:能由規則算的(代碼→URL)別存進 catalog。 |
| 3 | 列 catalog,每筆要有 200 背書;標 reachable 區分實測可取 vs 聽說。 |
| 4 | 畫授權邊界:身分每次當參數帶入,缺就回結構化 identity_required。 |
| 5 | 四件套工具:list / describe / find / query(含上游限制的本機補償)。 |
| 6 | 由內而外做 + 分層測:CP1 離線核心 → … → 部署;離線全綠、live opt-in。 |
| 7 | 寫下不做什麼:YAGNI 排除清單放進設計文件。 |
| 8 | 單一真相 + 演進留痕:design + git log + 「接手看這裡」一節。 |
一頁帶走的原則清單
- 上游是權威來源時,gateway 的價值在降低發現與存取摩擦,不在複製資料。
- YAGNI 要寫下來才守得住;主動排除 > 被動不做。
- 逆向對接以 known-good 呼叫為契約錨點;宣稱可取的都要實際 200。
- 能由規則生成的存規則不存結果。
- 畫清身分解析的責任邊界;gateway 不偷湊身分,寧可明確說「我需要身分」。
- gateway 盡量無狀態;上下文每次帶進來。
- 誠實失敗 > 似是而非;錯誤結構化、給可行動欄位;不做假資料 fallback。
- 先摸清上游不能做什麼,再決定自己這層補什麼。
- 大任務切由內而外的 checkpoint,每關綠燈才前進。
導向型 MCP Gateway 的程式碼可能只有幾百行,但背後那個轉念很關鍵:MCP server 不一定要持有資料,它可以只是一台幫 Agent 消除無知的路由器。當你手上已有一個權威但難以發現的 API 目錄,這套模式能用最小維運成本,把它變成 Agent 用得動的能力。
發佈留言