Tutorial

Wie ein MCP-Client den OAuth-Issuer festnagelt: was nach Python SDK 1.30 und 2.2 noch bleibt

OAuth am Server entscheidet nur, wer ein Tool aufrufen darf. Die Meldung vom 28. September fragt den Client: Glaubst du dem Login-Dienst, den die Discovery gerade genannt hat?

Am 28. September 2026 haben die Maintainer des offiziellen MCP-Python-SDK bestätigt, dass ein betroffener HTTP-Client das Client-Secret, den Authorization Code und den PKCE-Nachweis, die zum echten Login-Dienst gehören, an einen Token-Endpunkt schicken konnte, den die Gegenseite kontrolliert. Die korrigierten Stände sind 1.30.0 auf der 1.x-Linie und 2.2.0 auf der 2.x-Linie. Sie kamen am 7. September, die Release Notes nannten das eine Verhaltensänderung. Die Meldung selbst folgte am 28. September, Cycode veröffentlichte am selben Tag die Untersuchung, am nächsten Tag griffen Sicherheitsmedien sie auf. Der Einstieg ist der Bericht bei The Hacker News. Die heute richtige Client-Form steht in OAuth clients. Wie ein Server ein Token verlangt, bleibt der Beitrag MCP OAuth 2.1. Liegt der Remote-Server nicht vollständig bei euch, steht die Deploy-Form in Remote MCP in Produktion.

Die Meldung repariert den Client, nicht einen Server, den ihr schon geschützt habt

Betroffen ist ein Prozess, der das offizielle Python-SDK als HTTP-Client nutzt und OAuth eingeschaltet hat. Er verbindet sich mit einem MCP-Server, dem er nicht vollständig vertraut, und hält Zugangsdaten eines echten Login-Dienstes. Die Maintainer schreiben: Auf manchen Discovery-Pfaden stand der erwartete Login-Dienst nicht fest, bevor der Client den vom Server genannten übernahm. Die korrigierten Stände legen den erwarteten Issuer zuerst fest und holen danach die Metadaten des Authorization Servers. Das issuer im Dokument muss genau dieser Dienst sein, sonst lehnt der Client ab. Gespeicherte Zugangsdaten tragen den Login-Dienst, ein Datensatz für A wird nicht bei B eingelöst.

Das ist eine andere Schicht als OAuth 2.1 am MCP-Server. Der Server entscheidet, wer ein Tool aufrufen darf und ob das Token für diese Ressource ausgestellt wurde. Der Client entscheidet, ob der in der Discovery genannte Authorization Server der ist, der das Client-Secret ausgestellt hat. Beides braucht ihr. Ein geschützter Server hält den Client nicht davon ab, in der Discovery dem falschen Dienst zu folgen. Ein gepatchter Client deckt keinen Server ab, der die Audience weiter nicht prüft.

Die Änderung auf der Hauptlinie ist PR 3398. Die Metadaten des Authorization Servers müssen als issuer den Server nennen, von dem sie geholt wurden. Das steht in RFC 8414 Abschnitt 3.3. Bei Abweichung stoppen. Keinen Schalter einbauen, der issuer ignoriert.

Was ihr betreibt Diese Meldung Nach dem Upgrade noch zu tun
HTTP-Client, OAuthClientProvider1.9.1–1.29.1 oder 2.0.0–2.1.1Auf 1.30.0 oder 2.2.0, alte client_info löschen
Client Credentials oder Private-Key-JWTDieselben VersionsbereicheZusätzlich issuer übergeben
SDK-Server, stdio oder eigenes TokenAußerhalb dieser MeldungDie Abhängigkeit trotzdem auf einen Fix-Stand ziehen

Zuerst trennen, wer im Umfang liegt

Auf 1.x gilt 1.9.1 bis 1.29.1, auf 2.x 2.0.0 bis 2.1.1. Die Klassen sind OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider und der veraltete RFC7523OAuthClientProvider ohne issuer-Argument. Cycode stuft das Problem als hoch ein. Lokales stdio, Clients mit eigenem Token und Deployments, die das SDK nur zum Schreiben eines Servers nutzen, liegen außerhalb dieser Meldung.

Ein enger Umfang heißt nicht, dass das Repo den alten Pin behalten darf. CLI, Nachtjob und Desktop-Client teilen sich oft eine mcp-Abhängigkeit in derselben Lock-Datei. Auf der Folie nutzt nur der Server das SDK, installiert ist trotzdem ein HTTP-Client auf 1.29. Hochziehen nach der Version in der Lock-Datei, nicht nach der Rolle auf der Architekturfolie.

Auf den alten Ständen nennt die Meldung eine vorläufige Regel: Ein Client mit OAuth verbindet sich nur mit Servern, denen ihr vertraut. Hat er sich schon mit einem Server verbunden, dem ihr nicht vertraut, rotiert das Client-Secret beim Login-Dienst und widerruft die ausgestellten Tokens. Das gilt für Zugangsdaten, die eure Umgebung vielleicht schon verlassen haben. Es ist keine Aufforderung, die Discovery nachzustellen.

Nach 1.30 oder 2.2 braucht Machine-to-Machine weiterhin issuer

Der Browser-Flow OAuthClientProvider legt in den Fix-Ständen den erwarteten Login-Dienst fest, bevor er Metadaten holt. Die zwei Flows ohne Browser tun das nicht. ClientCredentialsOAuthProvider und PrivateKeyJWTOAuthProvider brauchen issuer, gesetzt auf den issuer-Wert im Dokument /.well-known/oauth-authorization-server dieses Authorization Servers. Discovery läuft weiter, Token-Requests entstehen aber nur aus den Metadaten dieser Partei. Zeigt der MCP-Server woandershin, endet der Flow mit OAuthFlowError.

Ohne issuer folgen diese zwei Provider auch im Fix-Stand dem, was die Discovery liefert. Die Meldung ist deutlich: Ohne das Argument erreicht das Upgrade Machine-to-Machine nicht. Auf 1.30.0 ist der Hinweis eine gewöhnliche DeprecationWarning, die Python standardmäßig verbirgt. In CI-Logs fehlt sie oft. In 3.0 wird das Argument Pflicht. Jetzt setzen. Nicht auf die Major-Version warten, bis der Job rot wird.

RFC7523OAuthClientProvider hat kein issuer-Argument. Wer darauf bleibt, kann den Login-Dienst mit dem Upgrade nicht festnageln. Wechsel zu ClientCredentialsOAuthProvider oder PrivateKeyJWTOAuthProvider und übergebt issuer. Das Geheimnis kommt aus der Umgebung oder einem Secret-Manager, nicht aus dem Repo.

In Metadaten, denen ihr vertraut, muss issuer gleich der geholten URL sein

Holt das Dokument von einem Authorization Server, dem ihr schon vertraut, zum Beispiel https://auth.example.com/.well-known/oauth-authorization-server. Das issuer darin muss dieser Server sein. Der Vergleich ist Zeichenfolgengleichheit, ohne Normalisierung. Ein Slash am Ende, andere Großschreibung des Hosts oder ein www ist ein anderer Wert. Übergebt dem Client die exakte Zeichenfolge aus dem Dokument. Tippt keine URL, die nur ähnlich aussieht.

Dieses JSON sind Metadaten eines Authorization Servers, dem ihr vertraut. issuer entspricht dem Origin, von dem ihr geholt habt, und der Token-Endpunkt liegt auf demselben Host.

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
  "code_challenge_methods_supported": ["S256"],
  "authorization_response_iss_parameter_supported": true
}

Protected Resource Metadata sagt dem Client nur, welcher Authorization Server diesen MCP-Server schützt. Sie ersetzt nicht die Identität des Login-Dienstes. Der Client holt danach die Metadaten dieses Servers und prüft issuer. Nennt die Ressourcen-Metadaten A und nennt sich das Authorization-Server-Dokument B, dann stoppen. B nicht pinnen, nur damit die Integration grün wird.

Die Revision vom 28. Juli 2026 stuft die dynamische Registrierung herab und bevorzugt ein Client ID Metadata Document. Ihr veröffentlicht ein Client-JSON unter einer stabilen HTTPS-URL, und diese URL ist die client_id. Das SDK nimmt sie als client_metadata_url. Wirbt der Authorization Server mit Unterstützung, fragt der Client /register nicht mehr nach einem frischen Paar aus Id und Secret. Die URL muss HTTPS mit einem Pfad ungleich der Wurzel sein, geprüft schon beim Konstruieren. Gespeicherte client_info gewinnt weiter gegen das Dokument. Ein alter Datensatz heißt: Der neue Issuer-Pin greift nicht.

Alte client_info löschen, Geheimnisse getrennt rotieren

Der Fix-Stand beschriftet gespeicherte Registrierungen mit dem Login-Dienst. client_info von vor dem Upgrade trägt dieses Label nicht. Die Meldung verlangt, diese Datensätze zu löschen, damit die nächste Registrierung nach der neuen Regel entsteht und an den richtigen Dienst gebunden wird. Nur das Paket hochziehen und die alte Datei behalten heißt: Der Prozess gibt weiter eine ungebundene Registrierung heraus.

Datensatz löschen und Geheimnis rotieren sind zwei Aufgaben. client_info weg, damit der neue Code neu registriert. Client-Secret rotieren und Tokens beim Login-Dienst widerrufen, weil die Zugangsdaten schon einem Server vorgelegen haben können, dem ihr nicht vertraut. Ohne diese Historie rotiert ihr nicht jeden internen Client. Mit dieser Historie macht ihr es beim Login-Dienst. Das Secret gehört nicht in ein Chat-Protokoll.

Gebt state und iss aus dem Callback unverändert an den Provider zurück. Der Provider vergleicht state mit dem selbst erzeugten Wert und iss mit dem entdeckten Issuer. Die zwei Felder sind die Verwechslungsprüfungen, keine Debug-Felder. Ein Callback-Parser, der iss verwirft, entfernt eine Prüfung, die der Fix-Stand gerade ergänzt hat.

Den festgenagelten Issuer ins Repo legen und als Vertrag prüfen

Jeder MCP-Endpunkt bekommt einen Datensatz: die Server-URL und das erlaubte issuer. Den Wert wortgetreu aus den Metadaten des Authorization Servers kopieren. Geprüft wird das formatierte JSON, nicht ein Hostname aus Notizen. Zwischen zwei Releases den Diff ansehen und erkennen, wer issuer auf einen anderen Host gezogen hat.

Diese Zuordnung bleibt im Repo. client_secret bleibt draußen. Das Geheimnis liegt im Secret-Manager. Diese Datei nagelt nur den Login-Dienst fest.

{
  "clients": [
    {
      "mcp_server": "https://orders.example.com/mcp",
      "issuer": "https://auth.example.com"
    }
  ]
}

Der Machine-to-Machine-Konstruktor und diese Liste müssen dieselbe Zeichenfolge nutzen. Die Liste sagt https://auth.example.com und der Code hängt einen Slash an, dann behandelt der Fix-Client das als anderen Dienst und stoppt. Genau dieses Scheitern wollt ihr: Der Job wird auf OAuthFlowError rot, statt still den Token-Endpunkt zu wechseln.

Vor dem Commit den JSON-Formatierer nutzen, um Metadaten und Issuer-Liste aufzuklappen, die JSON-Schema-Prüfung, um issuer und token_endpoint als Zeichenfolgen zu bestätigen, und den JSON-Vergleich, um zu sehen, wer den Login-Dienst geändert hat.

Häufige Fragen

Wenn wir nur einen lokalen stdio-Server betreiben, betrifft uns das?

Ein stdio-Client liegt außerhalb dieser Meldung. Hat dieselbe Umgebung auch einen HTTP-Client auf diesem Paket, zieht ihr nach dessen Version hoch. Steht in der Lock-Datei 1.9.1 bis 1.29.1 oder 2.0.0 bis 2.1.1, geht es auf den passenden Fix-Stand. Nicht überspringen, weil die Folie stdio sagt.

Wir sind auf 1.30.0 und das Log zeigt keine Warnung. Sind wir fertig?

Nein. Fehlt issuer bei ClientCredentialsOAuthProvider oder PrivateKeyJWTOAuthProvider, ist der Hinweis eine standardmäßig verborgene DeprecationWarning. Deprecation-Warnungen einschalten oder issuer im Konstruktor setzen. Ein stilles Log heißt nicht, dass das Argument da ist.

Dynamische Registrierung oder ein Client ID Metadata Document?

Wirbt der Authorization Server mit client_id_metadata_document_supported, veröffentlicht ein Client-JSON unter HTTPS und nutzt diese URL als client_id. Ihr fragt /register nicht bei jedem Treffen nach einem Secret. Wirbt er nicht, fällt das SDK auf dynamische Registrierung zurück. In beiden Fällen gewinnt gespeicherte client_info. Den alten Datensatz nach dem Upgrade löschen.

Wie hängt das mit dem Server-Beitrag zu OAuth 2.1 zusammen?

Der Server-Beitrag beschreibt, wie der Resource Server ein Access Token verlangt. Dieser hier beschreibt, dass der Client nicht dem falschen Login-Dienst folgt. Liegt der Remote-Server nicht hinter eurem eigenen Gateway, beide lesen: Der Server prüft das Token, der Client nagelt issuer im Konstruktor und in der Repo-Liste fest.

Fazit

Die Meldung vom 28. September nimmt „glaub, wen die Discovery nennt“ aus dem Default. HTTP-Clients auf 1.30.0 oder 2.2.0. Für Machine-to-Machine ein issuer, das wortgetreu zu den Metadaten passt. Den veralteten RFC7523-Provider ersetzen.

Danach alte client_info löschen. Hat der Client mit einem Server gesprochen, dem ihr nicht vertraut, Secret rotieren und Tokens beim Login-Dienst widerrufen. Im Repo nur Server-URL und Issuer behalten und diese Zeichenfolge mit Formatierer, Schema-Prüfung und Diff bewachen.