Zum Inhalt springen
lehrr planrDoku

Dokumentation MCP

planr als MCP-Server

planr lässt sich aus jedem Modell bedienen, das MCP spricht - ohne dass jemand HTTP-Aufrufe von Hand baut. Es gibt zwei Wege hinein, mit denselben Werkzeugen:

WegWo er läuftFür wen
POST /api/mcpin planr selbst, über HTTPSCustom Connectors, Claude Code, alles Entfernte
npm run mcpals Prozess auf dem eigenen Rechner, über stdioClients, die nur lokale Prozesse einbinden

Der entfernte Weg ist der normale. Der lokale bleibt für den Fall, dass ein Client keine URL entgegennimmt.

Endpunkt#

POST /api/mcp, Streamable HTTP, ein JSON-RPC-Umschlag je Anfrage. Ausgewiesen wird sich mit demselben persönlichen Schlüssel wie bei den übrigen Routen:

Authorization: Bearer planr_...

Einen anlegen unter Einstellungen → Schnittstellen-Schlüssel (/konto). Der Klartext erscheint einmal und danach nie wieder.

Ohne gültigen Schlüssel antwortet der Endpunkt mit 401 und WWW-Authenticate: Bearer realm="planr" - dort sucht ein MCP-Client nach dem Anmeldeweg.

In Claude Code#

claude mcp add --transport http planr https://planr-ten.vercel.app/api/mcp --header "Authorization: Bearer planr_..."

In Claude Desktop#

Hier klemmt es, und zwar nicht an planr. Ein Custom Connector ist eine URL; Claude erwartet dahinter OAuth 2.1 mit Dynamic Client Registration oder gar keine Anmeldung. Einen festen Schlüssel als Kopfzeile mitzugeben (static_headers) ist bei Anthropic in der Beta und muss freigeschaltet werden - genau das, was dieser Endpunkt schon annimmt.

Bis das freigeschaltet ist, führen drei Wege zum Ziel:

  1. den Beta-Zugang für Kopfzeilen anfragen (mcp-review@anthropic.com), dann trägt jede Lehrkraft ihren eigenen planr-Schlüssel ein und mehr ist nicht zu tun,
  2. planr selbst zum OAuth-Server ausbauen - Neons Managed Better Auth lässt keine eigenen Plugins zu, das wäre also Handarbeit: Registrierung, Zustimmungsseite, Token-Ausgabe, PKCE, zwei .well-known-Dokumente,
  3. übergangsweise den lokalen Weg über stdio nehmen.

Lokaler Weg#

Zwei Umgebungsvariablen:

VariableBedeutung
PLANR_URLAdresse der planr-Instanz, z.B. https://planr-ten.vercel.app oder http://localhost:3000
PLANR_API_TOKENpersönlicher Schlüssel aus /konto

Start von Hand:

PLANR_URL=http://localhost:3000 PLANR_API_TOKEN=planr_... npm run mcp

In einem Client eintragen:

{
  "mcpServers": {
    "planr": {
      "command": "npx",
      "args": ["tsx", "/Users/dein-name/Projekte/planr/mcp/server.ts"],
      "env": {
        "PLANR_URL": "https://planr-ten.vercel.app",
        "PLANR_API_TOKEN": "planr_..."
      }
    }
  }
}

Fehlt eine der beiden Variablen, sagt der Server beim Start welche und beendet sich - er startet nicht halb.

Werkzeuge#

WerkzeugWas es tutRoute dahinter
klassen_auflistenalle Lerngruppen mit Fach und JahrgangGET /api/tu-vorschlag
naechste_stundenächste Doppelstunde mit Stoff, samt Übungsthemen und AuffrischungGET /api/tu-vorschlag?klasse=
tag_uebersichtwas an einem Tag ansteht, über alle LerngruppenGET /api/stunde?datum=
anforderungenMeilensteine mit Anforderungen einer LerngruppeGET /api/anforderungen
stunde_eintragenPlanung einer Doppelstunde eintragen und das Soll festschreibenPOST /api/stunde
rueckmeldungnach dem Unterricht melden, was tatsächlich liefPOST /api/rueckmeldung
stundenplan_entwurfStundenplan prüfen und in Klartext zurückgeben, ohne zu schreibenPOST /api/stundenplan
stundenplan_uebernehmenLerngruppe anlegen und die Termine des Schuljahres erzeugenPOST /api/stundenplan
rahmenplan_lesendie Themen des Rahmenplans für Fach und Jahrgang, mit Kompetenzen und RichtwertGET /api/vorlage
vorlage_entwurfStundenliste einer Vorlage prüfen und in Klartext zurückgeben, ohne zu schreibenPOST /api/vorlage
vorlage_uebernehmendie Stundenliste in die Vorlage schreibenPOST /api/vorlage
zettel_lesenoffene Fehler und Wünsche zu planr, mit dem Pfad, auf dem sie notiert wurdenGET /api/zettel
zettel_antwortenam Eintrag antworten und seinen Stand setzenPOST /api/zettel
nachzutragenwo die Rückmeldung fehlt, über alle LerngruppenGET /api/alltag?was=nachtraege
woche_uebersichtwas zwischen zwei Daten ansteht, über alle KurseGET /api/alltag?was=woche
material_zeigenMaterial einer Stunde und was daran offen istGET /api/alltag?was=material
material_anlegenein Material an eine Stunde hängenPOST /api/alltag
material_abhakenkopiert / auf iServ setzen, über den TitelPOST /api/alltag
tue_erzeugenTÜ in drillr zur Stunde an diesem TagPOST /api/alltag
hausaufgabe_setzenAufgabenzahl in selbr stellen oder zurücknehmenPOST /api/alltag
grobplan_lesenEinheiten eines Kurses, nummeriertGET /api/grobplan
grobplan_aendernEinheit verlängern/kürzen, Leistung verschiebenPOST /api/grobplan

Die lesenden sind als solche gekennzeichnet - daran erkennt ein Client, was er ungefragt aufrufen darf. stundenplan_entwurf zählt dazu: er rechnet, schreibt aber nichts.

grobplan_lesen und grobplan_aendern gehören ebenso zusammen: geändert wird über die Nummern aus der Leseantwort, nie über Namen. Ein kleines Modell erfindet Namen, eine Nummer kann es nur richtig oder falsch treffen - und falsch fällt in der Prüfung auf.

Die beiden Stundenplan-Werkzeuge gehören zusammen. Der Entwurf gibt zurück, was planr aus der Eingabe verstanden hat, dazu eine Prüfsumme; ohne diese Prüfsumme schreibt stundenplan_uebernehmen nicht. Dazwischen gehört ein Mensch, der den Klartext liest - ein Modell liefert plausiblen Unsinn, und aus „9d, Di 1./2." wird sonst unbemerkt ein Mittwoch.

Lerngruppen heißen wie im Stundenplan (9d). Gibt es dieselbe Lerngruppe in zwei Fächern, gehört fach dazu; sonst antwortet planr mit „gibt es in mehreren Fächern". Welche Stunde der Stofffolge an einem Datum liegt, rechnet planr selbst aus - das Datum genügt, eine Stundennummer braucht niemand mitzuzählen.

Vom Rahmenplan zur Stundenliste#

Der Weg, für den rahmenplan_lesen, vorlage_entwurf und vorlage_uebernehmen gebaut sind - einmal je Fach und Jahrgang, danach jahrelang wiederverwendet:

  1. rahmenplan_lesen holt die Vorgabe des Landes: Themen mit Kompetenzen und Stundenrichtwert. "Terme und Gleichungen, ca. 40 Stunden" - keine Stundenfolge.
  2. Das Modell übersetzt sie in einzelne Doppelstunden, aus dem Lehrwerk oder dem schulinternen Fachcurriculum. Aus 40 Stunden werden 20 Zeilen mit eigenem Thema: Wiederholung, Minusklammer, Distributivgesetz, Ausklammern.
  3. vorlage_entwurf gibt in Klartext zurück, was planr verstanden hat - je Einheit die Doppelstunden mit ihrer Zählung, dazu der Abgleich mit den Themen des Plans (was fehlt, was über dem Richtwert liegt). Es schreibt nichts.
  4. Ein Mensch liest den Klartext und bestätigt.
  5. vorlage_uebernehmen schreibt, mit der Prüfsumme aus dem Entwurf.

Schritt 4 ist nicht verhandelbar. Bei "ca. 40 Stunden Terme" liegt ein Modell schnell plausibel daneben, und eine falsche Stundenfolge fällt erst im März auf.

Die Vorlage muss vorher in planr angelegt sein (/vorlage/neu) - ein Name, ein Fach, ein Jahrgang, eine Taktung. Angesprochen wird sie über ihren Namen.

Aufbau#

mcp/api.ts           Zugang über HTTP: Adressbau, Anmeldung, Fehler in Klartext
mcp/inprozess.ts     derselbe Zugang, aber ohne Netz - für den Endpunkt in planr
mcp/werkzeuge.ts     Werkzeugliste mit Eingabeprüfung, je Werkzeug eine Route
mcp/routen.ts        Pfad -> Handler, gemeinsam für den MCP-Endpunkt und den Chat im Kurs
mcp/registrieren.ts  der Server selbst, unabhängig vom Transport
mcp/server.ts        Anschluss über stdio
app/api/mcp/route.ts Anschluss über HTTP

Fachliche Regeln stehen nicht hier, sondern in lib/stundenApi.ts, lib/tuApi.ts, lib/stundenplanApi.ts und lib/anforderungenApi.ts. Die Hülle reicht durch - eine zweite Fassung derselben Regeln wäre eine, die irgendwann abweicht. Aus demselben Grund ruft der Endpunkt die Routen auf und nicht die Bibliotheken darunter: in den Routen stehen die Prüfung der Eingaben und die Form der Antwort.

Der Endpunkt ist zustandslos. Eine Vercel-Funktion lebt für die Dauer einer Anfrage; eine Sitzung im Arbeitsspeicher fände die nächste Anfrage nicht wieder. Also je Anfrage ein frischer Server, ohne Sitzungskennung, mit JSON statt Datenstrom - planr schickt nichts von sich aus. GET und DELETE beantwortet er deshalb mit 405.

Fehler von planr kommen als Text zurück, nicht als Absturz: das Modell liest „9a gibt es in mehreren Fächern (Physik, Mathematik) - fach mitgeben" und kann nachfragen, statt die Verbindung zu verlieren. Eingaben, die schon am Schema scheitern (ein Datum als 25.08.2026), erreichen planr gar nicht erst.