Zum Inhalt

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:

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 ist ein Protokoll mit einer einzigen asynchronen Methode. verify_token bekommt das rohe Token aus dem Authorization-Header und gibt ein AccessToken zurück, wenn es gültig ist, und None, 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= und auth= treten immer gemeinsam auf. Übergibst du das eine ohne das andere, löst MCPServer(...) einen ValueError aus, 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:

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)})"
  • 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_at und alle zusätzlichen claims, 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 None zurück. In-Memory und über stdio ist es immer None.

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.
  • TokenVerifier ist die gesamte Integrationsfläche: eine asynchrone Methode, Token rein, AccessToken | None raus.
  • token_verifier= und auth=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, deren WWW-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. stdio und 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.