前兩天那個 MCP server 是自己寫的,工具邊界、參數驗證與授權判定全部在自己手上。今天換成接一個別人寫好的 server,對象是 Google Workspace 的日曆與信箱。同一件事換一層來做,成本從實作移到授權與程序管理,組態檔的形狀也跟著變。
Google 在 Cloud Next '26 開放的託管 MCP 裡,Workspace 側是八個遠端端點。實際要用的時候卡在資格:那組端點屬於 Workspace Developer Preview Program,要求持有 Google Workspace 帳號並通過審核,個人 Gmail 帳號不符。
剩下兩種途徑的差別:
| 官方託管 MCP | 開源自架 | |
|---|---|---|
| 帳號資格 | Workspace 帳號並通過審核 | 任何 Google 帳號 |
| 執行位置 | Google 託管的遠端端點 | 本機程序 |
| OAuth client 是誰 | agent 自己 | MCP server 自己 |
| agent 端組態 | 需要 auth: 與 oauth: 兩個區塊 |
一行 url: |
| 信箱唯讀 | 強制要 gmail.compose 寫入權 |
--read-only 只申請 gmail.readonly |
最後一列是選型的決定性差異。唯讀這件事放在 prompt 層是一句指示,模型可能照做也可能不照做,放在 OAuth scope 上,寫入類工具根本不會載入,模型在協定層就沒有那個選項。
建立 GCP 專案之後要啟用底層 API,日曆這一支的識別碼與直覺不同:
gcloud services enable gmail.googleapis.com
gcloud services enable calendar-json.googleapis.com
日曆的服務名是 calendar-json.googleapis.com。 兩者都是正式服務,與 Preview 資格無關,填成 calendar.googleapis.com 會在啟用那一步就失敗。
OAuth 同意畫面這一側,User Type 選 External、Publishing status 留在 Testing,測試使用者上限 100 人。--read-only 模式實際使用的 scope 有五個:
openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/calendar.readonly
這五個是兩組組合起來的。auth/scopes.py 裡 BASE_SCOPES 固定帶 openid 與兩個 userinfo,TOOL_READONLY_SCOPES_MAP 給 gmail 的是 gmail.readonly、給 calendar 的是 calendar.readonly。前三個用於識別身分,真正碰到資料的只有後兩個。
OAuth client 建成 Web application 類型,已授權的重新導向 URI 填 http://localhost:8000/oauth2callback。
產品名與服務識別碼各自獨立命名,啟用哪一支以 Console 列出的識別碼為準
啟動指令與環境變數:
export WORKSPACE_MCP_PORT="8000"
export GOOGLE_OAUTH_REDIRECT_URI="http://localhost:8000/oauth2callback"
uvx workspace-mcp --transport streamable-http \
--tools gmail calendar --tool-tier core --read-only --single-user
| 旗標 | 作用 |
|---|---|
--transport streamable-http |
以 HTTP 端點對外,取代 stdio |
--tools gmail calendar |
只載入這兩個產品的工具 |
--tool-tier core |
每個產品只載入精簡工具集 |
--read-only |
只申請唯讀 scope,寫入類工具不載入 |
--single-user |
單人模式,省掉多使用者的 session 管理 |
--tool-tier core 的作用與前面量過的 context 預算直接相關。工具 schema 整份進 system prompt,掛得越多固定開銷越大,而工具數量上升時模型挑錯工具的機率也跟著上升。篩選後實際掛上來的是五個:
list_calendars:列出可存取的日曆get_events:讀指定日曆的事件search_gmail_messages:依查詢字串搜尋信件get_gmail_message_content:讀單一信件全文get_gmail_messages_content_batch:一次讀多封信件全文五個全是讀取。--read-only 讓寫入那一側在工具清單裡就不存在。
掛載的指令是一行:
hermes mcp add workspace --url "http://127.0.0.1:8000/mcp"
執行後會問一句 Does this server require authentication? [Y/n],答 n。Google 的授權流程由 MCP server 自己跑完,agent 這端不是 OAuth client,因此組態裡既沒有 auth: 也沒有 oauth:,只剩下一個 URL。
授權責任放在哪一層,決定了另一層的組態需要寫幾行
與自建那兩天對照起來,同一件事的成本落在不同位置:
| 自建 MCP | 外部 MCP | |
|---|---|---|
| 主要成本 | 實作工具、參數驗證、安全判定 | 申請憑證、登記 URI、管理程序生命週期 |
| 授權寫在哪 | 自己在工具裡實作確認閘門 | OAuth scope,由服務端強制 |
| 壞掉時查哪 | 自己的程式碼 | 憑證、port、程序是否還活著 |
| 症狀 | 原因 | 處置 |
|---|---|---|
redirect_uri_mismatch |
WORKSPACE_MCP_PORT 與 Console 登記的 URI 不一致 |
兩邊改成同一個 port |
| 掛載成功但連線時好時壞 | 同一個 port 上有第二個行程 | 查出來關掉它 |
回答提到 google_token.json |
同名的內建 skill 搶答 | 停用那個 skill |
第一個錯誤的成因單純,改了 port 就要回 Console 一起改。
第二個是 port 被占用,症狀是時好時壞。Docker Desktop 的後端行程會綁 0.0.0.0:8000,涵蓋所有介面,看起來像 MCP server 在監聽,實際連進去的是另一個程式。確認方式:
netstat -ano | findstr :8000
只有一個行程在這個 port 上才算乾淨。
第三件事與組態無關。agent 內建一個同名的 google-workspace skill,它會跟 MCP server 搶同一類問題,症狀是回答開始提到 google_token.json、google_client_secret.json,或把人導去 Console 建立 Desktop app 憑證。那是 skill 的流程,與 MCP server 無關。同一個領域同時有 skill 與 MCP 時,模型選哪一個由它自己決定,要讓 MCP 接手就得把 skill 停用。
workspace-mcp 是本機程序,跑在一個終端機視窗裡。終端機關掉程序就結束,agent 隨即連不上。 自建那個 MCP 包在容器裡,重開機之後容器自己起來,這一個要人再開一次終端機。
排錯順序因此固定成兩步:
curl http://127.0.0.1:8000/mcp 沒有回應就是程序沒在跑,與 Google 那一側無關redirect_uri_mismatch 的話回去對 port 與登記的 URI外部 MCP 的可用性綁在一個本機行程的壽命上,它的生命週期要自己管
啟動腳本印出伺服器在 8000 監聽,掛載指令回報成功,問日曆問題卻連不上,重試幾次之後又通了一次。時好時壞這件事本身是線索,固定壞或固定好都比較好查。
netstat 列出來 8000 上有兩個行程,另一個是 Docker Desktop 綁的 0.0.0.0:8000。綁在萬用位址的行程涵蓋所有介面,兩邊各自以為自己拿到了這個 port,實際連進去的是哪一個要看作業系統怎麼派。
「伺服器說它在監聽」與「連進去的是它」是兩段不同的資訊。前者來自伺服器自己的輸出,後者要從系統這一側去看誰真的綁著那個 port。
服務自己印出來的監聽訊息,證明的是它呼叫了 bind,證明不了連線會進到它那裡
工具掛上去之後,模型花了三分鐘查別的東西,一次都沒有呼叫它。