Wlo Mcp Server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Wlo Mcp Server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
content / collections / topicPages). Spart dem Client mehrere separate Such-Aufrufe (= weniger Round-Trips / Cold-Starts).-all- (~59) → deutlich kleinere Payloads. _DISPLAYNAME-Labels bleiben erhalten; get_node_details bleibt bewusst auf -all-.rerankNodes greift 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.ngsearch-Calls (Einzelterm-Varianten entfernt).nodeIds parallel.lookup_wlo_vocabulary (vocabulary: "license" / "targetGroup").PAGE_VARIANT_TEMPLATE_xxx-Slugs (Auto-Resolve via getNodeParents).mergeVariants=true (Default) zu einer Karte zusammengefasst."Lehrkräfte" / "Lernende" / "Allgemein") statt Slugs / ccrep://-URIs.nodeId.disciplines, educationalContexts, userRoles, learningResourceTypes, license als menschenlesbare Labels ("Mathematik", "CC BY-SA 4.0") statt URIs.includeRaw: true zeigt zusätzlich die unaufgelösten URIs für Debugging / Spezialfälle.CC_BY_SA werden überall zu "CC BY-SA 4.0" aufgelöst.reranker.ts): deterministischer Tie-Breaker auf nodeId bei gleichem Score; neue sortByTitle() für stabile alphabetische Sortierung.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).WEB_CONTENT_WHITELIST und fetchWebContent() aus wlo-api.ts entfernt.WLO_REPOSITORY_URL. WLO_ROOT_COLLECTION_IDS (Record) wurde zu WLO_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).
git clone https://github.com/yourorg/wlomcp.git
cd wlomcp
npm install
npm run buildcp .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).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| # | 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_ref haben 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) → erstget_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)
search_wlo_collectionsSucht 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)search_wlo_contentGlobale 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:
full: immer dabei)WLO_POOL_SIZE Treffer / Variante, Default 25)0.8 × quality + 0.1 × rrf + 0.1 × multi_appearance_bonusnodeId.localeCompare() für deterministische Reihenfolge bei gleichen Scoresget_collection_contentsInhalte 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: StattcontentFilter="folders"istbrowse_collection_tree(Tool 8) klarer und liefert direkt File-Counts.
get_node_detailsDetail-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
}lookup_wlo_vocabularyListet Vokabular-Werte. Quelle: lokale src/vocabs.ts (keine API-Calls).
| Parameter | Werte |
|---|---|
vocabulary | educationalContext, discipline, userRole, lrt, license, targetGroup |
search_wlo_topic_pagesThemenseiten 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:
collectionId gesetzt): prüft ccm:page_config_ref der Sammlung, traversiert Config-Kinder → Variantenquery gesetzt): Sucht Sammlungen per Keyword, filtert auf page_config_refquery, kein collectionId): Nutzt die page_variant-API. Variant-Owner-Sammlung wird über getNodeParents-Walk aufgelöst → echter Sammlungsname statt PAGE_VARIANT_TEMPLATE_xxxJSON-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": "…"}
]
}]
}get_subject_portalsLiefert 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=foldersbrowse_collection_treeDrilldown 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.
wlo_health_checkProbe 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.
get_nodes_detailsBulk-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.
search_wlo_allKombinierte 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.
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 |
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:
ccm:taxonid, ccm:educationalcontext, ccm:oeh_lrt_aggregated, ccm:oeh_intended_end_user_role.taxonid=[".../hochschulfaechersystematik/n71", ".../n8"] zeigt disciplines: ["Studienbereich Informatik", "Ingenieurwissenschaften"].vocabs.ts)._DISPLAYNAME leer ist (z.B. ccm:commonlicense_key hat nie ein DISPLAYNAME — daher die lokale License-Map).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:
"Mathematik" existiert in beiden, mit unterschiedlicher Semantik (Schul-Mathematik = discipline/380; Hochschul-n4 = "Mathematik, Naturwissenschaften" — viel breiter).educationalContext: "Hochschule" filtern.Resolver-Schritte für Label-Eingabe:
"Naturwiss" (matcht "naturwissenschaften")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" }).
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.
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: InhaltoutputFormat: "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.
Was du erwarten kannst:
nodeId.localeCompare() in allen RankingssortByTitle() mit Tie-Breaker für alphabetische Sortierungenget_subject_portals, browse_collection_tree, search_wlo_topic_pages (sort=alpha) sind vollständig deterministisch.search_wlo_topic_pages ist deterministisch (alphabetisch über Sammlungsnamen).Was außerhalb unserer Kontrolle liegt:
search_wlo_content / Mode B von search_wlo_topic_pages. Mit sort: "alpha" umgehst du das.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
WLO_REPOSITORY_URL=https://redaktion.openeduhub.net/edu-sharinghttps://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:
wlo-mcp-server-staging)WLO_REPOSITORY_URL=https://repository.staging.openeduhub.net/edu-sharing setzen (überschreibt den Default aus vercel.json)https://wlo-mcp-server-staging.vercel.app/mcpKonsumenten zeigen dann ihren MCP-Client je nach Welt auf die passende URL — kein Code-Switch im Konsumenten nötig.
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/mcpnpm 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/mcpresponse = 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",
)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"],
){
"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).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/… |
| 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 |
wlo_health_check einmalig nach Deploy, Latenz prüfen, Server-Status verifizierenget_node_details und search_wlo_topic_pages — die hatten in v1 die meisten Inkonsistenzen. Vorher: Markdown-Parsing → Nachher: JSON.parse()get_wirlernenonline_info etc. nutzt → entweder ein eigenes RAG aufsetzen oder das Tool aus dem Allowlist deines AI-Clients nehmen~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.