1. Worum es in dieser Vorlesung geht – und was nicht
Das wird eine sehr interessante Vorlesung; darin werden wir:
- das Bild des „Vertrauensdreiecks“ zwischen MCP Client, MCP Server und MCP Auth Server zusammensetzen — mit der Person (User), die „über“ diesem Dreieck als Eigentümer der Ressourcen steht;
- den Flow durchgehen: Wer wem ein Token sendet, wo sich der Benutzer anmeldet und warum der MCP‑Server sein Passwort nie sieht;
- das mit unserem Next.js/MCP‑Backend und der späteren Konfiguration von Keycloak/Auth0 verknüpfen.
Was wir nicht heute tun:
- keine Häkchen in Keycloak klicken und keinen konkreten IdP konfigurieren;
- keine vollständige JWT‑Prüfung oder Introspection implementieren — das sind Themen der nächsten Vorlesungen (über den Auth Server und über den MCP‑Server als geschützte Ressource).
Ziel jetzt ist es, dass Sie ein Blatt Papier nehmen, Pfeile zwischen ChatGPT, Ihrem Server und Auth0/Keycloak zeichnen und ohne zu stocken erklären können: Wo ist das Login, wo ist das Token, wo sind die Daten.
2. Das Vertrauensdreieck: MCP Client, MCP Server, MCP Auth Server
Beginnen wir mit den Akteuren. Das technische „Vertrauensdreieck“ bilden MCP Client, MCP Server und MCP Auth Server; der Benutzer (User) ist eine eigene Rolle, Ressourceneigentümer, der gewissermaßen über diesem Dreieck steht und seine Zustimmung zum Zugriff gibt. In der Spezifik von MCP und dem Apps SDK ist diese Architektur ziemlich klar formalisiert.
User (Resource Owner)
Das ist die Person auf der anderen Seite des Bildschirms. Die Person:
- öffnet ChatGPT;
- stellt eine Anfrage „Zeig mir meine Bestellungen / meine Geschenkelisten“;
- stimmt zu, das Konto Ihres Dienstes mit ChatGPT zu „verknüpfen“.
Wichtig: Sie/Er besitzt die Ressourcen (Bestellhistorie, Profile, Geschenkelisten) und erteilt die Zustimmung zum Zugriff darauf.
MCP Client
Für uns ist das hier:
- ChatGPT mit Apps SDK;
- manchmal — MCP Jam Inspector (beim Debuggen).
Der MCP Client kann:
- die Metadaten Ihres MCP‑Servers lesen (über .well-known);
- den OAuth‑Flow im Browser des Users starten;
- Tokens speichern und an Aufrufe von MCP‑Tools anhängen.
Wichtig: Der MCP Client ist ein Public Client. Er speichert kein client_secret und kommuniziert daher mit dem Auth Server wie eine öffentliche SPA‑Anwendung: Authorization Code + PKCE.
MCP Server (Resource Server)
Das ist Ihr Backend, das MCP implementiert:
- stellt eine Verbindung zu ChatGPT her;
- deklariert Tools (tools), Ressourcen, Prompts;
- prüft bei jedem Tool‑Aufruf den Header Authorization: Bearer <token>;
- validiert das Token (Signatur, exp, aud, scope) und führt bei Erfolg die Business‑Logik aus.
Wesentlich: Der MCP‑Server macht kein Login. Er sieht keine Passwörter, rendert kein Login‑Formular, verschickt keine „Bitte bestätigen Sie Ihre E‑Mail“‑Mails. Er vertraut ausschließlich kryptografisch signierten Tokens des Auth Servers.
MCP Auth Server (Authorization Server / IdP)
Das ist ein separater Dienst für Authentifizierung und Autorisierung: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD usw.
Er ist verantwortlich für:
- das Login‑UI (E‑Mail/Passwort, SSO, 2FA);
- die Verwaltung von Benutzerkonten;
- die Ausgabe von Tokens (Access Token, Refresh Token);
- die Veröffentlichung der OAuth/OIDC‑Metadaten (/authorize, /token, jwks_uri, /registration usw.).
Für MCP sollte er OAuth 2.1 für Public Clients unterstützen (PKCE S256, Dynamic Client Registration usw.).
Zusammenfassende Rollentabelle
| Wer | Was macht er/sie | Was er/sie nicht macht |
|---|---|---|
| User | Gibt Login/Passwort ein, erteilt die Zustimmung zum Zugriff auf Daten | Kommuniziert nicht direkt mit MCP Server |
| MCP Client (ChatGPT/Jam) | Initiiert OAuth, speichert das Token, ruft MCP‑Tools auf | Prüft keine Passwörter, prüft die Token‑Signatur nicht |
| MCP Server | Validiert Tokens, führt die Tool‑Business‑Logik aus | Zeigt kein Login‑Formular, speichert keine Passwörter |
| MCP Auth Server | Loggt den Benutzer ein, gibt Tokens aus | Weiß nichts über Ihre MCP‑Tools und deren Business‑Logik |
Wenn das in Ihrem Kopf bisher zu einem „großen Server, der alles tut“ verschwamm — es ist Zeit, sauber zu trennen.
3. Wie der Flow aussieht: von „kein Token“ bis zum geschützten Tool‑Aufruf
Schauen wir uns den Nachrichtenfluss an. In der MCP‑Spezifikation wird dieser Prozess „The Flow“ genannt: discovery → redirect → code → token → authorized calls.
Schritt 0. Versuch, ein geschütztes Tool ohne Token aufzurufen
Der User schreibt: „Zeig mir meine gespeicherten Geschenkideen.“
ChatGPT als MCP Client entscheidet: „Dafür muss das Tool getUserGiftLists auf unserem MCP‑Server aufgerufen werden.“ Es macht den Aufruf ohne Token (der User hat sich ja noch nicht angemeldet).
Ihr MCP‑Server:
- erkennt einen fehlenden oder fehlerhaften Authorization‑Header;
- antwortet mit 401 Unauthorized und fügt den Header WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" mit Verweis auf die Metadaten des geschützten Ressourcenservers (Resource Metadata, dazu gleich) hinzu.
Das sieht ungefähr so aus (Logik, kein vollständiges HTTP):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT sieht diesen Header und versteht: „Aha, die Ressource ist durch OAuth geschützt, wir müssen den OAuth‑Flow ausführen und das Konto verknüpfen.“
Discovery: .well-known/oauth-protected-resource
Anschließend fordert der MCP Client die Metadaten von Ihrem Server an:
GET /.well-known/oauth-protected-resource
Der Server antwortet mit einem JSON‑Dokument mit der Ressourcen‑ID und einer Liste von Authorization‑Servern, bei denen Tokens zu beziehen sind.
Minimalbeispiel (wir konfigurieren später im Detail, hier zählt die Idee):
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
Hier:
- resource — die kanonische ID Ihrer Ressource; diese sollte später als audience oder resource bei der Tokenausstellung verwendet werden;
- authorization_servers — die Liste der Auth Server, bei denen ChatGPT ein Token anfordern darf;
- scopes_supported — welche „Rechte“ Ihr MCP‑Server überhaupt versteht.
Authorization Request: Redirect zum Auth Server
Nach Erhalt der Metadaten geht der MCP Client zum Auth Server. Er öffnet im Browser einen Tab:
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (Rückleit-URL des MCP Clients)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
Der User:
- sieht den bekannten Login (z. B. Keycloak oder Auth0);
- gibt Login/Passwort ein, besteht die 2FA;
- bestätigt, dass ChatGPT seine Geschenkelisten lesen darf (Scope gifts.read).
Code → Token: Eintausch des Codes gegen ein Token mit PKCE
Nach erfolgreichem Login leitet der Auth Server den Benutzer mit einem code zurück zum MCP Client. Der MCP Client:
- macht einen POST auf /token;
- übermittelt code und code_verifier (der dem code_challenge vom vorherigen Schritt entspricht).
Der Auth Server prüft PKCE: hasht den code_verifier, vergleicht ihn mit dem ursprünglichen code_challenge. Wenn alles passt und der Client tatsächlich derselbe ist, der den Flow gestartet hat, dann:
- gibt er ein kurzlebiges access_token aus (meist JWT);
- trägt darin ein:
- sub — die Benutzer‑ID im Auth Server;
- aud oder resource — Ihren MCP‑Server;
- scope — erlaubte Aktionen (gifts.read, openid usw.).
Authenticated Request: Aufruf eines MCP‑Tools mit Token
Jetzt ist der MCP Client bereit, Ihr Tool erneut aufzurufen, aber mit folgendem Header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Der MCP‑Server:
- prüft die Signatur des Tokens (per JWK des Auth Servers) oder via Introspection;
- prüft die Ablaufzeit (exp);
- prüft aud / resource — dass das Token tatsächlich für https://api.giftgenius.com ausgestellt wurde;
- schaut auf scope und entscheidet, ob getUserGiftLists aufgerufen werden darf.
Danach geht er mit einem userId in Ihre DB und liefert die persönlichen Geschenkelisten.
Beachten Sie, dass wir bis hierhin nur über den Netzwerk‑Flow gesprochen haben: wie ein Token bezogen wird und zum MCP‑Server gelangt. Als Nächstes ist wichtig zu verstehen, wie aus sub und anderen Claims im Token eine konkrete userId in Ihrer DB wird — hier kommt die Identity Bridge ins Spiel.
4. Identity Bridge: wie der ChatGPT‑User zur userId in Ihrer Datenbank wird
Der interessanteste Teil der Architektur ist die „Identity Bridge“. In der MCP‑Spezifikation wird ausdrücklich betont: Der MCP‑Server kennt die ChatGPT‑Benutzer nicht; er verlässt sich auf die Daten im Token des Auth Servers.
Das Schema sieht ungefähr so aus:
flowchart TD User[User in ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(Ihre Datenbank)]
Schritt für Schritt sieht das so aus.
Erstens kennt der Auth Server seine Benutzer intern: Er hat Entitäten user, email, id, eventuell tenant, roles. Bei erfolgreichem Login legt er diese Informationen in das Token (in die Claims):
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
Zweitens zieht der MCP Server beim Validieren des Tokens diese Claims und entscheidet, wer das in seiner Welt ist. Zum Beispiel:
- Wenn sub bereits in der Tabelle User.authProviderId vorhanden ist — nehmen wir die verknüpfte userId;
- wenn nicht — legen wir einen lokalen Datensatz an (On‑the‑fly‑Provisioning) und verknüpfen ihn.
Ein typisches TypeScript‑Snippet auf der MCP‑Server‑Seite (vereinfacht, ohne Signaturprüfung) kann so aussehen:
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
Drittens holt der MCP‑Server mit seiner eigenen userId alles Nötige: Geschenkelisten, Bestellhistorie, Einstellungen, Tarif.
Damit wird der Auth Server zur „Brücke“ zwischen der Außenwelt (ChatGPT, Google, SSO) und Ihrer Innenwelt (customer_id in der Bestell‑Datenbank).
5. Warum man Auth Server und MCP Server trennen sollte
Es mag verlockend sein: „Lassen wir doch meinen MCP‑Server selbst Login anzeigen und Tokens ausstellen.“ Formal ist das möglich (Sie können darin einen Mini‑IdP einbauen), architektonisch ist es aber eine schlechte Idee. Die Gründe sind recht bodenständig.
Erstens: Sicherheit und Skalierbarkeit. Ein Auth Server ist eine schwere Maschine: 2FA, Social Logins, Passwortrichtlinien, Kontosperren, Wiederherstellung, Login‑Audits, ggf. Zertifizierungen. Das alles in jedem Microservice (in jedem MCP‑Server) neu zu schreiben, ist der Weg in die Hölle und Richtung PCI‑DSS. Viel einfacher ist es, das an Keycloak/Auth0 zu delegieren und einfach deren Token zu prüfen.
Zweitens: Austauschbarkeit der Clients. Heute haben Sie nur ChatGPT. Morgen binden Sie Claude Desktop, Ihr Web‑Frontend auf Next.js, eine Mobile App an. Alle können denselben Auth Server und dasselbe OAuth‑2.1‑Schema nutzen, und Ihr MCP‑Server prüft weiterhin Tokens. Sie müssen keine Business‑Logik für jeden neuen Client umschreiben.
Drittens: sauberer Code. Der MCP‑Server sollte idealerweise:
- das Endpoint /.well-known/oauth-protected-resource bereitstellen;
- ein Bearer‑Token prüfen und daraus userId, scopes, tenant extrahieren;
- die Business‑Tools implementieren (orders, gifts, profiles).
Die gesamte UI‑Logik des Logins — Formulare, Layout, Social Logins — lebt im Auth Server und vermüllt Ihr Backend nicht.
6. Wie das in unserer Übungsapp GiftGenius aussieht
Zurück zu der App, die wir im Kurs aufbauen. Angenommen, wir haben:
- eine ChatGPT‑App „GiftGenius“ mit Widget (Apps SDK), die Geschenke vorschlagen kann;
- einen MCP‑Server auf Node/Next.js, der folgende Tools bereitstellt:
- searchGifts — anonym, erfordert kein Login;
- getSavedGiftLists — persönlich, erfordert Authentifizierung;
- einen Auth Server (später: Keycloak/Auth0), bei dem jede Person ein Konto hat.
Szenario: anonymer vs. angemeldeter Nutzer
Wenn der User einfach schreibt: „Finde ein Geschenk für meinen Bruder, 30 Jahre, mag Brettspiele“, kann unsere App:
- das anonyme Tool searchGifts aufrufen;
- Empfehlungen im Interface ausgeben.
In diesem Fall:
- ist kein Token erforderlich;
- führt der MCP‑Server die Anfrage einfach aus (z. B. gegen Ihren Katalog oder eine Fremd‑API).
Sobald der User sagt „Speichere das in meinen Listen“ oder „Zeig mir meine gespeicherten Ideen“, entscheidet das Modell, das geschützte Tool getSavedGiftLists aufzurufen. Der Server antwortet mit 401 + WWW-Authenticate mit resource_metadata. ChatGPT startet den OAuth‑Wizard „Link GiftGenius account“, führt den User durchs Login und erhält ein Token.
Danach gilt bei jedem geschützten Aufruf:
- Der MCP Server sieht bereits Authorization: Bearer ...;
- extrahiert die userId aus dem Token;
- filtert Daten anhand dieser userId.
Dank dessen können wir:
- Daten verschiedener Nutzer trennen;
- sicher die Bestellhistorie, Favoritenlisten anzeigen;
- Commerce‑Funktionen umsetzen (später im Kurs).
Backend‑Architektur: Middleware + Tool‑Handler
In der Praxis sieht das im Node/Next.js‑Code oft wie eine Kette aus: „Authentication‑Middleware → Business‑Handler des Tools“. In der Vorlesung zu den Tool‑Handlern haben wir bereits betont, dass man ihnen den Kontext übergeben sollte: user_id, Tokens, Einstellungen.
Ein Codefragment kann so aussehen:
// auth-context.ts
export type AuthContext = {
userId: string | null; // null für anonyme Aufrufe
scopes: string[];
};
Middleware, die auf alle MCP‑Endpoints gelegt wird:
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // anonymer Benutzer
const claims = await verifyAndDecodeToken(token); // Token‑Verifizierung
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
Und der Tool‑Handler selbst bekommt diesen Kontext:
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
Die Idee ist, dass der Tool‑Handler nichts über OAuth oder PKCE wissen muss. Er arbeitet einfach mit der naheliegenden userId. Die ganze OAuth‑Magie steckt davor: im MCP‑Client und in der Auth‑Middleware.
7. Visuelle Schemata: Wie Client, Server und Auth zusammenleben
Wir haben den Flow in Abschnitt 3 bereits Schritt für Schritt textlich erklärt. Manchmal ist es einfacher, einmal zu zeichnen, als siebenmal zu erklären; daher zeigen wir dieselben Interaktionen jetzt in zwei Diagrammen.
Interaktionsskelett (The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. Chatten| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
So liest man das Schema.
Zuerst meldet sich der User über den Auth Server an, der im Grunde seine Identität bestätigt und ein Token ausstellt. Der MCP Client steuert diesen Prozess und verwendet das Token anschließend, um den MCP‑Server anzusprechen. Der MCP‑Server sieht kein Login/Passwort, er sieht nur das Token und entscheidet, was erlaubt ist.
Ablauf vom Request bis zur Response
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "Zeig mir meine Geschenkelisten" ChatGPT->>MCP: callTool(getSavedGiftLists) (ohne Token) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: Gibt Login/Passwort ein, erteilt Consent Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: JSON mit persönlichen Listen ChatGPT-->>User: Gerenderte Liste im Widget
Das ist das Diagramm, das Sie bis zum Ende des Moduls im Schlaf erklären können.
8. Ein bisschen tiefer: mehrere Ressourcen, mehrere Clients, DCR
Der Vorteil dieser Architektur: Sie skaliert.
Erstens können Sie mehrere MCP‑Server haben (z. B. einen für Geschenke, einen für Bestellungen) und einen Auth Server, der Tokens mit unterschiedlichen aud/resource ausstellt. Jeder Ressourcen‑Server muss prüfen, dass das Token wirklich für ihn bestimmt ist, sonst entsteht das klassische „confused deputy“‑Problem, wenn ein Token für einen Dienst von einem anderen akzeptiert wird.
Zweitens können Sie viele Clients haben:
- ChatGPT App;
- Ihr eigenes Frontend;
- eine Mobile App;
- eine Partner‑Integration über ein MCP Gateway.
Alle werden:
- /.well-known/oauth-protected-resource lesen;
- herausfinden, wo sich der Auth Server befindet;
- den OAuth‑2.1‑Flow durchlaufen;
- Tokens erhalten und den MCP‑Server aufrufen.
Drittens unterstützen moderne Auth Server immer häufiger Dynamic Client Registration (DCR) — die Möglichkeit, Clients per API dynamisch zu registrieren. Die MCP‑Spezifikation impliziert genau diese Möglichkeit: Der Client (ChatGPT/Jam) kann sich automatisch am Auth Server über dessen registration_endpoint registrieren.
In diesem Modul ist wichtig zu verstehen, dass:
- MCP Client, MCP Server und Auth Server über standardisierte Discovery‑Dokumente und Tokens kommunizieren;
- Sie nicht alle Clients „hart“ im Backend‑Code verdrahten müssen;
- Sie das Ökosystem ausbauen können, ohne das bestehende Autorisierungsmodell zu brechen.
9. Typische Missverständnisse in der MCP‑Autorisierungsarchitektur
Fehler Nr. 1: „Der MCP‑Server soll den Benutzer selbst einloggen“.
Manchmal versuchen Entwickler, ein Login‑Formular direkt in den MCP‑Server einzubauen und dann Login/Passwort über Tools zu schicken. Das bricht die Idee von OAuth. Der MCP‑Server darf Passwörter unter keinen Umständen sehen. Login und Consent sind die Verantwortung des Auth Servers. Der MCP‑Server arbeitet nur mit Tokens und deren Claims.
Fehler Nr. 2: Verwechslung zwischen MCP Client und MCP Server.
Mitunter wird ChatGPT als „Teil meines Backends“ wahrgenommen, und man versucht etwa, dort Secrets zu speichern oder erwartet, dass es selbst Zugriffsrechte prüft. In Wahrheit initiiert der MCP Client lediglich OAuth und hängt Tokens an. Die Prüfung des Tokens und der Berechtigungen ist Aufgabe des MCP‑Servers, nicht von ChatGPT.
Fehler Nr. 3: „API‑Key in der .env statt OAuth“.
Ein klassisches Antipattern: einen großen SERVICE_API_KEY anlegen, in .env des MCP‑Servers ablegen und glauben, die Aufgabe sei gelöst. So gibt es keine nutzerbezogene Rechte‑Trennung, persönliche Daten lassen sich nicht sicher anzeigen oder Käufe durchführen; alles passiert „im Namen des Service“, nicht des Users. Das widerspricht den Zielen der Autorisierung in ChatGPT Apps.
Fehler Nr. 4: Ignorieren von audience und resource.
Wenn der MCP‑Server jedes gültige JWT mit passender Signatur akzeptiert und nicht auf aud/resource schaut, kann jedes Token, das für einen anderen Service vom selben Auth Server ausgestellt wurde, zum Aufruf Ihrer Tools verwendet werden. Das verletzt das Sicherheitsmodell von OAuth direkt. Der Server muss prüfen, dass das Token genau für seine resource ausgestellt wurde.
Fehler Nr. 5: Vermischen von Auth‑Logik und Business‑Logik.
Mitunter wird die gesamte Token‑Analyse, Signaturprüfung, Arbeit mit JWK usw. in die Tool‑Handler gezogen. Der Code wird dadurch fragil und schwer wartbar. Sauberer ist es, die Schicht „Token prüfen, auf userId mappen“ (Middleware) von der Schicht „eigentliche Tool‑Logik“ zu trennen, die bereits einen verständlichen AuthContext bekommt.
Fehler Nr. 6: Erwartung, dass ChatGPT „alles selbst erledigt“ ohne .well-known.
Ohne den korrekten Endpoint /.well-known/oauth-protected-resource weiß der MCP‑Client schlicht nicht, wo Ihr Auth Server ist und welche Scopes benötigt werden. Ergebnis — der Chat „kann sich nicht einloggen“, und der Entwickler starrt lange in leere Logs. Der richtige Weg: Der MCP‑Server deklariert seine Autorisierungsanforderungen klar über .well-known, der Client liest sie und baut den Flow.
Fehler Nr. 7: Den User in der Business‑Logik vergessen.
Manchmal wird, selbst bei korrekt eingerichtetem OAuth und Mapping des Tokens auf die userId, diese nicht in DB‑Queries verwendet: z. B. wird vergessen, nach ownerId = userId zu filtern. Dann kann jeder autorisierte Nutzer fremde Daten sehen. Das Vorhandensein eines Tokens ist nur der erste Schritt; der zweite ist immer die korrekte Verwendung von userId und scope im Business‑Code.
GO TO FULL VERSION