Autorisierung
Übersetzung hinter der englischen Seite zurück
Die englische Seite hat sich geändert, nachdem diese Übersetzung entstanden ist, daher können Teile davon veraltet sein. Lies im Zweifel die englische Seite; Übersetzungen erklärt, wie die übersetzte Dokumentation funktioniert.
Über Streamable HTTP ist dein MCP-Server ein ganz gewöhnlicher Webdienst, und du schützt ihn so, wie du jeden Webdienst schützt: mit OAuth-2.1-Bearer-Tokens.
In der Sprache von OAuth ist dein Server ein Resource Server. Er meldet nie jemanden an und stellt nie ein Token aus. Er tut genau eine Sache: Er sieht sich bei jedem Request den Authorization-Header an und entscheidet, ob das Token darin gültig ist.
Diese Seite behandelt die Server-Seite. Ein Client, der deinen Authorization Server findet und das Token holt, steht unter OAuth-Clients.
Die drei Beteiligten
- Der Authorization Server meldet Personen an und stellt Access Tokens aus. Den schreibst du nicht. Das ist dein Identity Provider (Auth0, Keycloak, Entra, dein eigener).
- Der Resource Server ist dein MCP-Server. Er prüft das Token bei jedem Request.
- Der Client findet heraus, welchem Authorization Server du vertraust, holt sich dort ein Token und schickt es dir als
Authorization: Bearer <token>zurück.
Das ist das ganze Dreieck. Alles auf dieser Seite betrifft den mittleren Punkt.
Ein Token-Verifier
Das SDK hat keine Meinung dazu, wie ein gültiges Token aussieht. Das sagst du ihm, indem du TokenVerifier implementierst:
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"]
TokenVerifierist ein Protokoll mit einer einzigen asynchronen Methode.verify_tokenbekommt das rohe Token aus demAuthorization-Header und gibt einAccessTokenzurück, wenn es gültig ist, undNone, wenn nicht. Mehr gibt es nicht zu implementieren.- Dieser hier schlägt das Token in einer Tabelle nach. Ein echter prüft eine JWT-Signatur oder ruft den Token-Introspection-Endpunkt des Authorization Servers auf. Dieser Code gehört dir; das SDK ruft ihn nur auf.
token_verifier=undauth=treten immer gemeinsam auf. Übergibst du das eine ohne das andere, löstMCPServer(...)einenValueErroraus, bevor auch nur ein Request bedient wird.
AuthSettings ist das öffentliche Gesicht deines Resource Servers:
issuer_url: der Authorization Server, der deine Tokens ausstellt.resource_server_url: die öffentliche URL dieses MCP-Endpunkts. Sie benennt, für welche Ressource ein Token gilt, und unter ihr liegt das Discovery-Dokument.required_scopes: jedes Token muss alle davon tragen.
Tip
examples/servers/simple-auth/ im SDK-Repository enthält einen IntrospectionTokenVerifier, der den
RFC-7662-Endpunkt eines echten Authorization Servers aufruft. Diese Form haben die meisten Verifier in Produktion.
Was du über HTTP bekommst
Autorisierung lebt in HTTP-Headern, es gibt sie also nur auf den HTTP-Transporten. Betreibe sie auf dem, den du bereitstellst: mcp.run(transport="streamable-http") legt sie auf http://127.0.0.1:8000/mcp, und alles Weitere steht in Den Server betreiben. Die App hat jetzt zwei Routen:
/mcp
/.well-known/oauth-protected-resource/mcp
Du hast ein Tool registriert. Die zweite Route stammt vom SDK.
Discovery
Schick ein GET an diesen Well-Known-Pfad, und du bekommst Protected Resource Metadata nach RFC 9728, direkt aus deinen AuthSettings gebaut:
{
"resource": "http://127.0.0.1:8000/mcp",
"authorization_servers": ["https://auth.example.com/"],
"scopes_supported": ["notes:read"],
"bearer_methods_supported": ["header"]
}
Über dieses Dokument findet ein Client, der noch nie von deinem Server gehört hat, den Weg hinein: Er liest authorization_servers und holt sich dort ein Token. Nichts davon hast du geschrieben.
Check
Ruf /mcp ohne Token auf (oder mit einem, für das dein Verifier None zurückgegeben hat), und der Request wird
an der Tür abgewiesen:
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"}
Nichts wurde geparst, kein Tool ist gelaufen. Und der resource_metadata-Verweis in WWW-Authenticate
macht Discovery automatisch: 401 -> Metadaten-Dokument -> Authorization Server -> Token -> erneuter Versuch.
Warning
Nichts davon schützt stdio. Eine Pipe hat keinen Authorization-Header, also wird token_verifier dort nie
befragt. Die Sicherheitsgrenze eines stdio-Servers ist der Prozess, der ihn gestartet hat. Dasselbe
gilt für den In-Memory-Client(mcp), den du in Tests verwendest: Er verbindet sich direkt mit dem Server-Objekt
und überspringt die HTTP-Schicht, Autorisierung eingeschlossen.
Die Identität des Aufrufers
In jedem Handler ist get_access_token() das AccessToken, das dein Verifier für den aktuellen Request zurückgegeben hat:
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)})"
- Es funktioniert in Tools, Ressourcen und Prompts, und du musst nichts herumreichen: Die Auth-Middleware speichert es pro Request in einer Context-Variablen.
- Du bekommst dasselbe Objekt zurück, das dein Verifier gebaut hat:
client_id,scopes,subject,expires_atund alle zusätzlichenclaims, die du angehängt hast. Das ist der Ansatzpunkt für Regeln pro Tool: Lies die Scopes und lehne ab. - Außerhalb eines authentifizierten HTTP-Requests gibt es
Nonezurück. In-Memory und überstdioist es immerNone.
Ruf whoami mit Authorization: Bearer alice-token auf, und das Modell liest:
alice (scopes: notes:read)
Die Hälfte, die das SDK nicht übernimmt
Das SDK gibt dir die Resource-Server-Hälfte: prüfen, bekanntmachen, ablehnen. Es gibt dir keine Login-Seite, keinen Consent-Screen und kein Token.
Um alle drei Beteiligten in Bewegung zu sehen, starte examples/servers/simple-auth/ aus dem SDK-Repository (ein kleiner Authorization Server und ein Resource Server, genau wie auf dieser Seite eingerichtet) und richte dann examples/clients/simple-auth-client/ darauf, um den kompletten Ablauf aus Discovery und Token-Abruf zu sehen.
Info
Es gibt ein zweites Konstruktor-Argument, auth_server_provider=, das einen vollständigen Authorization
Server in deinen MCP-Server einbettet. Es stammt aus der Zeit vor der AS/RS-Trennung, um die herum die
MCP-Autorisierungsspezifikation gebaut ist. Neue Server sollten nicht danach greifen.
Ein Authorization Server kann statt einer Person, die sich durch einen Consent-Screen klickt, auch die signierte Assertion eines Unternehmens-Identity-Providers akzeptieren, und das SDK unterstützt beide Seiten dieses Austauschs. Der Grant und der Client, der ihn vorlegt, stehen unter Identity Assertion.
Zusammenfassung
- Über Streamable HTTP ist dein Server ein OAuth-2.1-Resource-Server: Er prüft Tokens, er stellt nie welche aus.
TokenVerifierist die gesamte Integrationsfläche: eine asynchrone Methode, Token rein,AccessToken | Noneraus.token_verifier=undauth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])treten immer gemeinsam auf.- Das SDK veröffentlicht Protected Resource Metadata nach RFC 9728 unter
/.well-known/oauth-protected-resource/...und beantwortet nicht authentifizierte Requests mit einer 401, derenWWW-Authenticate-Header darauf zeigt. Das ist die ganze Discovery-Geschichte. get_access_token()in jedem Handler sagt dir, wer aufruft.- Autorisierung ist eine HTTP-Angelegenheit.
stdiound der In-Memory-Client bekommen sie nie zu sehen.
Die Client-Hälfte (deinen Authorization Server finden und das Token für dich holen) steht unter OAuth-Clients. Und ein Client, der eine Identität behauptet, statt eine Person danach zu fragen, steht unter Identity Assertion.