MCP-Schnittstelle (Model Context Protocol)¶
Ab Version 1.12.0 stellt der DKS einen MCP-Server bereit. Damit kann die Korrektur ohne zusätzliche Adapter direkt aus MCP-fähigen KI-Clients und Automatisierungsplattformen verwendet werden – etwa Cursor, Claude Desktop, ChatGPT, n8n, Dify oder Langdock.
Überblick¶
Protokoll: Model Context Protocol, Revision
2025-06-18(die ältere Revision2025-03-26wird beim Handshake weiterhin akzeptiert)Transport: Streamable HTTP, stateless JSON-RPC 2.0
Endpunkt:
POST /mcpAuthentifizierung: API-Token (Bearer), Rolle
API– identisch zur REST-APIAktivierung: standardmäßig aktiviert; zweistufig über die Umgebungsvariable
MCP_DISABLED(harter Kill-Schalter) plus einen Schalter in der Admin-Oberfläche
Es werden keine SSE-Streams erzeugt und keine Sitzungen geführt – jeder Aufruf ist atomar und kommt als einzelne JSON-RPC-Antwort zurück.
In der Admin-Oberfläche unter Text-Assistent -> MCP-Server werden Status,
Endpunkt, Authentifizierung sowie die verfügbaren Werkzeuge (live aus
tools/list) und Einbindungsbeispiele für die gängigen Clients angezeigt.
Aktivierung¶
Der Endpunkt ist standardmäßig aktiviert. Um ihn hart zu deaktivieren,
wird die Umgebungsvariable MCP_DISABLED gesetzt:
MCP_DISABLED=true
Solange die Variable nicht gesetzt oder false ist, ist MCP verfügbar. Ist
MCP_DISABLED=true gesetzt, beantwortet der Server jeden Request an /mcp
mit einem leeren HTTP 404; Clients können nicht erkennen, dass die
Schnittstelle existiert. Eine Änderung dieser Variable wird erst nach einem
Neustart des Servers wirksam.
Laufzeit-Schalter (Admin-Oberfläche)¶
Solange MCP nicht per MCP_DISABLED=true hart abgeschaltet ist, lässt sich
die Schnittstelle zusätzlich zur Laufzeit über die Admin-Oberfläche unter
Text-Assistent -> MCP-Server an- und ausschalten – ohne Neustart. Die
Umgebungsvariable bleibt dabei der harte Master-Schalter (z. B. um MCP in
bestimmten Deployments vollständig zu sperren).
Wirksame Logik:
MCP_DISABLED=true-> MCP immer aus (Admin-Schalter ohne Wirkung).MCP_DISABLEDnicht gesetzt/falseund Admin-Schalter an (Default) -> MCP an.MCP_DISABLEDnicht gesetzt/falseund Admin-Schalter aus -> MCP aus.
Authentifizierung¶
Jede Anfrage an /mcp muss authentifiziert sein – identisch zur REST-API.
Bevorzugt wird ein API-Token im Authorization-Header:
Authorization: Bearer <API-Token>
Alternativ wird HTTP-Basic-Authentifizierung (Benutzername/Passwort)
akzeptiert. In jedem Fall ist die Rolle API erforderlich.
API-Tokens werden in der Admin-Oberfläche unter API-Tokens erstellt und
verwaltet (siehe Benutzerkonten). Empfohlen wird ein eigener Token pro
Client (z. B. Langdock Workspace RP), damit Aktivität nachvollziehbar
bleibt und ein einzelner Client jederzeit revoziert werden kann.
Verfügbare Tools¶
Der Server meldet die Werkzeuge über tools/list. Jedes Tool liefert dort
neben name, description und inputSchema zusätzlich:
title– menschenlesbarer AnzeigenameoutputSchema– JSON-Schema des strukturierten Ergebnisses; die lesenden Tools sind damit voll selbstbeschreibendannotations– Verhaltenshinweise (readOnlyHint,destructiveHint,idempotentHint,openWorldHint), an denen Clients ihre Sicherheits-UI und Auto-Freigabe ausrichten
Alle lesenden Tools sind readOnlyHint=true; propose_word schreibt
(readOnlyHint=false), ist aber idempotent.
Jeder tools/call liefert ein content-Array (Text mit dem JSON als
Zeichenkette) und – bei strukturierten Werkzeugen – dasselbe Ergebnis als
strukturiertes JSON im Feld structuredContent. Bei Fehlern ist
isError=true gesetzt.
check_text – Text prüfen¶
Prüft einen Text auf Rechtschreibung, Grammatik und Stil mit dem Duden-Korrektor. Liefert pro Fundstelle Position, Snippet, Typ, Fehlertext und Korrekturvorschläge.
Parameter:
text(string, Pflicht) – Der zu prüfende Text. Plain Text per Default; mitmarkupMode=xmlfür ausgezeichneten Text.language(string, Defaultde-DE) – Sprache des Textes (BCP-47). Mögliche Werte liefertlist_languages.level(integer, Default3) – Prüfstufe: 0 = keine, 1 = schnelle Rechtschreibung, 2 = Rechtschreibung+Grammatik, 3 = zusätzlich Stil (nur DE).orthographyStandard(string) – überschreibt den im Profil hinterlegten Rechtschreibstandard. Werte:duden,conservative,progressive,extended,press.propertySets(string[]) – Namen der anzuwendenden Korrekturprofile (siehelist_property_sets).dictionaries(string[]) – Namen zusätzlicher Custom-Wörterbücher (siehelist_dictionaries).markupMode(string, Defaulttext) –textoderxml.correctionProposals(boolean, Defaulttrue) – Korrekturvorschläge liefern.hyphenation(boolean, Defaultfalse) – zusätzlich Silbentrennpositionen liefern.glossary(boolean, Defaultfalse) – Glossartreffer mitliefern (nur wenn Glossarwörterbücher angegeben sind).singleWordMode(boolean, Defaultfalse) – Eingabe als einzelnes Wort behandeln (z. B. für Wortlisten).
Rückgabe (structuredContent):
errorCount(integer) – Anzahl der Fundstellen.errors(object[]) – eine Fundstelle je Problem, mitoffset,length,snippet,type,errorCode,message,longMessage,examples(jefalse/correct),group,categoryundsuggestions(string[]).hyphenationPositions(object[]) – nur wennhyphenation=true, sonst leer.glossaryMessages(object) – nur wennglossary=true, sonst ein leeres Objekt.
{
"errorCount": 1,
"errors": [
{
"offset": 12,
"length": 8,
"snippet": "Endwurff",
"type": "orth",
"errorCode": 4711,
"message": "Mögliche Falschschreibung",
"longMessage": "Das Wort wurde nicht im Wörterbuch gefunden ...",
"examples": [
{ "false": "Endwurff", "correct": "Entwurf" }
],
"group": "Rechtschreibung",
"category": "Schreibung",
"suggestions": ["Entwurf", "Entwürfe"]
}
],
"hyphenationPositions": [],
"glossaryMessages": {}
}
list_dictionaries – Wörterbücher auflisten¶
Listet die im DKS verfügbaren Wörterbücher. Hilfreich, bevor ein Agent
check_text oder propose_word mit einem bestimmten Wörterbuch aufruft.
Keine Parameter.
Rückgabe (structuredContent):
count(integer)dictionaries(object[]) – jename,description,language,isGlossary,isSystem,isReadOnly.
list_property_sets – Korrekturprofile auflisten¶
Listet alle Korrekturprofile (Property Sets) inklusive Sprache und Prüfstufe,
damit ein Agent ein gültiges Profil für check_text.propertySets wählen
kann. Keine Parameter.
Rückgabe (structuredContent):
count(integer)propertySets(object[]) – jeid,name,description,language,checklevel,orthstd,enforceunddictionaries(string[]).
list_languages – Sprachen auflisten¶
Listet die im DKS verfügbaren Sprachen, damit ein Agent einen gültigen Wert
für check_text.language wählen kann. Keine Parameter.
Rückgabe (structuredContent):
count(integer)languages(object[]) – jecode(BCP-47) undsource(dpf= eingebaut oderhunspell).
propose_word – Wort vorschlagen¶
Trägt ein Wort in ein beschreibbares Wörterbuch ein. Per Default wird das Wort
als korrekt akzeptiert (accept); mit correction wird es stattdessen als
Falschschreibung markiert und ein Korrekturvorschlag hinterlegt (reject).
Idempotent: ein bereits existierender Eintrag für dasselbe Wort und Wörterbuch
wird in-place aktualisiert statt dupliziert.
Parameter:
word(string, Pflicht, max. 76 Zeichen) – das vorzuschlagende Wort.dictionary(string, DefaultProposals) – Zielwörterbuch.Proposalsist das System-Wörterbuch für Nutzervorschläge; weitere Optionen siehelist_dictionaries.comment(string, optional, max. 256 Zeichen) – Begründung / Kontext, der mit dem Eintrag gespeichert wird.correction(string, optional, max. 256 Zeichen) – wird dieses Feld gesetzt, wird das Wort als Falschschreibung markiert (reject=true) undcorrectionals Vorschlagswort gespeichert. Mehrere Vorschläge per Semikolon trennen. Ohnecorrectionwird das Wort akzeptiert (accept=true).
Rückgabe (structuredContent):
status(string) –createdoderupdated.mode(string) –acceptoderreject.word(string)dictionary(string)entryId(integer)accept(boolean)reject(boolean)proposal(string) – hinterlegter Korrekturvorschlag (nur beireject).comment(string) – gespeicherte Begründung, falls vorhanden.
{
"status": "created",
"mode": "reject",
"word": "Endwurff",
"dictionary": "Proposals",
"entryId": 1,
"accept": false,
"reject": true,
"proposal": "Entwurf",
"comment": "Tippfehler aus dem MCP-Smoketest"
}
Beispiel-Aufrufe (curl)¶
Typischer Ablauf: initialize, dann tools/list, dann tools/call.
<API-Token> durch ein gültiges API-Token und <dks-host> durch den
Hostnamen ersetzen.
Initialisierung:
curl -s -X POST https://<dks-host>/mcp \
-H "Authorization: Bearer <API-Token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"demo","version":"1"}}}'
Tool-Liste abrufen:
curl -s -X POST https://<dks-host>/mcp \
-H "Authorization: Bearer <API-Token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
Text prüfen:
curl -s -X POST https://<dks-host>/mcp \
-H "Authorization: Bearer <API-Token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"check_text",
"arguments":{"text":"Das ist ein Testt.",
"language":"de-DE","level":3}}}'
Einbindung in KI-Clients¶
Der DKS-MCP-Server lässt sich in gängige KI-Clients und
Automatisierungsplattformen einbinden. Ersetzen Sie überall <API-Token>
durch ein gültiges API-Token (Rolle API) und <dks-host> durch den
Hostnamen Ihrer Instanz.
Bemerkung
Der Endpunkt muss vom Client erreichbar sein. Lokale Desktop-Clients (Cursor, Claude Desktop) genügen im selben Netz. Cloud-Dienste (ChatGPT, Langdock, Dify-Cloud) benötigen einen öffentlich über HTTPS erreichbaren Endpunkt.
Cursor¶
Cursor unterstützt Remote-MCP über Streamable HTTP direkt. Tragen Sie den
Server in ~/.cursor/mcp.json (global) oder .cursor/mcp.json
(projektbezogen) ein:
{
"mcpServers": {
"dks": {
"url": "https://<dks-host>/mcp",
"headers": {
"Authorization": "Bearer <API-Token>"
}
}
}
}
Danach Cursor neu laden („Reload Window“). Tipp: Statt Klartext kann das Token
als Umgebungsvariable referenziert werden, z. B.
"Bearer ${env:DKS_TOKEN}".
Claude Desktop¶
Claude Desktop spricht in seiner Konfiguration nur stdio; für Remote-HTTP wird
die Bridge mcp-remote genutzt. Konfigurationsdatei: macOS
~/Library/Application Support/Claude/claude_desktop_config.json, Windows
%APPDATA%\Claude\claude_desktop_config.json.
{
"mcpServers": {
"dks": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://<dks-host>/mcp",
"--transport", "http-only",
"--allow-http",
"--header", "Authorization:${DKS_AUTH}"
],
"env": { "DKS_AUTH": "Bearer <API-Token>" }
}
}
}
--allow-http ist nur bei unverschlüsseltem http:// nötig (bei HTTPS
weglassen). Das Bearer-Token wird wegen eines Whitespace-Bugs über die
env-Variable gesetzt. Anschließend Claude Desktop neu starten.
ChatGPT¶
ChatGPT bindet eigene MCP-Server über den „Developer Mode“ (Entwicklermodus) als Connector/App ein – nur im Web (Desktop-Browser), nicht in der Mobile-App. Voraussetzung ist ein bezahlter Plan; Plus/Pro können nur lesende Connectors nutzen, schreibende Tools brauchen Business/Enterprise/Education (dort muss ein Admin den Developer Mode erst freigeben).
Einstellungen -> „Apps & Connectors“ -> „Erweiterte Einstellungen“ -> „Developer Mode“ einschalten.
Danach erscheint die Schaltfläche „Erstellen“ (Create): Connector anlegen mit Name, Beschreibung und der Endpunkt-URL
https://<dks-host>/mcp.Im Chat über das „+“-/Tools-Menü „Developer Mode“ wählen und den Connector aktivieren.
Warnung
ChatGPT erreicht nur öffentlich über HTTPS erreichbare Endpunkte und unterstützt im Connector-Formular nur „OAuth“, „Keine Authentifizierung“ oder „Mixed“ – kein statisches Bearer-Token. Für den DKS-Server (Bearer-Auth) ist daher ein vorgelagerter HTTPS-Reverse-Proxy nötig, der OAuth bereitstellt oder das Token serverseitig injiziert.
n8n¶
In n8n verbindet der Node „MCP Client Tool“ externe MCP-Server (aktuelle
n8n-Version mit HTTP-Streamable-Unterstützung vorausgesetzt; alternativ der
Community-Node n8n-nodes-mcp).
Node „MCP Client Tool“ zum Workflow hinzufügen.
Connection Type auf „HTTP Streamable“ setzen und als URL
https://<dks-host>/mcpeintragen.Als Authentication „Bearer Auth“ wählen und ein Bearer-Token-Credential mit dem API-Token anlegen.
Operation wählen (z. B. „List Tools“ oder „Call Tool“) und den Node mit dem Agenten verbinden.
Dify¶
In Dify werden MCP-Server unter Tools eingebunden (nur HTTP-Transport).
Tools -> MCP -> „Add MCP Server (HTTP)“ öffnen.
Als Server-URL
https://<dks-host>/mcpeintragen sowie Name und eine feste Server-ID vergeben.Im Headers-Feld den Authorization-Header hinterlegen:
Authorization: Bearer <API-Token>
Das Headers-Feld gibt es erst in neueren Dify-Versionen; ältere unterstützen nur OAuth. Dify muss den Endpunkt erreichen können (bei Self-Hosting ggf. den SSRF-Proxy anpassen).
Langdock¶
In Langdock wird der Server als Integration hinzugefügt.
Workspace-Einstellungen -> Integrations -> „Add Integration“ -> Typ „MCP“ wählen.
Als URL
https://<dks-host>/mcpeintragen und als Authentifizierung „API Key“ wählen.Header-Typ „Authorization: Bearer“ setzen, das API-Token eintragen und mit „Test connection“ prüfen.
Langdock läuft in der Cloud – der DKS-Endpunkt muss daher öffentlich über HTTPS erreichbar sein. Liegt der DKS hinter einer Firewall mit IP-Whitelist, sollte die statische IP der Langdock-Plattform freigeschaltet werden (siehe Langdock Docs: Static IP Configuration).
Korrekturen automatisch anwenden (Prompt-Vorlage)¶
Der häufigste Anwendungsfall ist: einen Text von check_text prüfen lassen
und die gefundenen Korrekturen anschließend automatisch in den Text
übernehmen. check_text liefert dazu die Befunde (Fundstellen samt
Vorschlägen, Regeltext und Beispielen), aber es ändert den Text nicht –
das Anwenden übernimmt ein Sprachmodell (LLM).
Bewährtes Muster (ein Durchlauf pro Text):
check_textaufrufen und aus der Antwort daserrors-Array ausstructuredContententnehmen.Die Befunde zusammen mit dem Originaltext an ein LLM geben – mit der Prompt-Vorlage unten und
temperature = 0für deterministische Ausgabe.Die LLM-Antwort ist der korrigierte Text.
Die Vorlage nutzt bewusst alle Felder eines Befunds: liegen suggestions
vor, wählt das Modell den passendsten Vorschlag; ist die Liste leer, leitet es
die Korrektur aus longMessage und den examples (Paare aus false und
correct) ab. Dadurch werden auch Fehler behoben, für die DKS keinen
fertigen Vorschlag hat.
Variante A – Plain Text¶
Für einfache Texte ohne Auszeichnung (z. B. Titel, Überschriften, Fließtext). System-Prompt:
Du erhältst einen Text und eine Liste von DKS-Korrekturbefunden.
Jeder Befund enthält -- soweit verfügbar -- die Felder:
- snippet : die markierte Textstelle
- type : "orth" | "gram" | "style" | ...
- errorCode : numerischer DKS-Regel-Code
- message : Kurztext der Regel
- longMessage : ausführliche Beschreibung der Regel
- examples : Liste von { "false": ..., "correct": ... } Beispielen
derselben Regel -- die wichtigste Hilfe, wenn
suggestions leer ist.
- suggestions : ggf. von DKS vorgeschlagene Korrekturen
Wende AUSSCHLIESSLICH die Befunde an. Pro Befund gilt:
a) suggestions gefüllt -> wähle den grammatikalisch und semantisch
passendsten Vorschlag (Numerus, Genus, Kasus, Artikel, Verbkongruenz).
b) suggestions leer -> orientiere dich an longMessage und examples.
Übertrage das Muster aus den Beispielen ANALOG auf den vorliegenden
Text (gleiche Regel, anderer konkreter Wortlaut).
Bist du dir trotz Regelinfo und Beispielen nicht sicher, lass den
Befund unverändert.
Antworte ausschließlich mit dem korrigierten Text, ohne Anführungszeichen,
ohne Markdown, ohne Kommentar.
Als Benutzernachricht die Befunde und den Originaltext übergeben, z. B.:
=== DKS-Befunde (JSON) ===
<errors-Array aus structuredContent>
=== Originaltext ===
<der geprüfte Text>
Variante B – HTML/Markup erhalten¶
Wenn der Text Auszeichnung enthält (HTML, z. B. aus einem CMS) und diese
zeichengenau erhalten bleiben muss. Wichtig: Vor der Prüfung den sichtbaren
Text aus dem Markup extrahieren und an check_text geben; die Korrekturen
werden anschließend auf das originale Markup angewandt. System-Prompt:
Du bist ein deterministischer HTML-Korrekturassistent.
Wende AUSSCHLIESSLICH die unten gelisteten Befunde des Duden-Korrektor-
Servers (DKS) auf den gegebenen HTML-Inhalt an. Jeder Befund enthält --
soweit verfügbar -- die Felder snippet, type, errorCode, message,
longMessage, examples ({ "false": ..., "correct": ... }), group/category
und suggestions.
Strikte Regeln:
1. Ändere KEINE HTML-Tags, -Attribute, -Klassennamen, -IDs, -URLs oder
HTML-Entities. Die Tag-Struktur des Outputs muss zeichengenau mit der
des Inputs übereinstimmen; nur reine Text-Knoten dürfen sich ändern.
2. Pro Befund -- abhängig von "suggestions":
a) GEFÜLLT: Wähle den Vorschlag, der grammatikalisch und semantisch am
besten in den umgebenden Satz passt (Numerus, Genus, Kasus, Artikel,
Verbkongruenz, Satzbedeutung). longMessage und examples liefern den
Regel-Hintergrund. Beispiel: bei "ein Haii" mit ["Haie","Hai"] wählst
du "Hai", weil "ein" Singular verlangt.
b) LEER: Bestimme die Korrektur SELBST anhand von longMessage und
examples. Übertrage das in den Beispielen gezeigte Muster ANALOG auf
den vorliegenden Text (gleiche Regel, anderer konkreter Wortlaut).
Beispiele:
- type "orth", examples ["Entgeld"->"Entgelt", "watren"->"warten"]:
"Resteraunt" wird zu "Restaurant".
- type "gram", example "Wir vertrauen Herr Schmidt"->"Wir vertrauen
Herrn Schmidt": "bestellten ... ein komplizierter Cocktail" wird zu
"... einen komplizierten Cocktail" (Akkusativ nach "bestellen").
- type "style" ohne Vorschlag: meist nicht zwingend ändern -- nur bei
einem trivialen, eindeutigen Fix aus den examples.
3. Ermöglichen weder suggestions noch examples eine eindeutige Korrektur,
lass den Befund unverändert und ignoriere ihn still.
4. Korrigiere im sichtbaren Text nur die "snippet"-Strings (bei type "gram"
ggf. die umrissene Wortgruppe) -- berücksichtige umliegende Satzzeichen.
Erscheint ein Snippet mehrfach und der Kontext erlaubt keine eindeutige
Unterscheidung, korrigiere alle Vorkommen.
5. Ist ein Snippet im sichtbaren Text nicht auffindbar, ignoriere den Befund
still.
6. Füge KEINEN neuen Text, KEINE Erklärungen, KEINE Kommentare und KEINE
Markdown-Codeblöcke hinzu.
7. Antworte ausschließlich mit dem korrigierten HTML, beginnend exakt mit dem
ersten Zeichen des Originals.
Als Benutzernachricht die Befunde und die HTML-Quelle übergeben, z. B.:
=== DKS-Befunde (JSON-Array) ===
<errors-Array aus structuredContent>
=== HTML-Quelle ===
<das originale HTML>
Referenz-Workflow für Dify¶
Ein vollständiger Beispiel-Workflow, der genau diese Prompts verwendet, steht
als Dify-DSL bereit: ausgelöst durch das Veröffentlichen eines
WordPress-Artikels prüft er Titel und Inhalt per MCP (check_text) und
schreibt die Korrekturen als Entwurf (Autosave) samt Editor-Kommentar zurück.
Der Workflow ist ein optionales Beispiel und nicht Teil der
DKS-Installation.
Zum Verwenden den folgenden Inhalt als .yml speichern und in Dify unter
Studio -> Import DSL importieren
(direkter Download). Über die
Schaltfläche oben rechts im Codeblock lässt sich der gesamte Inhalt in die
Zwischenablage kopieren.
## Dify Workflow DSL for "DKS Spellcheck"
## Tested against Dify 1.10.1 (DSL version 0.5.0).
##
## Placeholders that init_dify.sh substitutes before /apps/imports:
## __WP_BASIC_AUTH__ = admin:<WP_APP_PASSWORD>
## __DKS_API_TOKEN__ = <DKS_API_TOKEN> (from api_token table)
## __WP_INTERNAL_URL__ = http://wordpress-60
## __DKS_MCP_URL__ = http://dks:80/mcp
##
## Pipeline (zwei parallele Branches: Titel + Inhalt, am Ende zusammengefuehrt):
## start(post_id, post_type)
## -> derive_endpoints (post_type -> wp_collection 'posts'/'pages')
## -> fetch_post (GET WP REST raw post/page)
## -> strip_html (plain text for DKS, keeps raw_html + title)
## -> [Titel-Branch]
## -> dks_check_title (level=2: Rechtschr.+Gramm., kein Stil)
## -> extract_findings_title
## -> llm_apply_title (plain text, keine Markup-Sorgen)
## -> [Inhalt-Branch]
## -> dks_check_content (level=3: + Stil)
## -> extract_findings_content
## -> llm_apply_content (raw HTML, Markup wird erhalten)
## -> build_payloads (kombiniert beide LLM-Outputs)
## -> create_autosave (POST /wp/v2/{type}/{id}/autosaves)
## -> add_comment (Editor-Kommentar mit Befunden)
## -> end
##
## Das LLM-Modell ist auf beiden llm_apply_* Nodes gesetzt. Nach dem Import unter
## Dify -> Settings -> Model Provider das OpenAI-Plugin installieren und API-Key
## hinterlegen. Default-Provider "langgenius/openai/openai", Modell "gpt-4o" --
## anderen Provider/Modell pro Node im UI waehlbar.
app:
description: Triggered by WordPress publish, runs Duden Korrektor (DKS) via MCP and writes corrections back as a draft.
icon: "\U0001F4DD"
icon_background: "#FFEAD5"
icon_type: emoji
mode: workflow
name: DKS Spellcheck
kind: app
version: 0.5.0
workflow:
features: {}
environment_variables: []
conversation_variables: []
graph:
nodes:
- id: start
position: {x: 0, y: 0}
data:
type: start
title: Start
desc: "Receives post_id + post_type from the WordPress mu-plugin"
variables:
- variable: post_id
label: "Post ID"
type: number
required: true
- variable: post_type
label: "Post Type"
type: text-input
required: false
default: "post"
- variable: site_url
label: "Site URL (override)"
type: text-input
required: false
default: "http://wordpress-60"
- id: derive_endpoints
position: {x: 125, y: 0}
data:
type: code
title: Derive REST Collection
desc: "Mappt post_type (post|page) auf REST-Collection-Plural (posts|pages)"
code_language: python3
code: |
def main(post_type: str) -> dict:
## fallback auf 'posts' falls leer/unbekannt
pt = (post_type or "post").strip().lower()
mapping = {"post": "posts", "page": "pages"}
collection = mapping.get(pt, pt + "s")
return {"wp_collection": collection}
variables:
- variable: post_type
value_selector: [start, post_type]
outputs:
wp_collection:
type: string
- id: fetch_post
position: {x: 250, y: 0}
data:
type: http-request
title: Fetch Post/Page
desc: "GET WordPress post/page with raw content"
method: get
url: "__WP_INTERNAL_URL__/wp-json/wp/v2/{{#derive_endpoints.wp_collection#}}/{{#start.post_id#}}?context=edit"
authorization:
type: api-key
config:
type: basic
api_key: "__WP_BASIC_AUTH__"
header: ""
headers: ""
params: ""
body:
type: none
data: []
- id: strip_html
position: {x: 500, y: 0}
data:
type: code
title: Strip HTML
desc: "Parse fetch_post.body JSON and reduce post content.raw to plain text"
code_language: python3
code: |
import json, re, html
def main(post_body: str) -> dict:
try: post = json.loads(post_body or "{}")
except: post = {}
raw_html = (post.get("content") or {}).get("raw") or ""
title = (post.get("title") or {}).get("raw") or ""
txt = re.sub(r"<[^>]+>", " ", raw_html)
txt = html.unescape(re.sub(r"\s+", " ", txt)).strip()
return {"plain": txt, "raw_html": raw_html, "title": title}
variables:
- variable: post_body
value_selector: [fetch_post, body]
outputs:
plain:
type: string
raw_html:
type: string
title:
type: string
- id: dks_check_title
position: {x: 750, y: -180}
data:
type: http-request
title: DKS MCP check_text (Titel)
desc: "level=2 -- nur Rechtschreibung+Grammatik, kein Stil (kein 'fehlender Punkt am Ende' Geraffel)"
method: post
url: "__DKS_MCP_URL__"
authorization:
type: api-key
config:
type: bearer
api_key: "__DKS_API_TOKEN__"
header: ""
headers: "Content-Type:application/json"
params: ""
body:
type: json
data:
- key: ""
type: text
value: '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_text","arguments":{"text":"{{#strip_html.title#}}","language":"de-DE","level":2}}}'
- id: extract_findings_title
position: {x: 1000, y: -180}
data:
type: code
title: Extract Findings (Titel)
code_language: python3
code: |
import json
def main(mcp_body) -> dict:
if isinstance(mcp_body, str):
try: mcp_body = json.loads(mcp_body)
except: mcp_body = {}
if not isinstance(mcp_body, dict):
mcp_body = {}
struct = (mcp_body.get("result") or {}).get("structuredContent") or {}
errors = struct.get("errors", []) or []
findings, md_lines = [], []
for f in errors:
snippet = f.get("snippet", "")
if not snippet:
continue
findings.append({
"snippet": snippet,
"suggestions": (f.get("suggestions") or [])[:8],
"type": f.get("type", ""),
"errorCode": f.get("errorCode"),
"message": f.get("message", "") or "",
"longMessage": f.get("longMessage", "") or "",
"examples": (f.get("examples") or [])[:3],
"group": f.get("group", "") or "",
"category": f.get("category", "") or "",
})
sugs = findings[-1]["suggestions"]
rhs = ("[" + " | ".join(sugs) + "]") if sugs else "(kein Vorschlag)"
extra = " (" + findings[-1]["message"] + ")" if findings[-1]["message"] else ""
md_lines.append("- [Titel/" + (findings[-1]["type"] or "?") + "] '" + snippet + "' -> " + rhs + extra)
return {
"findings_json": json.dumps(findings, ensure_ascii=False),
"findings_md": "\n".join(md_lines),
"count": len(findings),
}
variables:
- variable: mcp_body
value_selector: [dks_check_title, body]
outputs:
findings_json: {type: string}
findings_md: {type: string}
count: {type: number}
- id: llm_apply_title
position: {x: 1250, y: -180}
data:
type: llm
title: LLM Apply Corrections (Titel)
desc: "Plain-Text Korrektur, kein HTML"
model:
provider: langgenius/openai/openai
name: gpt-4o
mode: chat
completion_params:
temperature: 0
max_tokens: 512
prompt_template:
- role: system
text: |
Du erhaeltst einen kurzen Artikel-Titel und eine Liste von DKS-Korrekturbefunden.
Jeder Befund enthaelt -- soweit verfuegbar -- die Felder:
- snippet : die markierte Textstelle
- type : "orth" | "gram" | "style" | ...
- errorCode : numerischer DKS-Regel-Code
- message : Kurztext der Regel (z.B. "Dieses Wort korrigieren?")
- longMessage : ausfuehrliche Beschreibung der Regel
- examples : Liste von { "false": ..., "correct": ... } Beispielen
derselben Regel -- DIE wichtigste Hilfe, wenn
suggestions leer ist.
- suggestions : ggf. von DKS vorgeschlagene Korrekturen
Wende AUSSCHLIESSLICH die Befunde an. Pro Befund gilt:
a) suggestions gefuellt -> waehle den grammatikalisch und semantisch
passendsten Vorschlag (Numerus, Genus, Kasus, Artikel, Verbkongruenz).
b) suggestions leer -> orientiere dich an longMessage und examples.
Uebertrage das Muster aus den Beispielen ANALOG auf den vorliegenden
Titel (gleiche Regel, anderer konkreter Text).
Bist du dir trotz Regelinfo + Beispielen nicht sicher, lass den
Befund unveraendert.
Antworte ausschliesslich mit dem korrigierten Titel als plain text,
ohne Anfuehrungszeichen, ohne Markdown, ohne Kommentar.
- role: user
text: |
=== DKS-Befunde (JSON) ===
{{#extract_findings_title.findings_json#}}
=== Original-Titel ===
{{#strip_html.title#}}
context: {enabled: false, variable_selector: []}
vision: {enabled: false}
structured_output: {enabled: false}
- id: dks_check_content
position: {x: 750, y: 180}
data:
type: http-request
title: DKS MCP check_text (Inhalt)
desc: "level=3 -- Rechtschreibung+Grammatik+Stil"
method: post
url: "__DKS_MCP_URL__"
authorization:
type: api-key
config:
type: bearer
api_key: "__DKS_API_TOKEN__"
header: ""
headers: "Content-Type:application/json"
params: ""
body:
type: json
data:
- key: ""
type: text
value: '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_text","arguments":{"text":"{{#strip_html.plain#}}","language":"de-DE","level":3}}}'
- id: extract_findings_content
position: {x: 1000, y: 180}
data:
type: code
title: Extract Findings (Inhalt)
code_language: python3
code: |
import json
def main(mcp_body) -> dict:
if isinstance(mcp_body, str):
try: mcp_body = json.loads(mcp_body)
except: mcp_body = {}
if not isinstance(mcp_body, dict):
mcp_body = {}
struct = (mcp_body.get("result") or {}).get("structuredContent") or {}
errors = struct.get("errors", []) or []
findings, md_lines = [], []
for f in errors:
snippet = f.get("snippet", "")
if not snippet:
continue
findings.append({
"snippet": snippet,
"suggestions": (f.get("suggestions") or [])[:8],
"type": f.get("type", ""),
"errorCode": f.get("errorCode"),
"message": f.get("message", "") or "",
"longMessage": f.get("longMessage", "") or "",
"examples": (f.get("examples") or [])[:3],
"group": f.get("group", "") or "",
"category": f.get("category", "") or "",
})
sugs = findings[-1]["suggestions"]
rhs = ("[" + " | ".join(sugs) + "]") if sugs else "(kein Vorschlag)"
extra = " (" + findings[-1]["message"] + ")" if findings[-1]["message"] else ""
md_lines.append("- [Inhalt/" + (findings[-1]["type"] or "?") + "] '" + snippet + "' -> " + rhs + extra)
return {
"findings_json": json.dumps(findings, ensure_ascii=False),
"findings_md": "\n".join(md_lines),
"count": len(findings),
}
variables:
- variable: mcp_body
value_selector: [dks_check_content, body]
outputs:
findings_json: {type: string}
findings_md: {type: string}
count: {type: number}
- id: llm_apply_content
position: {x: 1250, y: 180}
data:
type: llm
title: LLM Apply Corrections (Inhalt)
desc: "Apply DKS findings to the original HTML, preserving every tag, attribute and entity"
model:
provider: langgenius/openai/openai
name: gpt-4o
mode: chat
completion_params:
temperature: 0
max_tokens: 8192
prompt_template:
- role: system
text: |
Du bist ein deterministischer HTML-Korrekturassistent.
Wende AUSSCHLIESSLICH die unten gelisteten Befunde des Duden-Korrektor-Servers (DKS) auf den gegebenen WordPress-HTML-Inhalt an.
Jeder Befund enthält -- soweit verfügbar -- die Felder:
- snippet : die markierte Textstelle im sichtbaren Text
- type : "orth" | "gram" | "style" | ...
- errorCode : numerischer DKS-Regel-Code
- message : Kurztext der Regel (z.B. "Dieses Wort korrigieren?")
- longMessage : ausführliche Erklärung der Regel
- examples : Liste von { "false": ..., "correct": ... } Beispielen
derselben Regel -- maßgebliche Orientierungshilfe
besonders wenn suggestions leer ist.
- group/category : grobe DKS-Kategorisierung
- suggestions : ggf. von DKS vorgeschlagene Korrekturen
Strikte Regeln:
1. Ändere KEINE HTML-Tags, -Attribute, -Klassennamen, -IDs, -URLs oder HTML-Entities. Die Tag-Struktur des Outputs muss zeichengenau mit der des Inputs übereinstimmen; nur reine Text-Knoten dürfen sich ändern.
2. Pro Befund -- abhängig von "suggestions":
a) GEFÜLLT: Wähle den Vorschlag, der grammatikalisch und semantisch am besten in den umgebenden Satz passt -- berücksichtige Numerus (Singular/Plural), Genus, Kasus, Artikel, Verbkongruenz und Satzbedeutung. longMessage und examples beschreiben dabei den Regel-Hintergrund. Beispiel: bei "ein Haii" mit ["Haie","Hai"] wählst du "Hai", weil "ein" Singular verlangt.
b) LEER: DKS hat den Fehler erkannt, aber keinen automatischen Vorschlag erzeugt. Bestimme die Korrektur SELBST anhand von longMessage und examples. Übertrage das in den Beispielen gezeigte Muster ANALOG auf den vorliegenden Text (gleiche Regel, anderer konkreter Wortlaut). Beispiele:
- type "orth", longMessage "Schreibweise unbekannt", examples ["Entgeld"->"Entgelt", "watren"->"warten"]: → "Resteraunt" wird zu "Restaurant", "verschiednen" wird zu "verschiedenen".
- type "gram", longMessage "steht im Nominativ, möglicherweise passt anderer Kasus besser", example "Wir vertrauen Herr Schmidt"->"Wir vertrauen Herrn Schmidt": → "bestellten ... ein komplizierter Cocktail" wird zu "bestellten ... einen komplizierten Cocktail" (Akkusativ nach "bestellen").
- type "style" ohne Vorschlag: meist nicht zwingend ändern -- nur wenn die examples einen trivialen, eindeutigen Fix zeigen.
3. Wenn weder suggestions noch examples eine eindeutige Korrektur ermöglichen, lass den Befund unverändert und ignoriere ihn still.
4. Korrigiere im sichtbaren Text nur die "snippet"-Strings (bzw. bei type "gram": die ggf. längere, durch das snippet umrissene Wortgruppe) -- berücksichtige umliegende Satzzeichen. Erscheint ein Snippet mehrfach und der Kontext erlaubt keine eindeutige Unterscheidung, korrigiere alle Vorkommen.
5. Wenn ein Snippet im sichtbaren Text gar nicht auffindbar ist, ignoriere den Befund still.
6. Füge KEINEN neuen Text, KEINE Erklärungen, KEINE Kommentare und KEINE Markdown-Codeblöcke hinzu.
7. Antworte ausschließlich mit dem korrigierten HTML, beginnend exakt mit dem ersten Zeichen des Originals.
- role: user
text: |
=== DKS-Befunde (JSON-Array) ===
{{#extract_findings_content.findings_json#}}
=== Anzahl Befunde ===
{{#extract_findings_content.count#}}
=== HTML-Quelle ===
{{#strip_html.raw_html#}}
context: {enabled: false, variable_selector: []}
vision: {enabled: false}
structured_output: {enabled: false}
- id: build_payloads
position: {x: 1500, y: 0}
data:
type: code
title: Build WP Payloads
desc: "Kombiniert korrigierten Titel + korrigierten HTML-Inhalt zu Autosave + Comment"
code_language: python3
code: |
import json
def main(post_id,
title_orig: str, corrected_title: str,
raw_html: str, corrected_html: str,
count_title, count_content,
findings_md_title: str, findings_md_content: str) -> dict:
## --- Titel ---------------------------------------------------------
t = (corrected_title or "").strip()
## LLM koennte aus Reflex Anfuehrungszeichen oder Codefences setzen
if t.startswith("```"):
nl = t.find("\n")
t = t[nl+1:] if nl != -1 else t[3:]
if t.endswith("```"):
t = t[:-3]
t = t.strip()
if len(t) >= 2 and t[0] == t[-1] and t[0] in ('"', "'", "\u201c", "\u201e"):
t = t[1:-1].strip()
if not t:
t = title_orig or ""
## --- Inhalt --------------------------------------------------------
s = (corrected_html or "").strip()
if s.startswith("```"):
nl = s.find("\n")
s = s[nl+1:] if nl != -1 else s[3:]
if s.endswith("```"):
s = s[:-3]
s = s.strip()
if not s:
s = raw_html or ""
## --- Autosave + Comment -------------------------------------------
autosave = json.dumps({"title": t, "content": s})
total = int(count_title) + int(count_content)
md_parts = [x for x in [findings_md_title, findings_md_content] if x]
comment = json.dumps({
"post": int(post_id),
"content": "DKS-Vorschlag als Autosave hinterlegt -- im Editor pruefen.\n\n"
+ str(total) + " Befund(e):\n" + ("\n".join(md_parts) or "(keine Befunde)"),
})
return {
"autosave_json": autosave,
"comment_json": comment,
"total_count": total,
}
variables:
- variable: post_id
value_selector: [start, post_id]
- variable: title_orig
value_selector: [strip_html, title]
- variable: corrected_title
value_selector: [llm_apply_title, text]
- variable: raw_html
value_selector: [strip_html, raw_html]
- variable: corrected_html
value_selector: [llm_apply_content, text]
- variable: count_title
value_selector: [extract_findings_title, count]
- variable: count_content
value_selector: [extract_findings_content, count]
- variable: findings_md_title
value_selector: [extract_findings_title, findings_md]
- variable: findings_md_content
value_selector: [extract_findings_content, findings_md]
outputs:
autosave_json: {type: string}
comment_json: {type: string}
total_count: {type: number}
- id: create_autosave
position: {x: 1750, y: 0}
data:
type: http-request
title: Create Autosave
desc: "POST /wp/v2/{type}/{id}/autosaves -- Editor sieht 'neuere Version' beim Oeffnen des Live-Artikels"
method: post
url: "__WP_INTERNAL_URL__/index.php?rest_route=/wp/v2/{{#derive_endpoints.wp_collection#}}/{{#start.post_id#}}/autosaves"
authorization:
type: api-key
config:
type: basic
api_key: "__WP_BASIC_AUTH__"
header: ""
headers: "Content-Type:application/json"
params: ""
body:
type: raw-text
data:
- key: ""
type: text
value: "{{#build_payloads.autosave_json#}}"
- id: add_comment
position: {x: 2000, y: 0}
data:
type: http-request
title: Add Editor Comment
desc: "POST comment on the original post summarising findings (no-op fuer pages mit deaktivierten comments)"
method: post
url: "__WP_INTERNAL_URL__/index.php?rest_route=/wp/v2/comments"
authorization:
type: api-key
config:
type: basic
api_key: "__WP_BASIC_AUTH__"
header: ""
headers: "Content-Type:application/json"
params: ""
body:
type: raw-text
data:
- key: ""
type: text
value: "{{#build_payloads.comment_json#}}"
- id: end
position: {x: 2250, y: 0}
data:
type: end
title: End
outputs:
- variable: autosave_status
value_selector: [create_autosave, status_code]
- variable: comment_status
value_selector: [add_comment, status_code]
- variable: findings_count
value_selector: [build_payloads, total_count]
- variable: findings_count_title
value_selector: [extract_findings_title, count]
- variable: findings_count_content
value_selector: [extract_findings_content, count]
- variable: wp_collection
value_selector: [derive_endpoints, wp_collection]
edges:
- {id: e0, source: start, target: derive_endpoints, sourceHandle: source}
- {id: e1, source: derive_endpoints, target: fetch_post, sourceHandle: source}
- {id: e2, source: fetch_post, target: strip_html, sourceHandle: source}
## Titel-Branch
- {id: e3t, source: strip_html, target: dks_check_title, sourceHandle: source}
- {id: e4t, source: dks_check_title, target: extract_findings_title, sourceHandle: source}
- {id: e5t, source: extract_findings_title, target: llm_apply_title, sourceHandle: source}
- {id: e6t, source: llm_apply_title, target: build_payloads, sourceHandle: source}
## Inhalt-Branch
- {id: e3c, source: strip_html, target: dks_check_content, sourceHandle: source}
- {id: e4c, source: dks_check_content, target: extract_findings_content,sourceHandle: source}
- {id: e5c, source: extract_findings_content, target: llm_apply_content, sourceHandle: source}
- {id: e6c, source: llm_apply_content, target: build_payloads, sourceHandle: source}
## Merge -> WP
- {id: e7, source: build_payloads, target: create_autosave, sourceHandle: source}
- {id: e8, source: create_autosave, target: add_comment, sourceHandle: source}
- {id: e9, source: add_comment, target: end, sourceHandle: source}
