AI 工程

Cline Desktop v0.0.10 拆解:遠端 MCP server 的 OAuth 授權怎麼跑、怎麼設、哪裡有雷

遠端 MCP server 這一年變多了:Linear、Notion、GitHub、Context7、Sentry,全都給你一個 https://.../mcp 的網址,叫你貼進 agent 的設定檔。貼完之後呢?多數人的做法是去該服務後台開一把 personal access token,寫成 "headers": { "Authorization": "Bearer ..." },能動就好。

這個做法有三個問題:token 明碼躺在你家目錄的 JSON 裡、它不會自動 refresh、而且權限通常大得離譜(一把 PAT 常常等於整個帳號)。正解一直都是 OAuth,但 coding agent 這端的支援一直很爛——Cline 自己的 issue #4523 標題就叫「MCP servers are not allowed to use oauth」,社群長期的 workaround 是外掛一層 mcp-remote 當代理。

2026 年 8 月 7 日,Cline 發了 Desktop v0.0.10,release note 第一條就是這件事:

Remote MCP servers can now authenticate with OAuth from Settings → MCP — authorize a server, see its auth status, and cancel or retry a pending authorization.

這篇把它整個拆開:MCP 規格上的 OAuth 是怎麼設計的、Cline 在原始碼裡怎麼實作(含它自己加的那些防呆)、你今天要怎麼設定、以及有哪幾個雷——其中一個蠻大的,release note 沒寫。

先聲明:這篇的依據是 GitHub release、merged PR(#12983 / #12984 / #13050)跟 cline/cline main 分支的原始碼,不是我在機器上跑過的實測。v0.0.10 的桌面安裝檔只有 darwin-aarch64darwin-x86_64 兩個 target(看 release 的 latest.json),也就是目前只有 macOS 版,我手上的 Linux 開發機根本裝不起來。哪些是我從程式碼讀出來的、哪些是維護者自己在 PR 裡寫的驗證結果,下面我會分開標。

一、這個功能到底解決什麼

MCP 的授權規格(2025-06-18 版)把角色切得很乾淨:

  • MCP server 是 OAuth 2.1 的 resource server,它只負責驗 token、拒絕就回 401。
  • MCP client(也就是 Cline)是 OAuth 2.1 client,負責跑完授權流程、拿 token、每個 HTTP request 都帶 Authorization: Bearer
  • authorization server 是第三方,可能跟 MCP server 同源,也可能不是。

規格對 client 端下了幾個 MUST:必須看得懂 401 回來的 WWW-Authenticate、必須用 RFC 9728 的 protected resource metadata 去找 authorization server、必須用 RFC 8414 的 authorization server metadata、必須做 PKCE、必須在授權跟換 token 兩個 request 都帶 RFC 8707 的 resource 參數。DCR(RFC 7591 動態註冊)則只是 SHOULD。

「SHOULD 不是 MUST」正是 v0.0.10 那條 release note 後半段在講的事:

Servers that require a pre-registered OAuth client (client ID/secret) instead of dynamic registration are now supported.

翻成人話:以前 Cline 的 OAuth 路徑假設對方一定有 /register 端點。沒有的話,流程在開瀏覽器之前就死掉,錯誤訊息是 Incompatible auth server: does not support dynamic client registration。這段是 Cline 的 Bee(GitHub: abeatrix)在 PR #12983 的說明裡寫的,她點名的受害者就是 GitHub 官方的 remote MCP server https://api.githubcopilot.com/mcp/——GitHub 要你自己去建 OAuth App,不給你動態註冊。

二、Cline 實際上跑了哪六步

Cline 遠端 MCP OAuth 授權的六個步驟

核心邏輯在 sdk/packages/core/src/extensions/mcp/oauth.tsauthorizeMcpServerOAuth()。照原始碼順序:

Step 0:前置檢查。 stdio transport 直接拒絕(does not support OAuth browser flow),因為規格明講 stdio 應該從環境變數拿憑證。接著它會掃你的 headers,只要有任何一個 key 小寫後等於 authorization,就直接丟錯:has a static Authorization header. Remove it before starting OAuth. 這個防呆很值得注意——靜態 token 跟 OAuth 是互斥的,你不能兩個都留著。

Step 1:先開本機 callback server。 startLocalOAuthServer() 綁在 127.0.0.1,依序試 port 1456 → 1457 → 1458,路徑固定 /mcp/oauth/callback,等待上限 5 分鐘(DEFAULT_MCP_OAUTH_TIMEOUT_MS = 5 * 60 * 1000)。所以 redirect URI 會長成 http://127.0.0.1:1456/mcp/oauth/callback——這串就是你去 GitHub / Google 建 OAuth App 時要填的 callback URL,而且三個 port 最好都填進去,因為它是搶到哪個算哪個。

Step 2:試連一次,讓 401 自己冒出來。 Cline 不會盲目開瀏覽器。它先 client.connect() + listTools();如果已經有有效 token,就直接回「already authorized」結束。只有在拋出 401 時,才把 authorizationRequired: true 寫進設定檔、並且把 SDK 在這輪產生的授權 URL 撈出來。

這裡有個設計值得學:探測跟授權是分離的。桌面版 sidecar 的註解寫得很白——「只有明確呼叫這個函式才會開瀏覽器;一般的 server enable/connect 只會持久化 SDK 的 authorizationRequired 狀態」。所以你在 Settings → MCP 把某台 server 打開,它不會突然彈一個瀏覽器視窗給你,只會亮出一條琥珀色橫幅 OAuth authorization required 和一顆 Connect。按下去才真的走流程。

Step 3:決定 client 身分。 這是 v0.0.10 新增的分岔。Cline 讀你這台 server 設定裡的 oauthClient 欄位:

{
  "mcpServers": {
    "github": {
      "type": "streamableHttp",
      "url": "https://api.githubcopilot.com/mcp/",
      "oauthClient": {
        "clientId": "Iv1.xxxxxxxxxxxx",
        "clientSecret": "選填,public client 可以不給"
      }
    }
  }
}

clientId 就用它、跳過註冊;沒有就退回原本的 RFC 7591 動態註冊路徑。動態註冊時 Cline 送出的 client metadata 是寫死的:client_name: "Cline"grant_types: ["authorization_code", "refresh_token"]response_types: ["code"]token_endpoint_auth_method: "none"(也就是以 public client 註冊)。

Step 4:PKCE + state + 瀏覽器。 MCP SDK 產生 code_verifier(Cline 把它連同 discoveryState 一起寫進設定檔的 oauth 區塊)、code_challengeresource 參數;Cline 這邊自己用 randomUUID()state。授權 URL 交給 openUrl 開瀏覽器。

Step 5:收 code、對 state、換 token。 本機 server 收到 callback 後,Cline 會比對 state,不合就直接 OAuth authorization failed: state mismatch.;合了才呼叫 transport.finishAuth(code)。token 存進 cline_mcp_settings.jsonoauth.tokens,同時記一個 lastAuthenticatedAt

Step 6:重連驗收。 拿到 token 不算數——Cline 會開一個全新的 client 再 connect 一次、再 listTools() 一次,兩個都過才清掉錯誤狀態、回報 OAuth authorization completed。這是我覺得這個實作最紮實的一點:它驗的是「這把 token 真的能列工具」,不是「授權端點回 200」。

一個容易被忽略的細節:client 換了,token 就作廢

release note 最後半句「stored tokens are invalidated when a server's client configuration changes」在程式碼裡是一整組 guard。oauth.ts 裡有個 assertOAuthClientUnchanged(),在 saveClientInformation / saveTokens / saveCodeVerifier / invalidateCredentials / saveDiscoveryState 每一個寫入點都會呼叫;不符就丟 McpOAuthClientChangedError,訊息是「OAuth client configuration changed while authorizing… Start authorization again.」。另外 tokens() getter 本身也會比對:目前設定的 client 跟存起來的 client 不一致時,直接回 undefined,等於舊 token 立刻失效。

這解的是一個實務上很陰的 bug:你把 clientId 從 App A 改成 App B,但舊 token 還在檔案裡,agent 就會拿 A 的 token 去打 B 的資源、然後得到看不懂的 403。

那 token 過期怎麼辦

Cline 動態註冊時申請的 grant_types 包含 refresh_token,所以只要對方 authorization server 有發 refresh token,換新 token 這件事是 MCP SDK 在 transport 層自己處理的,不需要你再按一次 Connect。設定檔裡的 lastAuthenticatedAt 只是給 UI 顯示用的時間戳,不是有效期。

不過要注意 Cline 目前的錯誤語意:它把「需要授權」跟「連線失敗」拆成兩個狀態(authorizationRequiredlastError),refresh 失敗時通常會退回前者、重新亮出 Connect。這是設計上比較好的地方——很多工具在這種情況只會給你一個含糊的「connection failed」,你根本分不清是網路掛了還是 token 死了。

另外一件規格層面、但實作在對面的事:MCP server 必須驗證 token 的 audience 是不是自己(RFC 8707 / RFC 9068),而且不准把收到的 token 原封不動轉發給下游 API——這是 confused deputy 的標準防線。這條 MUST 落在 server 端,client 再乖也管不到。所以在把公司內部資料接上遠端 MCP 之前,值得直接問對方一句:你們的 MCP server 有沒有做 audience 驗證。

三、三種認證方式,你該選哪一個

靜態 header、動態註冊、預註冊 client 三種方式對照

(A) 靜態 headers + Bearer token。 最快,適合自己寫的內部 server、或只給 read-only scope 的短期 token。缺點:不會 refresh、明碼落地、而且如前所述,設了它就不能用 OAuth

(B) 動態註冊(oauthClient 留空)。 對方 authorization server 有 /register 就走這條,你什麼都不用填。Cline 維護者 Saoud Rizwan 在 PR #13050 的測試說明裡,是拿 Linear 的 https://mcp.linear.app/mcp 做端對端驗證的。

(C) 預註冊 client(v0.0.10 新增)。 對方沒有 /register 就走這條。以 GitHub 為例,你要先去 GitHub 建一個 OAuth App,callback URL 填 http://127.0.0.1:1456/mcp/oauth/callback(保險起見連 1457、1458 一起填),再把 client ID 填進 oauthClient.clientId

至於 transport,設定檔的三個值分別對應桌面版 UI 上的標籤:stdio = Local · stdiosse = Remote · SSE (legacy)streamableHttp = Remote · Streamable HTTP能選 streamableHttp 就選它——理由下一段講。

四、v0.0.10 有個 release note 沒寫的洞(SSE)

這是這篇最實用的一段。看時間戳:release desktop-v0.0.10 發佈於 2026-08-07T07:04:07Z;PR #13050「fix(core): surface OAuth authorization for SSE MCP servers on 401」merge 於同日 23:42:43Z晚了 16 小時,所以這個修正不在 v0.0.10 裡。 PR 描述自己也寫明了:「Found during desktop v0.0.10 regression testing」。

那個 bug 是什麼?Saoud Rizwan 在 PR 裡寫得很清楚:走 SSE(legacy)transport 的遠端 server——包括你只寫 {"url": "..."}、沒寫 type 的那種,因為省略 type 會 fallback 到 legacy SSE——OAuth 流程完全不會出現。你在 Settings → MCP 把開關打開,它會靜靜地彈回關閉,沒有 banner、沒有錯誤、沒有任何授權入口。

根因是型別在跨層時被吃掉了:Cline 在 fetch 邊界丟出的 UnauthorizedError,在 streamable HTTP transport 會正常往上傳,但 SSE 的 stream request 跑在 EventSource 裡,EventSource 會把這個例外吞掉、重新發成一個沒有 status 的 SseError(訊息 SSE error: MCP server requires authorizationcode: undefined)。client.ts 裡四處 error instanceof UnauthorizedError 的判斷因此全部落空,走到 markConnectionError 而不是 markAuthorizationRequiredauthorizationRequired 從沒被寫進去,UI 自然什麼都不顯示。

修法是兩段:一是給 SSE transport 傳 eventSourceInit.fetch,讓 401 以帶著 code: 401SseError 形式失敗(順便避開 EventSource 對 thrown fetch error 的重連排程);二是抽一個 isMcpUnauthorizedError() 述詞同時認 UnauthorizedErrorSseError && code === 401,取代原本四處 instanceof 檢查。

所以你現在該做的事:在 v0.0.10 上,所有遠端 MCP server 都明確寫 "type": "streamableHttp" 只有對方真的只支援 legacy SSE 時,才需要等下一版桌面 build(或改用 CLI/VS Code 擴充,因為修正已經在 main 上)。

五、照著做:桌面版、CLI、直接改 JSON

設定檔位置~/.cline/data/settings/cline_mcp_settings.json。可以用環境變數 CLINE_MCP_SETTINGS_PATH 覆寫。這個檔案會被 CLI、VS Code 擴充、JetBrains、桌面版同時讀寫,所以 Cline 用了 temp file + rename 的原子寫入——你手改的時候別讓編輯器開著同一個檔案跑背景 autosave

路線 1:桌面版 GUI。 Settings → MCP → 加 server(填 URL、選 transport)→ 存檔 → 打開開關 → 看到琥珀橫幅 OAuth authorization required → 按 Connect → 瀏覽器完成登入 → 橫幅變成 Waiting for OAuth authorization(旁邊有 Cancel)→ 成功後顯示 OAuth connected。取消的語意也做了區分:桌面 sidecar 的 shouldRestoreEnabledStateAfterOAuthCancellation() 只有在使用者自己按 Cancel(reason = user)時才把 server 恢復成原本的啟用狀態;因為 server 被停用、被新流程取代(superseded)、或 webview 關掉(owner-closed)而中斷的,不會亂改你的開關。

路線 2:CLI。 cline mcp 開互動精靈,或直接:

cline mcp install github --transport http https://api.githubcopilot.com/mcp/
cline mcp install events --transport sse https://example.com/sse

注意這兩個指令都是開精靈、不是靜默安裝,所以需要 TTY。精靈在 Authentication 那步選 OAuth 之後,會問兩個問題,字面就是:

  • OAuth client ID (leave empty for dynamic registration)
  • OAuth client secret (leave empty for public clients)

留空就是走動態註冊。精靈主選單另外有 Authorize OAuth 可以對既有 server 重跑授權。順帶一提,同一批更新裡也加了 cline mcp uninstall(PR #12985)。

路線 3:直接改 JSON。 最小可用設定(動態註冊版):

{
  "mcpServers": {
    "linear": {
      "type": "streamableHttp",
      "url": "https://mcp.linear.app/mcp",
      "disabled": false,
      "autoApprove": []
    }
  }
}

授權成功後,Cline 會在同一個 server 物件底下長出一個 oauth 區塊,欄位是 clientInformation / tokens / codeVerifier / discoveryState / redirectUrl / lastError / lastAuthenticatedAt / authorizationRequired這個檔案現在裝著 refresh token,請把它當成 credential 檔對待——別 commit、別放進 dotfiles repo、備份時記得排除。

排錯速查:

症狀 意義
has a static Authorization header headers 裡的 Authorization 刪掉再授權
state mismatch callback 的 state 對不上,重按一次 Connect
Timed out waiting for...callback 超過 5 分鐘沒完成,重跑
Unable to bind local MCP OAuth callback server 1456–1458 三個 port 都被佔了
OAuth client configuration changed while authorizing 授權途中 oauthClient 被改動,重跑
開關自己彈回、什麼都沒顯示 幾乎確定是上一段的 SSE bug

授權完之後怎麼確認它真的成功了:別只看 UI 的綠字。從程式碼看,Cline 判定成功的條件是「重新 connect 且 listTools() 有回應」,所以最直接的驗收是回到對話裡下一句話,叫 agent 列出那台 server 的工具、然後挑一個唯讀的工具實際呼叫一次。例如接了 Linear 就叫它「用 linear 這台 MCP server 列出我被指派的 issue,只讀不要改」。工具列得出來但呼叫回 401,通常代表 scope 不足而不是授權失敗——這兩者在 UI 上長得一樣,但解法完全不同(前者要回去改 OAuth App 的 scope 設定)。

順帶提醒一個範圍問題:OAuth 解的是身分,不是權限控制。授權完之後,agent 能呼叫這台 server 上所有它被允許的工具,該不該讓它自動執行仍然是 autoApprove 的事。遠端 MCP server 的工具描述本身也可能夾帶惡意指令(prompt injection / tool poisoning),這件事 OAuth 一點忙都幫不上。第三方 server 的 autoApprove 建議一律留空。

六、對工程團隊的意義

一、可以開始把 PAT 從 agent 設定檔裡拔掉了。 以前「agent 存的是一把長效 PAT」是被迫接受的現實;現在至少在 Cline 這條線上,你可以換成短期 access token + refresh token,而且 token 綁定了 resource(RFC 8707),不能拿去打別的服務。對於會把 agent 跑在共用開發機、或有 SOC 2 稽核壓力的團隊,這是很實際的一格。

二、oauthClient 讓「公司統一發一個 OAuth App」變可行。 這是預註冊 client 被低估的價值:平台團隊可以建一個內部 OAuth App、把 clientId 寫進團隊共用的 MCP 設定範本發下去,每個工程師各自跑一次瀏覽器授權、拿到自己身分的 token。權限稽核看得到是誰、撤銷可以單獨撤。動態註冊做不到這件事——每台機器註冊出來的都是不同的匿名 client。

三、把「靜態 header 互斥」寫進你的設定範本註解。 這是最容易讓人卡半小時的坑:從舊設定遷移過來的人,往往留著 Authorization header 又去按 Connect,然後看到一個看不懂的錯誤。

四、規格已經定了,工具鏈才剛跟上。 MCP 的授權章節該有的都有(PRM 發現、PKCE、resource indicator、audience 驗證、禁止 token passthrough),真正落後的是 client 端實作。v0.0.10 這種「規格早就寫好、client 花一年才補上、補上第一版還漏了 legacy transport」的節奏,大概就是接下來一年 MCP 生態的常態。看到新版本,值得先確認的永遠是同一件事:它蓋到的是哪一條 transport 路徑

七、來源

整理:DataAgent · Coding Agent 實戰教學

發表迴響

%d 位部落客按了讚: