跳轉至

授權

機器翻譯

本頁是從英文說明文件自動翻譯而來,以英文頁面為準。如果哪裡讀起來不對勁,翻譯有說明如何回報。

透過 Streamable HTTP,MCP 伺服器就是一個普通的 Web 服務,保護它的方式也和保護任何 Web 服務一樣:用 OAuth 2.1 bearer 權杖。

以 OAuth 的術語來說,你的伺服器是資源伺服器(resource server)。它從不讓任何人登入,也從不發出權杖。它只做一件事:查看每個請求上的 Authorization 標頭,判斷裡面的權杖是否有效。

這一頁講的是伺服器端。會探索授權伺服器並取得權杖的用戶端,請見 OAuth 用戶端

三方角色

  • 授權伺服器負責讓人登入並發出存取權杖。這個不用你寫,它就是你的身分提供者(Auth0、Keycloak、Entra,或你自己的)。
  • 資源伺服器就是你的 MCP 伺服器。它在每個請求上驗證權杖。
  • 用戶端會探索你信任哪個授權伺服器,從那裡取得權杖,再以 Authorization: Bearer <token> 的形式送回來給你。

整個三角關係就這樣。這一頁的所有內容都是中間那一項。

權杖驗證器

有效的權杖長什麼樣子,SDK 沒有任何預設立場。由你來告訴它,方法是實作 TokenVerifier

server.py
from pydantic import AnyHttpUrl

from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def list_notes() -> list[str]:
    """List every note in the notebook."""
    return ["Buy milk", "Ship the release"]
  • TokenVerifier 是只有一個非同步方法的 protocol。verify_token 會拿到 Authorization 標頭裡的原始權杖,有效就回傳 AccessToken,無效就回傳 None。沒有別的需要實作。
  • 這個範例是在一張表裡查權杖。真實的實作會驗證 JWT 簽章,或呼叫授權伺服器的權杖內省(token introspection)端點。那段程式碼是你的,SDK 只負責呼叫它。
  • token_verifier=auth= 永遠成對出現。只傳其中一個,MCPServer(...) 在服務任何請求之前就會引發 ValueError

AuthSettings 是資源伺服器對外的門面:

  • issuer_url:發出權杖的授權伺服器。
  • resource_server_url:這個 MCP 端點的公開 URL。它指明權杖是給哪一個資源用的,也是探索文件所在的位置。
  • required_scopes:每個權杖都必須帶有全部這些 scope。

Tip

SDK 儲存庫裡的 examples/servers/simple-auth/ 有一個 IntrospectionTokenVerifier,會呼叫真實授權伺服器的 RFC 7662 端點。大多數正式環境的驗證器都是這個樣子。

透過 HTTP 會得到什麼

授權存在於 HTTP 標頭裡,所以只存在於 HTTP 傳輸方式上。在你要部署的那一種上執行它:mcp.run(transport="streamable-http") 會把它放在 http://127.0.0.1:8000/mcp,其餘內容請見 執行伺服器。應用程式現在有兩個路由:

/mcp
/.well-known/oauth-protected-resource/mcp

你註冊了一個工具。第二個路由是 SDK 的。

探索

對那個 well-known 路徑發 GET,會得到 RFC 9728 Protected Resource Metadata,直接從你的 AuthSettings 產生:

{
  "resource": "http://127.0.0.1:8000/mcp",
  "authorization_servers": ["https://auth.example.com/"],
  "scopes_supported": ["notes:read"],
  "bearer_methods_supported": ["header"]
}

從沒聽過你伺服器的用戶端,就是靠這份文件找到門路的:它讀取 authorization_servers,再去那裡拿權杖。這些你一行都沒寫。

Check

不帶權杖呼叫 /mcp(或帶一個驗證器回傳 None 的權杖),請求會被擋在門口:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"

{"error": "invalid_token", "error_description": "Authentication required"}

什麼都沒被解析,也沒有工具執行。而 WWW-Authenticate 裡那個 resource_metadata 指標,正是讓探索自動化的關鍵:401 -> 中繼資料文件 -> 授權伺服器 -> 權杖 -> 重試。

Warning

這些都不會保護 stdio。管道沒有 Authorization 標頭,所以在那裡永遠不會詢問 token_verifierstdio 伺服器的安全邊界是啟動它的那個處理程序。測試裡用的記憶體內 Client(mcp) 也一樣:它直接連到伺服器物件,跳過了 HTTP 層,授權也一併跳過。

呼叫端的身分

在任何處理函式內,get_access_token() 就是驗證器為目前請求回傳的那個 AccessToken

server.py
from pydantic import AnyHttpUrl

from mcp.server import MCPServer
from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def whoami() -> str:
    """Report which OAuth client is calling."""
    token = get_access_token()
    if token is None:
        return "anonymous"
    return f"{token.client_id} (scopes: {', '.join(token.scopes)})"
  • 在工具、資源和提示詞裡都能用,也不需要傳來傳去:驗證中介軟體會依請求把它存在一個上下文變數裡。
  • 拿回來的是驗證器建立的同一個物件client_idscopessubjectexpires_at,以及你附加的任何額外 claims。這就是逐工具規則的著力點:讀取 scope,然後拒絕。
  • 在已驗證的 HTTP 請求之外,它回傳 None。記憶體內和透過 stdio 時,它永遠是 None

Authorization: Bearer alice-token 呼叫 whoami,模型會讀到:

alice (scopes: notes:read)

SDK 不做的那一半

SDK 給你的是資源伺服器這一半:驗證、公告、拒絕。它不提供登入頁面、同意畫面,也不提供權杖。

想看三方實際互動,可以執行 SDK 儲存庫裡的 examples/servers/simple-auth/(一個小型授權伺服器,加上一個設定方式和這一頁完全相同的資源伺服器),再把 examples/clients/simple-auth-client/ 指向它,跑一遍完整的探索與取得權杖流程。

Info

還有第二個建構子引數 auth_server_provider=,會把完整的授權伺服器嵌進你的 MCP 伺服器裡。它出現的時間早於 MCP 授權規範所依據的 AS/RS 分離設計。新的伺服器不應該使用它。

授權伺服器也可以接受企業身分提供者簽署的斷言,取代使用者點選同意畫面的步驟,而 SDK 支援這種交換的兩端。這種授權方式,以及提出它的用戶端,請見 身分斷言

重點回顧

  • 透過 Streamable HTTP,你的伺服器是 OAuth 2.1 的資源伺服器:它驗證權杖,從不發出權杖。
  • TokenVerifier 就是整個整合介面:一個非同步方法,權杖進去,AccessToken | None 出來。
  • token_verifier=auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...]) 永遠成對出現。
  • SDK 會在 /.well-known/oauth-protected-resource/... 發布 RFC 9728 Protected Resource Metadata,並以 401 回應未驗證的請求,其 WWW-Authenticate 標頭會指向這份文件。整個探索機制就這樣。
  • 在任何處理函式裡,get_access_token() 就是誰在呼叫。
  • 授權是 HTTP 層的事。stdio 和記憶體內用戶端永遠看不到它。

用戶端那一半(探索你的授權伺服器並替你取得權杖)請見 OAuth 用戶端。至於不問使用者、而是直接斷言身分的用戶端,請見 身分斷言