Autorisierung
Maschinelle Übersetzung
Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die englische Seite ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt Übersetzungen, wie du es melden kannst.
Ü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.