Skip to content

把 MCP server 放在 tailnet:Serve、bearer token 與 tsidp

用 streamable HTTP 的 MCP server 以 Tailscale Serve 只開給 tailnet,Claude Code/Claude Desktop 的遠端 MCP 設定方式,bearer token 與 tsidp 當 OAuth authorization server(DCR)的實驗性做法

Updated View as Markdown

MCP(Model Context Protocol)讓 Claude Code、Claude Desktop 這類 agent 用戶端,透過標準化的方式呼叫外部的工具與資料來源。最常見的用法是 stdio:用戶端把 MCP server 當子行程啟動,透過標準輸入輸出溝通。但如果你想把 MCP server 放在另一台機器上(例如家裡那台常開的 agent 主機),讓多台裝置共用,就要改用 streamable HTTP 傳輸——MCP server 跑成一個獨立的 HTTP 服務,用戶端用網址連過去。

問題是,一個能讀檔案、查資料庫、執行工具呼叫的 MCP server,絕對不能直接開在公網。這篇講怎麼把它放在 tailnet 裡:用 Tailscale Serve 只對 tailnet 開放,再依規模選 bearer token(簡單的靜態憑證)或 tsidp(OAuth authorization server,支援 DCR)。身分標頭與「為什麼後端要只聽 loopback」的基礎,先看 Serve 的身分標頭。

本文依 2026 年 10 月的 Tailscale、Anthropic、MCP 規格與 tsidp 官方文件整理,以 Tailscale v1.104 為準。MCP 的 streamable HTTP 傳輸以 2025-11-25 版規格、Authorization 以 draft 版規格為準;Claude Code 以核對時點的 v2.1.x 為準(官方 changelog 於 2026 年 9 月底已到 v2.1.283,更新頻繁);Claude Desktop 沒有單一的公開版本號(各平台 build 編號不一,例如 MacUpdate 列的 2.9939.2 與 Debian 套件的 1.52386.6),其設定以 2026 年 10 月核對的文件為準;tsidp 為實驗性專案(版本尚未滿 1.0.0)。本文未實機跑過整套設定,屬文件整理。

MCP 的 streamable HTTP 傳輸

MCP 規格目前定義了兩種標準傳輸:stdio(子行程、標準輸入輸出)與 streamable HTTP(獨立 HTTP 服務)。streamable HTTP 取代了舊的 HTTP+SSE 傳輸,特點是:

  • MCP server 提供單一一個 HTTP 端點(例如 https://example.com/mcp),同時接受 POST 與 GET。
  • 用戶端用 POST 送出 JSON-RPC 訊息,server 可以回傳單一 JSON,或開一條 SSE(Server-Sent Events)串流回傳多則訊息。
  • 規格給實作者三條安全要求:驗證 Origin 標頭(防 DNS rebinding)、本機執行時只綁 127.0.0.1、以及對所有連線做驗證。

這三條正是「放在 tailnet」的切入點:把 server 綁在 loopback,再由 Serve 轉發出去,tailnet 上的其他人只能走 Serve 進來,不能直連後端;驗證則交給下一節的 bearer token 或 tsidp。

第一步:MCP server 只聽 loopback

和 Serve 的身分標頭 裡講的原則一樣,MCP server 要綁 127.0.0.1,不要綁 0.0.0.0。綁 loopback 之後,能直接碰到它的只剩同一台主機上的程式,tailnet 上的人都得經 Serve 轉發。

另外注意 Serve 的反向代理永遠轉給 http://127.0.0.1,所以 server 必須聽在 IPv4 的迴路位址,只綁 IPv6 的 [::1] 會被拒絕(細節見 Tailscale Serve)。你的 MCP server 若以 localhost 起,記得確認它實際綁的是 127.0.0.1。

第二步:用 Serve 發布

假設 MCP server 跑在本機 9933 連接埠(tsidp 官方範例也是用這個埠):

tailscale serve --bg http://127.0.0.1:9933

之後 tailnet 內用 https://<你的主機>.tailxxxxx.ts.net 就能連到它。streamable HTTP 的 POST/GET 與 SSE 串流都是標準 HTTP,Serve 的 HTTP 代理可以正常轉發,不需要特別的旗標。想讓多個服務共用同一個主機名稱時,可以用 --set-path 把 MCP 端點掛在子路徑(例如 --set-path=/mcp),此時用戶端的網址就是 https://<你的主機>.tailxxxxx.ts.net/mcp。

第三步:設定 Claude Code

Claude Code 對遠端 MCP server 的官方做法是 HTTP 傳輸:

claude mcp add --transport http <名稱> <網址>

以「有 bearer token」的 server 為例:

claude mcp add --transport http my-mcp https://mcp.tailxxxxx.ts.net \
  --header "Authorization: Bearer <你的-token>"
  • --transport http:這個 server 是放在網址上的 HTTP 服務,不是本機子行程。
  • --header:把靜態憑證(bearer token)帶上,每次請求都會送出。
  • 不加 --scope 時寫進本機專案;--scope user 對所有專案生效,--scope project 寫進可提交進版本控制的 .mcp.json。

用 JSON 設定檔(.mcp.json、~/.claude.json,或 claude mcp add-json)時的寫法:

{
  "mcpServers": {
    "my-mcp": {
      "type": "http",
      "url": "https://mcp.tailxxxxx.ts.net",
      "headers": {
        "Authorization": "Bearer <你的-token>"
      }
    }
  }
}

兩個要點:type 必填,可以寫 http 或規格裡的 streamable-http(兩者等價);只有 url 沒有 type 會被當成 stdio server 而失敗。要確認連線狀態,用 claude mcp list,或在 Claude Code 裡開 /mcp 面板看。

如果你的 server 走 OAuth(下一節的 tsidp 就是),指令一樣,但不用帶 --header:

claude mcp add --transport http my-mcp https://mcp.tailxxxxx.ts.net

加了之後 claude mcp list 會顯示 ! Needs authentication,接著在 Claude Code 裡開 /mcp 完成瀏覽器登入即可。

Claude Desktop 的遠端 MCP

Claude Desktop 的設定檔 claude_desktop_config.json(macOS 在 ~/Library/Application Support/Claude/)是給本機 stdio server 用的(command/args)。遠端 MCP server 走的是另一條路:Connectors UI——Settings → Connectors → Add → Add custom connector → 貼上網址,接著照提示完成驗證(通常是 OAuth)。

所以對 Claude Desktop 來說,放在 tailnet 的 MCP server 就貼上它的 Serve 網址,讓它對 tsidp 跑 OAuth 登入。要注意的是,Claude Desktop 的原生 connector 主要走 OAuth;如果你用的是純 bearer token(靜態憑證)驗證,Anthropic 官方把這種 static_headers 型標為 beta、只開放給部分組織。要用最直接、跨平台穩定的靜態憑證做法,Claude Code 的 --header 是更簡單的路線。

bearer token 與「為什麼不要 Funnel」

把上面的設定攤開看,其實是兩層防護疊在一起:

  • 網路層:Serve 只對 tailnet 開放,公網連不到。這層由 Tailscale 把關。
  • 應用層:bearer token,用戶端每次請求都帶 Authorization: Bearer <token>,server 驗證後才放行。token 怎麼設是每個 MCP server 各自定義的(可能是啟動旗標、環境變數或反代設定),要查該 server 自己的文件。

bearer token 適合小規模、只有你自己和少數信任裝置的情境。它把「誰能連」簡化成「誰有這把鑰匙」——鑰匙洩漏就要換,且沒有使用者身分、沒有權限細分。

為什麼不用 Funnel?Funnel 是把服務公開到整個網際網路,對一個能讀你檔案、執行工具呼叫的 MCP server 來說,這是完全錯誤的暴露等級。Funnel 不帶 tailnet 身分標頭,公開後你就得獨自承擔 MCP 規格裡警告的那些風險(DNS rebinding、惡意 Origin、暴力破解 token)。MCP server 除非是給外部 SaaS 呼叫,否則一律走 Serve。tsidp 自己也有 --funnel 旗標,但那只是為了讓跑在 tailnet 外的 SaaS 應用能用它登入,跟「把 MCP server 開出去」是兩回事。

更進一步:tsidp 當 OAuth authorization server(DCR)

bearer token 的「一把鑰匙」模型,在多人、多 agent、或需要轉發的中間架構下會不夠用。MCP 的 Authorization 規格(選用,HTTP 傳輸建議遵循)定義了一套 OAuth 2.1 流程:MCP server 當 resource server,用戶端當 OAuth client,再交給一個 authorization server 發 access token,之後每次請求帶 Authorization: Bearer <access-token>。

tsidp 就是 Tailscale 開源、跑在 tailnet 裡的 OIDC/OAuth 身分提供者。它用 tsnet 與 LocalAPI 的 whois 直接拿 tailnet 身分,宣稱支援 MCP Authorization 規格要求的端點,包括 DCR(Dynamic Client Registration,RFC 7591)。

DCR 解決的是「手動建立 OAuth client」的麻煩:用戶端第一次連線時,自動向 authorization server 的 /register 端點註冊自己、拿回 client ID。多數大型 IdP(Google、GitHub、Microsoft Entra ID)至今不支援 DCR,所以 MCP 這種大量動態註冊的情境,是 DCR 少見的實際應用場域。Tailscale 在這篇部落格說明瞭這個設計。

有一點要留意:MCP Authorization 的 draft 版規格已把 DCR(RFC 7591)標為 deprecated——保留給不支援 Client ID Metadata Documents 的 authorization server 做向後相容,長期方向是以 Client ID Metadata Documents 作為建議做法。所以 DCR 是「現階段 tsidp 已支援、能動」的機制,但不是規格長期首選,寫進產品規劃時要一併考慮。

tsidp 是實驗性專案,這是本文最重要的提醒:

  • 官方文件與 README 都標示「experimental、actively developed、可能隨時有 breaking changes」。
  • 跑起來需要設 TAILSCALE_USE_WIP_CODE=1,這個環境變數就是為了「版本還沒到 1.0.0」而設的門檻。
  • 它屬於社群專案,官方不提供支援,有問題到 GitHub repo 發 issue。
  • app capability 的 schema 仍在開發中,可能隨時改。

部署 tsidp 需要:MagicDNS、HTTPS 憑證、一把 auth key(或 OAuth client)讓節點註冊,以及一個 tailscale.com/cap/tsidp 的 app capability grant。預設 admin UI 與 DCR 端點都是拒絕存取的,要分別用 allow_admin_ui 與 allow_dcr 兩個欄位打開(見下一節的 grants 範例)。tsidp 需要持久化狀態(-dir/TS_STATE_DIR),無狀態部署每次重啟都會遺失 DCR 註冊的 client。

DCR 的運作方式,官方有 MCP client/server 與 MCP gateway 兩個範例可照著做。

最小權限的 grants 範例

MCP 的 OAuth 流程要求用戶端帶 resource indicator(RFC 8707),指定要存取哪個 MCP server 的 canonical URI;tsidp 會拿 grant 的 resources 欄位比對這個值,決定要不要發 token。而 DCR 端點由 allow_dcr 控制,兩者各自獨立——users/resources 只管「誰能為哪些資源拿到 token」,allow_dcr/allow_admin_ui 才是「誰能註冊 client/用後台」。

所以範例拆成三條 grant:成員能連 MCP server、成員能拿 token、只有管理員能 DCR 註冊:

"tagOwners": {
  "tag:mcp-server": ["autogroup:admin"],
  "tag:tsidp": ["autogroup:admin"],
},
"grants": [
  // 成員可以連到 MCP server 的 Serve 443(後端原始埠不開放)
  {
    "src": ["autogroup:member"],
    "dst": ["tag:mcp-server"],
    "ip": ["tcp:443"],
  },
  // 成員可以為這個 MCP server 拿 access token(users 用 "*" 或實際 login 名稱)
  {
    "src": ["autogroup:member"],
    "dst": ["tag:tsidp"],
    "ip": ["tcp:443"],
    "app": {
      "tailscale.com/cap/tsidp": [
        {
          "users": ["*"],
          "resources": ["https://mcp.tailxxxxx.ts.net"],
        },
      ],
    },
  },
  // 只有管理員能透過 DCR 註冊 client(要後台管理就一併開 allow_admin_ui)
  {
    "src": ["autogroup:admin"],
    "dst": ["tag:tsidp"],
    "ip": ["tcp:443"],
    "app": {
      "tailscale.com/cap/tsidp": [
        {
          "allow_dcr": true,
          "allow_admin_ui": true,
        },
      ],
    },
  },
],

欄位語意(對照 tsidp 原始碼 server/clients.go、server/token.go、server/appcap.go):

  • allow_dcr:DCR 端點(RFC 7591 的 /register)只看這個布林值。clients.go 的 serveDynamicClientRegistration 只檢查 access.allowDCR,與 users/resources 無關——所以它要獨立放進「管理員」那條,放進「成員」那條等於每個成員都能註冊 client。

  • users/resources:只在 token 發放時用。token.go 的 validateResourcesForUser 比對 users 是否含 "*" 或使用者的 login 名稱、resources 是否含請求的 resource。users 不支援 autogroup——寫 autogroup:admin 永遠不會 match,要嘛用 "*",要嘛用實際登入名稱(email)。

  • 多條 grant 的 app capability 會累積合併:appcap.go 的 addGrantAccessContext 把每條 rule 的 allowAdminUI/allowDCR OR 起來。所以管理員同時吃得到「成員」那條的 users/resources 與自己那條的 allow_dcr。

  • MCP server 與 tsidp 各自打上 tag,身分不再綁在個人帳號上(見 Tags 與 auth key)。

  • 上面 users 用 "*" 表示「tailnet 內任何成員都能為這個 MCP server 拿 token」;要更收緊就換成具體的 login 名稱(例如 alice@example.com)。

  • 如果 tsidp 要當「gateway 中間層」做 token exchange(RFC 8693),才需要額外開 TSIDP_ENABLE_STS=1;單純的 client ↔ server 流程用不到。

  • tsidp 的 app capability schema 仍在開發中,欄位可能變動,部署前以當時的 README 為準。規則寫法見 ACL / Grants 入門。

小結

驗證方式 適用情境 把關層
Serve(網路層) 所有情境,一律要做 只有 tailnet 內
bearer token 小規模、自己用 應用層靜態憑證
tsidp(OAuth) 多人、多 agent、需要身分與轉發 OAuth 2.1 + DCR

無論選哪一種,骨架都一樣:MCP server 綁 loopback → Serve 只開 tailnet → 應用層再補驗證。bearer token 是把事情做小的選擇,tsidp 是往標準 OAuth 走的選擇,但後者還掛著「實驗性」的牌子,要放進正式環境前先評估那份 breaking changes 的風險。

參考資料

Navigation

Type to search…

↑↓ navigate↵ selectEsc close