WLO MCP Server
MCP server for WirLernenOnline.de that enables searching and retrieving educational materials, collections, topic pages, and metadata through natural language, compatible with OpenAI and Claude.
README
WLO MCP Server
WLO MCP ist ein Model Context Protocol Server für WirLernenOnline.de (WLO) – die deutsche OER-Plattform für freie Bildungsmaterialien.
Kompatibel mit OpenAI (Responses API + native MCP), Anthropic Claude und allen anderen MCP-fähigen Clients.
Inhaltsverzeichnis
- Was ist neu in v2
- Installation
- Tools im Überblick
- Tools – Detail & API-Endpunkte
- Filter-Parameter & Vokabular
- Output-Formate (markdown vs. json)
- Determinismus & Stabilität
- Deployment
- Konfiguration in AI-Clients
- Architektur
- Migration v1 → v2
- Kompatibilität
Was ist neu in v2
v2.1 — Performance & kombinierte Suche (2026-06)
search_wlo_all— neues Tool: Einzel-Inhalte + Sammlungen + Themenseiten in EINEM parallelen Aufruf, mit getrennten Töpfen (content/collections/topicPages). Spart dem Client mehrere separate Such-Aufrufe (= weniger Round-Trips / Cold-Starts).- Kuratierter
propertyFilter— Such-/Bulk-Fetches fordern nur die ~24 real genutzten Felder statt-all-(~59) → deutlich kleinere Payloads._DISPLAYNAME-Labels bleiben erhalten;get_node_detailsbleibt bewusst auf-all-. - Einheitliches Reranking —
rerankNodesgreift jetzt auch für Sammlungen und Themenseiten (zuvor nur Inhalte): Exakt-Treffer rücken nach oben, off-topic raus. Sicher — nur Umsortierung + Entfernen gelöschter Knoten, kein Score-Drop. - Such-Varianten gedeckelt — Query-Expansion auf max. 5 parallele
ngsearch-Calls (Einzelterm-Varianten entfernt). WLO_POOL_SIZE(Env, Default 25, vorher fix 40) — Kandidaten-Pool je Such-Variante.- Details, Einstellungen + Messungen: siehe
PERFORMANCE.md.
Hinzugekommen
get_subject_portals— listet die Fachportale (Top-Level-Sammlungen unter dem WLO-Wurzelknoten) deterministisch alphabetisch.browse_collection_tree— strukturierter Drilldown in Sub-Sammlungen (Tiefe 1 oder 2), optional mit File-Counts.wlo_health_check— Probe gegen die WLO-API, gibt Latenz + Status zurück.get_nodes_details— Bulk-Metadata für mehrerenodeIdsparallel.outputFormat: "json"für alle Such- und Detail-Tools — strukturierte Daten statt Markdown-Parsing.excludeNodeIds: string[]für die drei Such-Tools — bereits gesehene IDs aus Folge-Calls ausblenden.- License- und TargetGroup-Vocabularies in
lookup_wlo_vocabulary(vocabulary: "license"/"targetGroup").
Verbessert
search_wlo_topic_pages:- Mode C (Liste-aller) zeigt jetzt die Sammlungsnamen als Titel statt kryptischer
PAGE_VARIANT_TEMPLATE_xxx-Slugs (Auto-Resolve viagetNodeParents). - Mehrere Varianten derselben Sammlung werden per
mergeVariants=true(Default) zu einer Karte zusammengefasst. targetGroupLabelliefert lesbare Labels ("Lehrkräfte"/"Lernende"/"Allgemein") statt Slugs /ccrep://-URIs.sort: "alpha" | "relevance"mit deterministischem Tie-Breaker aufnodeId.
- Mode C (Liste-aller) zeigt jetzt die Sammlungsnamen als Titel statt kryptischer
get_node_details:- Output-Felder sind jetzt identisch zu
formatNode()der Such-Tools —disciplines,educationalContexts,userRoles,learningResourceTypes,licenseals menschenlesbare Labels ("Mathematik","CC BY-SA 4.0") statt URIs. - Optional
includeRaw: truezeigt zusätzlich die unaufgelösten URIs für Debugging / Spezialfälle.
- Output-Felder sind jetzt identisch zu
- Lizenz-Mapping: Roh-Keys wie
CC_BY_SAwerden überall zu"CC BY-SA 4.0"aufgelöst. - Reranker (
reranker.ts): deterministischer Tie-Breaker aufnodeIdbei gleichem Score; neuesortByTitle()für stabile alphabetische Sortierung.
Entfernt (Breaking Changes)
get_wirlernenonline_info,get_edu_sharing_network_info,get_edu_sharing_product_info,get_metaventis_info— die Webseiten-Crawler-Tools sind weg. Diese Aufgabe übernimmt jetzt das Konsumenten-eigene RAG (z.B. das BadBoerdi-RAG).- Damit auch
WEB_CONTENT_WHITELISTundfetchWebContent()auswlo-api.tsentfernt. environment-Parameter aus allen Tool-Schemas entfernt. Die Repository-Auswahl läuft jetzt ausschließlich über die Env-VariableWLO_REPOSITORY_URL(siehe „Umgebungsvariablen"). Pro Server-Instanz wird genau eine Edu-Sharing-Welt adressiert; Konsumenten zeigen ihren MCP-Client auf die jeweilige URL.WLO_ENVist weg — ersetzt durchWLO_REPOSITORY_URL.WLO_ROOT_COLLECTION_IDS(Record) wurde zuWLO_ROOT_COLLECTION_ID(eine Konstante, optional per Env überschreibbar).
Backward-Kompatibilität für MCP-Clients: Tool-Inputs werden mit Zod's Default-Mode (
.strip) validiert — schickt ein alter Client denenvironment-Key trotzdem mit, wird er silent ignoriert, ohne Crash. Der Server antwortet aber immer mit den Daten der konfigurierten Repository-URL. Wer aktiv Staging-Daten wollte, muss seinen MCP-Client auf den Staging-Deployment-Endpoint umstellen (z.B.https://wlo-mcp-server-staging.vercel.app/mcp).
Installation
Voraussetzungen
- Node.js ≥ 18.x
- npm ≥ 9.x
Lokale Installation
git clone https://github.com/yourorg/wlomcp.git
cd wlomcp
npm install
npm run build
Umgebungsvariablen
cp .env.example .env
| Variable | Werte | Standard | Beschreibung |
|---|---|---|---|
WLO_REPOSITORY_URL |
URL des Edu-Sharing-Frontend-Hosts (z.B. https://redaktion.openeduhub.net/edu-sharing oder https://repository.staging.openeduhub.net/edu-sharing) |
WLO-Production | Edu-Sharing-Instanz, gegen die der Server arbeitet. Pfade sind in allen Instanzen identisch (<base>/rest/... für REST, <base>/components/... für Frontend) — daher reicht die Base-URL als Schalter zwischen Prod / Staging / eigenem Repository. Eingabe-Toleranz: Whitespace, Trailing-Slash(es) und ein angehängtes /rest werden automatisch entfernt; fehlendes Protokoll wird zu https:// ergänzt. Bei verdächtigen Eingaben (deep links auf /components/..., doppeltes /edu-sharing) loggt der Server beim Start eine Warnung. |
WLO_ROOT_COLLECTION_ID |
UUID | 5e40e372-735c-4b17-bbf7-e827a5702b57 |
Wurzelknoten der Sammlungs-Hierarchie. Identisch auf WLO-Production und -Staging. Override nur nötig, wenn ein eigenständiges Repository mit anderem Root läuft. |
PORT |
Zahl | 3000 |
HTTP-Port (nur HTTP-Modus) |
WLO_POOL_SIZE |
Zahl | 25 |
Kandidaten-Pool je Such-Variante (enhancedSearch) fürs Reranking — nicht die ausgelieferte Trefferzahl (das ist maxResults). Kleiner = schnellere/kleinere Fetches bei minimal geringerem Recall. |
Ein Server = ein Repository. Der MCP zeigt pro Prozess auf genau eine Edu-Sharing-Instanz. Wer parallel beide Welten anbieten will, deployt zwei Instanzen (z.B. zwei Vercel-Projekte mit unterschiedlichem
WLO_REPOSITORY_URL).
Server starten
node dist/http.js # HTTP-Modus → http://localhost:3000/mcp
node dist/stdio.js # stdio (Claude Desktop, lokale Clients)
npm run dev # stdio mit Auto-Reload
npm run dev:http # HTTP mit Auto-Reload
Tools im Überblick
| # | Tool | Zweck | Output-Formate |
|---|---|---|---|
| 1 | search_wlo_collections |
Sammlungen suchen (Volltext + Tree-Fallback) | markdown / json |
| 2 | search_wlo_content |
Globale Volltextsuche nach Bildungsinhalten | markdown / json |
| 3 | get_collection_contents |
Inhalte/Sub-Sammlungen einer Sammlung (paginierbar) | markdown / json |
| 4 | get_node_details |
Detail-Metadaten + optional Volltext + Eltern | markdown / json |
| 5 | lookup_wlo_vocabulary |
Vokabular-Werte: Bildungsstufe, Fach, Zielgruppe, LRT, Lizenzen, TargetGroup | markdown |
| 6 | search_wlo_topic_pages |
Themenseiten finden / listen, Varianten zusammenfassen | markdown / json |
| 7 | get_subject_portals |
Fachportale (Top-Level-Sammlungen unter WLO-Root) | markdown / json |
| 8 | browse_collection_tree |
Drilldown in Sub-Sammlungen (Tiefe 1–2), optional mit File-Counts | markdown / json |
| 9 | wlo_health_check |
Status + Latenz der WLO-API | json |
| 10 | get_nodes_details |
Bulk-Metadata für mehrere nodeIds parallel |
json |
| 11 | search_wlo_all |
Kombiniert: Inhalte + Sammlungen + Themenseiten in EINEM parallelen Aufruf, getrennte Töpfe | json / markdown |
Konzept: In WLO sind Sammlungen und Themenseiten dasselbe. Eine Sammlung wird im Repository als Themenseite angezeigt und bündelt Inhalte in Schwimmlinien (Swimlanes/Karussells). Sub-Sammlungen entsprechen Unter-Themenseiten. Sammlungen mit
ccm:page_config_refhaben eine kuratierte Themenseite mit zielgruppenspezifischen Varianten (Lehrkräfte / Lernende / Allgemein).
Tool-Routing-Heuristik (für LLMs):
- User fragt breit nach einem Thema und will Inhalte + Sammlungen + Themenseiten gemeinsam →
search_wlo_all(ein Aufruf, getrennte Töpfe)- User fragt nach Material/Inhaltstyp (Video, Arbeitsblatt, …) →
search_wlo_content- User fragt nach Themenseite/Sammlung zu einem Thema →
search_wlo_topic_pages(Mode B mit query)- User will durch ein Fach navigieren (Drilldown) → erst
get_subject_portals, dannbrowse_collection_tree- User klickt eine Karte →
get_node_detailsmit dieser nodeId- Bot zeigt 10 Karten und braucht Metadaten zu allen →
get_nodes_details(nodeIds=[...])(1 Aufruf statt 10)
Tools – Detail & API-Endpunkte
1. search_wlo_collections
Sucht thematische Sammlungen. Drei-stufige Strategie: Volltext-API → Baum-Traversierung Level 2 (≤25 Parents) → Level 3 (≤15 Parents). Treffer werden bei vorhandenem query nach Relevanz gerankt (rerankNodes), bevor maxResults greift — bringt Exakt-Treffer nach oben.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
query |
string | "" |
Suchbegriff. Leer = Top-Level-Sammlungen unter Root oder parentNodeId |
parentNodeId |
string | – | nodeId einer Eltern-Sammlung für Sub-Tree-Suche |
educationalContext |
string | – | Bildungsstufe (Label oder URI) |
discipline |
string | – | Fach (Label oder URI) |
userRole |
string | – | Zielgruppe (Label oder URI) |
maxResults |
int | 5 |
1–20 |
excludeNodeIds |
string[] | – | Diese IDs in der Antwort überspringen |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
API-Endpunkte:
POST /search/v1/queries/-home-/mds_oeh/collections?contentType=COLLECTIONS (Volltext)
GET /node/v1/nodes/-home-/{nodeId}/children?filter=folders (Tree)
2. search_wlo_content
Globale Volltextsuche nach Bildungsmaterialien (Files). Mit Multi-Query-Expansion + RRF (clientseitig, ohne Transformer-Modell — Vercel-tauglich).
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
query |
string | Pflicht | Suchbegriff |
educationalContext |
string | – | Bildungsstufe |
discipline |
string | – | Fach |
userRole |
string | – | Zielgruppe |
learningResourceType |
string | – | Ressourcentyp (Arbeitsblatt, Video, …) |
publisher |
string | – | Anbieter-Filter (Klexikon, Serlo, ZUM, …) |
maxResults |
int | 8 |
1–20 |
excludeNodeIds |
string[] | – | IDs überspringen |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Reranking-Pipeline:
- Query Expansion: Volltext, Title-Match, Keyword-Hits, Synonym-Map — gedeckelt auf max. 5 Varianten (nach Gewicht,
full:immer dabei) - Parallele API-Calls (
WLO_POOL_SIZETreffer / Variante, Default 25) - RRF (Reciprocal Rank Fusion) mit Variant-Gewichtung
- Quality Score (Titel: 30 Pt., Keywords: 10 Pt., Beschreibung: 8 Pt., Metadaten-Qualität)
- Endformel:
0.8 × quality + 0.1 × rrf + 0.1 × multi_appearance_bonus - Tie-Breaker:
nodeId.localeCompare()für deterministische Reihenfolge bei gleichen Scores
3. get_collection_contents
Inhalte einer Sammlung. Unterstützt Pagination via skipCount und Filter.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
nodeId |
string | Pflicht | nodeId der Sammlung |
query |
string | – | Optional zum Reranking |
contentFilter |
enum | "files" |
"files" / "folders" / "both" |
includeSubcollections |
bool | false |
Rekursive BFS-Traversierung (nur bei files) |
maxResults |
int | 20 |
1–100 |
skipCount |
int | 0 |
Pagination-Offset |
excludeNodeIds |
string[] | – | IDs überspringen |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Tipp: Statt
contentFilter="folders"istbrowse_collection_tree(Tool 8) klarer und liefert direkt File-Counts.
4. get_node_details
Detail-Metadaten zu einem einzelnen Node (Content oder Sammlung).
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
nodeId |
string | Pflicht | – |
includeTextContent |
bool | false |
Gespeicherten Volltext (gecrawlt) abrufen, max. 4000 Zeichen |
includeParents |
bool | false |
Eltern-Sammlungen mitliefern |
includeRaw |
bool | false |
Zusätzlich die rohen URI-Werte (vor Label-Resolution) |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Output-Konsistenz: Im JSON-Modus liefert get_node_details dieselben Felder wie ein Eintrag aus search_wlo_* (Output-Format formatNode()):
{
"nodeId": "bd8be6d5-…",
"title": "Mathematik",
"description": "…",
"keywords": ["…"],
"disciplines": ["Mathematik"],
"educationalContexts": ["Sekundarstufe i", "Hochschule"],
"userRoles": [],
"learningResourceTypes": [],
"license": "CC BY-SA 4.0",
"publisher": "ZUM",
"url": "https://…",
"previewUrl": "https://…",
"topicPageUrl": "https://…/topic-pages?collectionId=…",
"renderUrl": "https://…/components/render/…",
"nodeType": "collection",
"parents": [{"nodeId": "…", "title": "…"}], // wenn includeParents
"textContent": "…", // wenn includeTextContent
"raw": { … } // wenn includeRaw
}
5. lookup_wlo_vocabulary
Listet Vokabular-Werte. Quelle: lokale src/vocabs.ts (keine API-Calls).
| Parameter | Werte |
|---|---|
vocabulary |
educationalContext, discipline, userRole, lrt, license, targetGroup |
6. search_wlo_topic_pages
Themenseiten finden, listen oder eine spezifische Sammlung prüfen.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
query |
string | "" |
Thematische Suche (Mode B) |
targetGroup |
enum | – | "teacher" / "learner" / "general" |
educationalContext |
string | – | Bildungsstufe |
collectionId |
string | – | Direkt-Check einer Sammlung (Mode A) |
mergeVariants |
bool | true |
Mehrere Varianten derselben Sammlung in eine Karte zusammenfassen |
sort |
enum | Query→"relevance", sonst "alpha" |
"alpha" (deterministisch) oder "relevance". Bei Themen-Query (Mode B) ist Relevanz Default (Eingangs-Sammlungen werden gerankt); beim reinen Auflisten (Mode C) alphabetisch. |
maxResults |
int | 5 |
1–20 |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Drei Suchmodi:
- Mode A — Direkt-Check (
collectionIdgesetzt): prüftccm:page_config_refder Sammlung, traversiert Config-Kinder → Varianten - Mode B — Thematische Suche (
querygesetzt): Sucht Sammlungen per Keyword, filtert aufpage_config_ref - Mode C — Alle auflisten (kein
query, keincollectionId): Nutzt diepage_variant-API. Variant-Owner-Sammlung wird übergetNodeParents-Walk aufgelöst → echter Sammlungsname stattPAGE_VARIANT_TEMPLATE_xxx
JSON-Output (Beispiel):
{
"total": 1,
"results": [{
"title": "Physik",
"collectionId": "94f22c9b-…",
"topicPageUrl": "https://…/topic-pages?collectionId=…",
"educationalContexts": ["Sekundarstufe I", "Sekundarstufe II"],
"variants": [
{"variantId": "…", "targetGroup": "teacher", "targetGroupLabel": "Lehrkräfte", "topicPageUrl": "…"},
{"variantId": "…", "targetGroup": "learner", "targetGroupLabel": "Lernende", "topicPageUrl": "…"}
]
}]
}
7. get_subject_portals
Liefert die WLO-Fachportale — die direkten Sub-Sammlungen unter dem Wurzel-Knoten.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
educationalContext |
string | – | Filter (Default: alle) |
includeContentCounts |
bool | false |
Zusätzlich subCollectionCount pro Portal |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Output ist deterministisch alphabetisch (Tie-Breaker nodeId). Ideal als Einstiegspunkt für geführte Drilldowns.
API:
GET /node/v1/nodes/-home-/{ROOT_ID}/children?filter=folders
8. browse_collection_tree
Drilldown unter eine bestimmte Sammlung — Tiefe 1 (direkte Kinder) oder 2 (Enkel-Knoten).
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
nodeId |
string | Pflicht | Eltern-Sammlung |
depth |
int | 1 |
1 oder 2 |
includeContentCounts |
bool | false |
Pro Sammlung den File-Count mitholen |
maxResults |
int | 50 |
1–100 |
outputFormat |
enum | "markdown" |
"markdown" oder "json" |
Achtung Latenz:
depth=2+includeContentCounts=truemachtO(parents × children + parents)API-Calls. Bei breiten Bäumen entsprechend lang.
9. wlo_health_check
Probe gegen die WLO-API. Nimmt keine Parameter — testet die durch WLO_REPOSITORY_URL konfigurierte Instanz.
Output (JSON):
{
"ok": true,
"repositoryUrl": "https://redaktion.openeduhub.net/edu-sharing",
"baseUrl": "https://redaktion.openeduhub.net/edu-sharing/rest",
"rootNodeId": "5e40e372-735c-…",
"rootResolved": "Portale",
"latencyMs": 392,
"checkedAt": "2026-04-27T16:28:04.108Z"
}
Bei Fehler: ok: false, error: "<message>", isError: true im MCP-Result. Nützlich für Konsumenten, um "WLO ist down" von "deine Query liefert keine Treffer" zu unterscheiden.
10. get_nodes_details
Bulk-Fetch für mehrere nodeIds (max. 50 / Aufruf, parallel).
| Parameter | Typ | Default |
|---|---|---|
nodeIds |
string[] | Pflicht |
Output:
{
"requested": 3,
"resolved": 2,
"failed": ["00000000-0000-0000-0000-000000000000"],
"results": {
"bd8be6d5-…": { "title": "Mathematik", "disciplines": ["Mathematik"], … },
"742d8c87-…": { "title": "Informatik", "disciplines": ["Informatik"], … }
}
}
Einzelne Fehler (gelöschte Node, Netzwerk-Fehler) landen im failed[] — der Batch crasht nicht.
11. search_wlo_all
Kombinierte Suche: liefert Einzel-Inhalte + Sammlungen + Themenseiten in EINEM Aufruf, intern parallel (Promise.all). Spart dem Client mehrere separate Such-Aufrufe. Themenseiten = Sammlungen mit ccm:page_config_ref → eine Sammlungssuche bedient beide Töpfe (kein separater Durchlauf). Nutzt bewusst den schnellen Keyword-Pfad für Sammlungen (nicht den Baum-Fallback) → niedrige Concurrency.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
query |
string | Pflicht | Suchbegriff |
educationalContext |
string | – | Bildungsstufe (Label oder URI) |
discipline |
string | – | Fach (Label oder URI) |
userRole |
string | – | Zielgruppe (Label oder URI) |
learningResourceType |
string | – | Ressourcentyp (Label oder URI) |
publisher |
string | – | Anbieter-Filter |
maxContent |
int | 8 |
Max. Einzel-Inhalte (1–50) |
maxCollections |
int | 5 |
Max. je Sammlungen / Themenseiten (1–20) |
include |
string[] | alle | Teilmenge aus content / collections / topicPages |
excludeNodeIds |
string[] | – | Bereits gesehene IDs überspringen |
outputFormat |
enum | "json" |
"json" (getrennte Töpfe, für maschinelle Aufteilung) oder "markdown" |
JSON-Output (Envelope mit getrennten Töpfen):
{
"query": "Photosynthese",
"content": { "total": 209, "count": 8, "results": [ /* FormattedNode[] */ ] },
"collections": { "total": 6, "count": 5, "results": [ /* … */ ] },
"topicPages": { "total": 1, "count": 1, "results": [ /* Sammlungen mit topicPageUrl */ ] }
}
Jeder Topf trägt zusätzlich ein _queryMeta (Such-URL für den jeweiligen Topf). Alle drei Listen sind reranked (Relevanz). Der Client kann die Töpfe direkt in getrennte Anzeige-Bereiche einsortieren.
Filter-Parameter & Vokabular
Alle Vocab-Filter akzeptieren deutsche Labels, englische Synonyme oder vollständige URIs:
| Parameter | Beispielwerte | Vocabulary-Key |
|---|---|---|
educationalContext |
Primarstufe, Sekundarstufe I, Hochschule, vocational education |
educationalContext |
discipline |
Mathematik, Mathe, mathematics, Biologie, Bio, MINT |
discipline |
userRole |
Lehrer/in, teacher, Lerner/in, Eltern, parent |
userRole |
learningResourceType |
Arbeitsblatt, Video, Quiz, interactive media |
lrt |
Vocab-Resolution: zwei asymmetrische Pfade
Der Server unterscheidet bewusst zwischen Anzeige (URI → Label) und Filter-Eingabe (Label → URI):
Anzeige-Pfad (URI → Label) — vollständige Abdeckung über _DISPLAYNAME
Beim Aufbau eines FormattedNode (alle Such-/Detail-Tools) werden Vocab-Felder so resolvt:
- Bevorzugt: server-seitiges
<property>_DISPLAYNAME, das edu-sharing für jeden indexierten Knoten direkt mitliefert.- Funktioniert für
ccm:taxonid,ccm:educationalcontext,ccm:oeh_lrt_aggregated,ccm:oeh_intended_end_user_role. - Deckt automatisch beide Disziplin-Vokabulare ab: das Schulfächer-Vocab UND die Hochschulfächersystematik, ohne dass wir die ~100 Hochschul-Klassen lokal pflegen müssen.
- Beispiel: ein Hochschul-Knoten mit
taxonid=[".../hochschulfaechersystematik/n71", ".../n8"]zeigtdisciplines: ["Studienbereich Informatik", "Ingenieurwissenschaften"].
- Funktioniert für
- Fallback: lokales
labelFromUri-Lookup (vocabs.ts).- Greift, wenn
_DISPLAYNAMEleer ist (z.B.ccm:commonlicense_keyhat nie ein DISPLAYNAME — daher die lokale License-Map).
- Greift, wenn
- Letzter Fallback: rohe URI — damit der Konsument zumindest etwas zurückbekommt.
Rauschfilter: Vokabular-Root-URIs (z.B. nur .../discipline/ ohne konkretes Fach-Slug) werden ausgefiltert — sonst würde DISPLAYNAME den Vokabular-Titel ("Schulfächer", "Destatis-Systematik der Fächergruppen, Studienbereiche und Studienfächer") als Disziplin anzeigen.
Filter-Eingabe-Pfad (Label → URI) — bewusst konservativ
resolveVocab (vocabs.ts) deckt nur die Schulfächer-Liste ab. Begründung:
- Das Hochschul-Vocab und das Schul-Vocab teilen sich Labels:
"Mathematik"existiert in beiden, mit unterschiedlicher Semantik (Schul-Mathematik =discipline/380; Hochschul-n4= "Mathematik, Naturwissenschaften" — viel breiter). - Wer User-Input automatisch auch ins Hochschul-Vocab mappt, riskiert ungewollte Filter (z.B. wird auch Physik/Chemie/Bio mit-gefiltert).
- Mehrheit der WLO-Inhalte ist schulisch.
- Wer gezielt Hochschul-Inhalte will, kann orthogonal über
educationalContext: "Hochschule"filtern.
Resolver-Schritte für Label-Eingabe:
- URI? → durchreichen
- Direkt-Match auf Label / Alias (case-insensitive)
- Substring-Match (Label im Input ODER Input im Label) — fängt Tippfehler/Paraphrasen wie
"Naturwiss"(matcht"naturwissenschaften") - Kein Match → null (Caller kann den Wert trotzdem an die WLO-API durchgeben)
Empfehlung für Konsumenten (LLM-gestützte Apps):
Bei freier Nutzereingabe ("sciences", "Mathe Klasse 11 Gym") zuerst per LLM auf einen lookup_wlo_vocabulary-Eintrag mappen, dann den canonical Label / URI an die Such-Tools übergeben. So fängst du Paraphrasen ab, die Substring-Matching nicht mehr trifft.
Alle Werte mit URIs: lookup_wlo_vocabulary({ vocabulary: "discipline" }).
Offizielle Vocab-Quellen
Wer das lokale Vocab in src/vocabs.ts aktualisieren oder Aliases ergänzen will:
| Vocab | URL |
|---|---|
Schulfächer (discipline) |
https://vocabs.openeduhub.de/w3id.org/openeduhub/vocabs/discipline/index.json |
Bildungsstufe (educationalContext) |
https://vocabs.openeduhub.de/w3id.org/openeduhub/vocabs/educationalContext/index.json |
Zielgruppe (userRole / intendedEndUserRole) |
https://vocabs.openeduhub.de/w3id.org/openeduhub/vocabs/intendedEndUserRole/index.json |
Lernressourcentypen (aggregiert) (lrt) |
https://vocabs.openeduhub.de/w3id.org/openeduhub/vocabs/new_lrt_aggregated/index.json |
| Hochschulfächersystematik (NICHT lokal gepflegt) | https://vocabs.openeduhub.de/w3id.org/openeduhub/vocabs/hochschulfaechersystematik/index.json |
Anzeige-Resolution für Hochschul-URIs erfolgt automatisch über _DISPLAYNAME aus dem edu-sharing-Index — ein lokales Hochschul-Vocab ist daher nicht nötig.
Output-Formate (markdown vs. json)
Alle Tools mit Suchergebnissen unterstützen outputFormat:
outputFormat: "markdown" (Default)
Menschenlesbare Karten, gut für Chat-Bots, die das LLM die Cards selbst rendern lassen. Beispiel:
## Bruchrechnung Einführung
nodeId: dc6b3f33-…
Beschreibung: …
Fach: Mathematik
Bildungsstufe: Sekundarstufe i
Lizenz: CC BY-SA 4.0
URL: https://…
Vorschaubild: https://…
Themenseite: https://…
Typ: Inhalt
outputFormat: "json"
Strukturierter JSON-String, optimal für Konsumenten, die Daten programmatisch weiterverarbeiten:
{
"total": 287,
"count": 5,
"results": [
{
"nodeId": "dc6b3f33-…",
"title": "Bruchrechnung Einführung",
"description": "…",
"keywords": ["Bruch", "Mathematik"],
"disciplines": ["Mathematik"],
"educationalContexts": ["Sekundarstufe i"],
"userRoles": [],
"learningResourceTypes": ["Video"],
"license": "CC BY-SA 4.0",
"publisher": "Mathe by Daniel Jung",
"url": "https://…",
"previewUrl": "https://…",
"topicPageUrl": "",
"nodeType": "content"
}
]
}
Wichtig:
disciplines,educationalContexts,userRoles,learningResourceTypes,licensesind immer Labels, nie URIs. Konsumenten könnencard.disciplines[0]direkt anzeigen.
Determinismus & Stabilität
Was du erwarten kannst:
- Bei identischer Input + identischem WLO-Server-Pool liefern alle Tools deterministische Reihenfolge:
- Tie-Breaker
nodeId.localeCompare()in allen Rankings sortByTitle()mit Tie-Breaker für alphabetische Sortierungenget_subject_portals,browse_collection_tree,search_wlo_topic_pages(sort=alpha) sind vollständig deterministisch.
- Tie-Breaker
- Mode C von
search_wlo_topic_pagesist deterministisch (alphabetisch über Sammlungsnamen).
Was außerhalb unserer Kontrolle liegt:
- Die WLO-API kann bei sehr ähnlichen Score-Treffern in seltenen Fällen einen anderen Pool liefern (Solr-Cache-Reshuffles serverseitig). Das beeinflusst
search_wlo_content/ Mode B vonsearch_wlo_topic_pages. Mitsort: "alpha"umgehst du das.
Deployment
Option A: Vercel (empfohlen)
Da die Web-Crawler-Tools entfernt sind, läuft v2 ohne Einschränkung auf Vercel Hobby (
maxDuration: 10sreicht). Dievercel.jsonsetzt trotzdemmaxDuration: 30als Sicherheitspolster fürbrowse_collection_tree(depth=2, includeContentCounts=true)bei breiten Bäumen.
Production-Deploy
- Repo auf GitHub pushen
- Vercel → New Project → Repo importieren
- Environment Variable (optional, Default greift):
WLO_REPOSITORY_URL=https://redaktion.openeduhub.net/edu-sharing - Deploy → MCP-Endpoint:
https://dein-projekt.vercel.app/mcp
Staging-Deploy (zweite Instanz)
Um parallel zur Production-Instanz eine Staging-Instanz zu betreiben, ein zweites Vercel-Projekt aus demselben Repo anlegen:
- Vercel → New Project → gleiches Repo, anderer Project Name (z.B.
wlo-mcp-server-staging) - Settings → Environment Variables →
WLO_REPOSITORY_URL=https://repository.staging.openeduhub.net/edu-sharingsetzen (überschreibt den Default ausvercel.json) - Deploy → MCP-Endpoint:
https://wlo-mcp-server-staging.vercel.app/mcp
Konsumenten zeigen dann ihren MCP-Client je nach Welt auf die passende URL — kein Code-Switch im Konsumenten nötig.
Option B: Docker
docker build -t wlomcp .
# Production
docker run -p 3000:3000 wlomcp # nutzt Default
docker run -p 3000:3000 -e WLO_REPOSITORY_URL=https://redaktion.openeduhub.net/edu-sharing wlomcp
# Staging
docker run -p 3000:3000 -e WLO_REPOSITORY_URL=https://repository.staging.openeduhub.net/edu-sharing wlomcp
# → http://localhost:3000/mcp
Option C: Lokal
npm install
npm run build
# Production (Default)
node dist/http.js
# Staging
WLO_REPOSITORY_URL=https://repository.staging.openeduhub.net/edu-sharing node dist/http.js
# → http://localhost:3000/mcp
Konfiguration in AI-Clients
OpenAI (Responses API)
response = client.responses.create(
model="gpt-5",
tools=[{
"type": "mcp",
"server_label": "wlo",
"server_url": "https://dein-projekt.vercel.app/mcp",
"require_approval": "never",
}],
input="Finde Unterrichtsmaterialien zu Bruchrechnung für die Grundschule",
)
Anthropic (Claude)
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
mcp_servers=[{"type": "url", "url": "https://dein-projekt.vercel.app/mcp", "name": "wlo"}],
messages=[{"role": "user", "content": "Suche WLO-Sammlungen zum Thema Klimawandel"}],
betas=["mcp-client-2025-04-04"],
)
Claude Desktop
{
"mcpServers": {
"wlo": {
"command": "node",
"args": ["/pfad/zu/wlomcp/dist/stdio.js"],
"env": { "WLO_REPOSITORY_URL": "https://redaktion.openeduhub.net/edu-sharing" }
}
}
}
Für eine Staging-Verbindung den Wert auf
https://repository.staging.openeduhub.net/edu-sharingändern (oder ein zweites Server-Profil hinzufügen).
Architektur
wlomcp/
├── src/
│ ├── server.ts # 11 Tool-Definitionen (transport-agnostisch)
│ ├── vocabs.ts # Label ↔ URI Mappings (educationalContext, discipline,
│ │ # userRole, lrt, license, targetGroup)
│ ├── wlo-api.ts # WLO/EduSharing-API-Client + resolveVariantCollection
│ ├── reranker.ts # Multi-Query-Expansion + RRF + Quality-Score (pure JS)
│ ├── formatter.ts # WloNode → FormattedNode → Markdown / JSON
│ ├── stdio.ts # Entry: stdio-Transport
│ └── http.ts # Entry: Streamable HTTP
├── api/
│ └── mcp.ts # Vercel-Serverless-Function-Wrapper
├── vercel.json # Vercel-Config
├── Dockerfile
└── .env.example
FormattedNode-Schema (das gemeinsame Output-Format aller Tools):
{
nodeId: string;
title: string;
description: string;
keywords: string[];
disciplines: string[]; // Labels: ["Mathematik"]
educationalContexts: string[]; // Labels: ["Sekundarstufe i"]
userRoles: string[]; // Labels: ["Lehrer/in"]
learningResourceTypes: string[]; // Labels: ["Arbeitsblatt"]
url: string;
previewUrl: string;
license: string; // Label: "CC BY-SA 4.0"
publisher: string;
nodeType: 'collection' | 'content';
topicPageUrl: string; // wenn ccm:page_config_ref vorhanden
}
API-Basis-URLs:
Die REST-API liegt unter <WLO_REPOSITORY_URL>/rest/..., das Frontend (Render- und Themenseiten-Links) unter <WLO_REPOSITORY_URL>/components/.... Die Pfade sind in allen Edu-Sharing-Instanzen identisch — der einzige Unterschied zwischen Welten ist die konfigurierte Repository-URL.
| Welt | WLO_REPOSITORY_URL |
Beispiel-Endpunkt (REST) |
|---|---|---|
| Production | https://redaktion.openeduhub.net/edu-sharing |
…/edu-sharing/rest/search/v1/… |
| Staging | https://repository.staging.openeduhub.net/edu-sharing |
…/edu-sharing/rest/search/v1/… |
| Custom | beliebige Edu-Sharing-Instanz | <base>/rest/search/v1/… |
Migration v1 → v2
Wenn du heute v1 nutzt
| v1-Verhalten | v2-Verhalten |
|---|---|
get_node_details liefert Fach-URI: http://w3id.org/.../380 |
get_node_details liefert Fach: Mathematik (Label). Mit includeRaw: true zusätzlich URI |
Lizenz: CC_BY_SA (Roh-Key) |
Lizenz: CC BY-SA 4.0 (Display-Form) |
## PAGE_VARIANT_TEMPLATE_xxx als Themenseiten-Titel im Mode C |
## Mathematik (Sammlungsname) — Owner wird via API resolvt |
Zielgruppe: teacher (Slug) |
Zielgruppe: Lehrkräfte (Label) |
| Mehrere Varianten = N separate Karten | 1 Karte mit variants[]-Array (oder mergeVariants: false für altes Verhalten) |
| Gleicher Score → unspezifizierte Reihenfolge | Tie-Breaker nodeId.localeCompare() → deterministisch |
get_*_info-Tools (4 Stück) |
Entfernt. Konsument soll RAG/eigene Page-Extraction nutzen |
Empfohlene Migration
- Sofort ausführen:
wlo_health_checkeinmalig nach Deploy, Latenz prüfen, Server-Status verifizieren - JSON-Output progressiv aktivieren: Beginne mit
get_node_detailsundsearch_wlo_topic_pages— die hatten in v1 die meisten Inkonsistenzen. Vorher: Markdown-Parsing → Nachher:JSON.parse() - Webseiten-Tool-Aufrufe entfernen: Wenn dein Client
get_wirlernenonline_infoetc. nutzt → entweder ein eigenes RAG aufsetzen oder das Tool aus dem Allowlist deines AI-Clients nehmen - Variant-Merge-Code entfernen: Wenn dein Client mehrere Varianten desselben Themas selbst gemerged hat → das macht der Server jetzt
- Discipline-URI-Resolver in Konsumenten: Wer früher Karten-Disciplines manuell auf Labels resolvte, kann diese Logik abklemmen — Server liefert immer Labels
Kompatibilität
- Tool-Namen sind ausschließlich Kleinbuchstaben + Unterstriche → kompatibel mit OpenAI / Anthropic
- Input-Schemas sind JSON Schema (via Zod) → Standard-konform
- Transport: Streamable HTTP (MCP spec 2025-03-26) für Vercel/Docker; stdio für lokale Clients
- Stateless: Kein Session-State → skaliert auf Vercel Serverless
- Vercel-tauglich: Reranker ist pure JS, kein Transformer-Modell, kein Speicherbedarf > Function-Limit
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.