docs: name tools and error codes the way the client sees them

Indirect abilities are written lit/<name> (via mcp-adapter-execute-ability) in
guides, descriptions and hints, direct tools lit-<name>; guides use the
client-visible codes (write_failed, partial_write) and say exactly which tools
return a revision_id. ToolNamingTest keeps guides and sources consistent with
Ability::isDirect() and checks the overview's direct-tool list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 01:28:18 +02:00
co-authored by Claude Opus 5.5
parent b3c9109e47
commit fea75b03d6
19 changed files with 186 additions and 55 deletions
+26 -14
View File
@@ -1,5 +1,8 @@
# Inhalte, Felder und Status # Inhalte, Felder und Status
Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities, die über
`mcp-adapter-execute-ability` (Name in `ability_name`, Eingaben in `parameters`) aufgerufen werden.
## Inhaltstypen erkennen ## Inhaltstypen erkennen
- `lit-content-types` ohne Parameter listet alle Inhaltstypen (Seiten, Beiträge, eigene Typen) mit - `lit-content-types` ohne Parameter listet alle Inhaltstypen (Seiten, Beiträge, eigene Typen) mit
@@ -32,20 +35,27 @@
- Begriffe: `terms: {"category": [4, "Neuer Begriff"]}` ersetzt die Zuordnung dieser Taxonomie (IDs oder - Begriffe: `terms: {"category": [4, "Neuer Begriff"]}` ersetzt die Zuordnung dieser Taxonomie (IDs oder
Namen; unbekannte Namen werden angelegt, falls erlaubt). Namen; unbekannte Namen werden angelegt, falls erlaubt).
- Felder leeren: Wert `null`. Pflichtfelder können nicht geleert werden. - Felder leeren: Wert `null`. Pflichtfelder können nicht geleert werden.
- Beziehungsfelder erwarten Eintrags-IDs, Datei-/Bildfelder Anhang-IDs aus der Mediathek. - Beziehungsfelder erwarten IDs von Einträgen, die der Nutzer lesen darf; Datei-/Bildfelder Anhang-IDs aus der
Mediathek.
- Einträge im Papierkorb werden nicht geändert (auch nicht veröffentlicht, geplant oder wiederhergestellt);
zuerst in WordPress aus dem Papierkorb holen.
- Ist ein geplanter Eintrag über seinen Veröffentlichungszeitpunkt hinaus (verpasster Termin), würde jedes
Speichern ihn sofort veröffentlichen – `lit-content-update`, `lit/revision-restore` und
`lit/post-replace-with-copy` verweigern das dann. Mit dem Nutzer klären: jetzt veröffentlichen
(`lit-post-publish`) oder neu planen (`lit/post-schedule`).
## Status ## Status
- Veröffentlichen, Zurückziehen und Planen sind eigene Werkzeuge (`lit-post-publish`, `lit-post-unpublish`, - Veröffentlichen, Zurückziehen und Planen sind eigene Werkzeuge (`lit-post-publish`, `lit/post-unpublish`,
`lit-post-schedule` mit `date` = „JJJJ-MM-TT HH:MM“ in der Zeitzone der Website). Nur auf ausdrücklichen `lit/post-schedule` mit `date` = „JJJJ-MM-TT HH:MM“ in der Zeitzone der Website). Nur auf ausdrücklichen
Wunsch des Nutzers verwenden. Wunsch des Nutzers verwenden.
- `lit-post-trash` legt in den Papierkorb (nie endgültig) und braucht `confirm: true` nach Rückfrage. - `lit/post-trash` legt in den Papierkorb (nie endgültig) und braucht `confirm: true` nach Rückfrage.
## Sicher überarbeiten ## Sicher überarbeiten
1. `lit-post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`. 1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`.
2. Kopie mit `lit-content-update` (oder den Divi-Tools) bearbeiten und dem Nutzer die Vorschau zeigen. 2. Kopie mit `lit-content-update` (oder den Divi-Tools) bearbeiten und dem Nutzer die Vorschau zeigen.
3. `lit-post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben). 3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben).
Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus
`lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am `lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am
Original geändert wurden, werden dabei überschrieben), und erst nach Rückfrage mit `confirm: true` Original geändert wurden, werden dabei überschrieben), und erst nach Rückfrage mit `confirm: true`
@@ -53,14 +63,16 @@
Papierkorb (`backup_id` in der Antwort); danach wandert die Kopie ebenfalls in den Papierkorb. Ist der Papierkorb (`backup_id` in der Antwort); danach wandert die Kopie ebenfalls in den Papierkorb. Ist der
Papierkorb der Website deaktiviert, wird der Austausch verweigert, statt die Kopie endgültig zu löschen. Papierkorb der Website deaktiviert, wird der Austausch verweigert, statt die Kopie endgültig zu löschen.
Jede Änderung liefert `revision_id` (Stand vorher) und `log_id`. Rückgängig: `lit-revisions-list` → Jede Änderung liefert `log_id`. Eine `revision_id` (Revision mit dem Stand vorher) liefern nur
`lit-revision-restore` mit der `rev` des Eintrags (aus `lit-content-get`) und `confirm: true`. Revisionen `lit-content-update`, `lit/post-replace-with-copy` und `lit/revision-restore`; bei allen anderen Tools ist sie
umfassen Titel, Inhalt und Auszug – Felder und Begriffe nicht. `null`. Rückgängig: `lit/revisions-list` → `lit/revision-restore` mit der `rev` des Eintrags (aus
`lit-content-get`) und `confirm: true`. Revisionen umfassen Titel, Inhalt und Auszug – Felder und Begriffe
nicht (dafür bei `lit/post-replace-with-copy` die Sicherung `backup_id`).
## Wenn ein Schreibvorgang fehlschlägt ## Wenn ein Schreibvorgang fehlschlägt
Schlägt ein Schreibvorgang nach ersten Änderungen fehl, meldet das Tool `lit_write_failed` (alles wurde Fehler stecken als JSON (`code`, `message`, `hint`, ggf. weitere Felder) in der Fehlermeldung. Schlägt ein
zurückgenommen, nichts wurde gespeichert) oder `lit_partial_write` (die Rücknahme war unvollständig). Beide Schreibvorgang nach ersten Änderungen fehl, ist `code` = `write_failed` (alles wurde zurückgenommen, nichts
Fehler stecken als JSON (`code`, `message`, `hint`, ggf. weitere Felder) in der Fehlermeldung. Bei wurde gespeichert) oder `partial_write` (die Rücknahme war unvollständig). Bei `partial_write` vor einem
`lit_partial_write` vor einem erneuten Versuch immer erst den aktuellen Stand prüfen (`lit-content-get`, erneuten Versuch immer erst den aktuellen Stand prüfen (`lit-content-get`,
`lit-menu-get` o. Ä.) statt den Aufruf einfach zu wiederholen – dem `hint` folgen. `lit/menu-get` o. Ä.) statt den Aufruf einfach zu wiederholen – dem `hint` folgen.
+6 -3
View File
@@ -20,7 +20,8 @@ des verbundenen WordPress-Benutzers.
Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`, Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`,
`lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`. `lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`.
Über `mcp-adapter-discover-abilities` / `mcp-adapter-execute-ability` (Ability-Namen `lit/…`): Über `mcp-adapter-discover-abilities` / `mcp-adapter-execute-ability` (Ability-Namen `lit/…`; in Guides und
Hinweisen immer so geschrieben):
Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`lit/post-duplicate`, Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`lit/post-duplicate`,
`lit/post-replace-with-copy`), Revisionen (`lit/revisions-list`, `lit/revision-restore`), Menüs `lit/post-replace-with-copy`), Revisionen (`lit/revisions-list`, `lit/revision-restore`), Menüs
(`lit/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`), (`lit/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`),
@@ -30,6 +31,8 @@ Weitere Themen: `lit-guide` mit `topic` = `content-types` oder `workflows`.
## Rückmeldungen ## Rückmeldungen
- Erfolg: `ok`, `summary`, `changed`, `warnings`, `revision_id` (Stand vorher), `log_id`, URLs. - Erfolg: `ok`, `summary`, `changed`, `warnings`, `rev`, `revision_id` (Revision mit dem Stand vorher, nur bei
- Fehler: JSON mit `code`, `message`, `hint` (und ggf. `fields`). Dem `hint` folgen. Tools, die Inhalt überschreiben, sonst `null`), `log_id`, URLs.
- Fehler: JSON mit `code` (z. B. `validation_failed`, `rev_mismatch`, `write_failed`, `partial_write`),
`message`, `hint` (und ggf. `fields`). Dem `hint` folgen.
- `confirm_required`: Nutzer fragen, dann mit `confirm: true` wiederholen. - `confirm_required`: Nutzer fragen, dann mit `confirm: true` wiederholen.
+4 -4
View File
@@ -3,18 +3,18 @@
## Neue Website aufsetzen ## Neue Website aufsetzen
1. `lit-site-status` und `lit-content-types` lesen. 1. `lit-site-status` und `lit-content-types` lesen.
2. Grundeinstellungen: `lit-wp-settings-get`, dann `lit-wp-settings-update` (Titel, Untertitel, Sprache, 2. Grundeinstellungen: `lit/wp-settings-get`, dann `lit/wp-settings-update` (Titel, Untertitel, Sprache,
Zeitzone, Datumsformat, Permalinks `/%postname%/`). `previous` in der Antwort enthält die alten Werte. Zeitzone, Datumsformat, Permalinks `/%postname%/`). `previous` in der Antwort enthält die alten Werte.
3. Seiten als Entwürfe anlegen (`lit-content-create`), Hierarchie über `parent`. 3. Seiten als Entwürfe anlegen (`lit-content-create`), Hierarchie über `parent`.
4. Startseite: Seite veröffentlichen (nach Rückfrage), dann 4. Startseite: Seite veröffentlichen (nach Rückfrage), dann
`{"settings": {"page_on_front": <ID>, "show_on_front": "page"}}`. `{"settings": {"page_on_front": <ID>, "show_on_front": "page"}}`.
5. Menü: `lit-menu-create` (optional mit `location` aus `lit-menu-list`), Einträge mit `lit-menu-set-items`. 5. Menü: `lit/menu-create` (optional mit `location` aus `lit/menu-list`), Einträge mit `lit/menu-set-items`.
6. Veröffentlichen erst, wenn der Nutzer es ausdrücklich möchte. 6. Veröffentlichen erst, wenn der Nutzer es ausdrücklich möchte.
## Menüs pflegen ## Menüs pflegen
- `lit-menu-get` liefert den Baum. Für größere Umbauten den Baum verändern und vollständig als `items` an - `lit/menu-get` liefert den Baum. Für größere Umbauten den Baum verändern und vollständig als `items` an
`lit-menu-set-items` senden (bestehende Einträge mit `id`, neue ohne `id`). Wird der Baum unverändert `lit/menu-set-items` senden (bestehende Einträge mit `id`, neue ohne `id`). Wird der Baum unverändert
zurückgeschickt, meldet das Tool „keine Änderung“ und schreibt nichts. zurückgeschickt, meldet das Tool „keine Änderung“ und schreibt nichts.
- Für kleine Änderungen `operations`: `{op: "add", item, parent_id?, position?}`, - Für kleine Änderungen `operations`: `{op: "add", item, parent_id?, position?}`,
`{op: "move", id, parent_id?, position?}`, `{op: "remove", id}` (Kinder eines entfernten Eintrags rücken `{op: "move", id, parent_id?, position?}`, `{op: "remove", id}` (Kinder eines entfernten Eintrags rücken
+3 -2
View File
@@ -144,8 +144,9 @@ final class Errors
trim('Speichern fehlgeschlagen: ' . $reason) . ' Die Änderung konnte nicht vollständig zurückgenommen ' trim('Speichern fehlgeschlagen: ' . $reason) . ' Die Änderung konnte nicht vollständig zurückgenommen '
. 'werden. ' . $state, . 'werden. ' . $state,
$hint ?? 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand prüfen (lit-content-get, bei ' $hint ?? 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand prüfen (lit-content-get, bei '
. 'einem neuen Entwurf lit-content-query) und ggf. über lit-revisions-list den vorherigen Stand ' . 'einem neuen Entwurf lit-content-query) und ggf. mit lit/revisions-list und lit/revision-restore '
. 'wiederherstellen; dem Nutzer Bescheid geben.', . '(über mcp-adapter-execute-ability) den vorherigen Stand wiederherstellen; dem Nutzer Bescheid '
. 'geben.',
500, 500,
$details $details
); );
@@ -34,7 +34,7 @@ final class MenuAssignLocationAbility implements Ability
public function description(): string public function description(): string
{ {
return 'Weist einer Theme-Position (siehe lit-menu-list) ein Menü zu; menu_id 0 entfernt die Zuweisung. ' return 'Weist einer Theme-Position (siehe lit/menu-list) ein Menü zu; menu_id 0 entfernt die Zuweisung. '
. 'Das vorher zugewiesene Menü steht in previous_menu_id.'; . 'Das vorher zugewiesene Menü steht in previous_menu_id.';
} }
@@ -86,7 +86,7 @@ final class MenuAssignLocationAbility implements Ability
} }
if ($menuId > 0 && ! is_object(wp_get_nav_menu_object($menuId))) { if ($menuId > 0 && ! is_object(wp_get_nav_menu_object($menuId))) {
return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.');
} }
$previous = MenuLocations::assigned()[$location] ?? 0; $previous = MenuLocations::assigned()[$location] ?? 0;
@@ -34,7 +34,7 @@ final class MenuCreateAbility implements Ability
public function description(): string public function description(): string
{ {
return 'Legt ein leeres Menü an und weist es optional einer Theme-Position zu. Einträge danach mit ' return 'Legt ein leeres Menü an und weist es optional einer Theme-Position zu. Einträge danach mit '
. 'lit-menu-set-items setzen.'; . 'lit/menu-set-items setzen.';
} }
public function inputSchema(): array public function inputSchema(): array
@@ -43,7 +43,7 @@ final class MenuCreateAbility implements Ability
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'name' => ['type' => 'string', 'minLength' => 1], 'name' => ['type' => 'string', 'minLength' => 1],
'location' => ['type' => 'string', 'description' => 'Theme-Position aus lit-menu-list.'], 'location' => ['type' => 'string', 'description' => 'Theme-Position aus lit/menu-list.'],
], ],
'required' => ['name'], 'required' => ['name'],
'additionalProperties' => false, 'additionalProperties' => false,
@@ -88,7 +88,7 @@ final class MenuCreateAbility implements Ability
return Errors::invalidInput( return Errors::invalidInput(
'Menü konnte nicht angelegt werden: ' . $menuId->get_error_message(), 'Menü konnte nicht angelegt werden: ' . $menuId->get_error_message(),
['name' => 'bereits vergeben oder ungültig'], ['name' => 'bereits vergeben oder ungültig'],
'Vorhandene Menüs über lit-menu-list abrufen.' 'Vorhandene Menüs über lit/menu-list abrufen.'
); );
} }
@@ -29,7 +29,7 @@ final class MenuGetAbility implements Ability
return 'Liefert ein Menü als Baum (id, title, type post/taxonomy/custom, object/object_id, url, ' return 'Liefert ein Menü als Baum (id, title, type post/taxonomy/custom, object/object_id, url, '
. 'children). object_id und url sind bei post/taxonomy-Einträgen nur informativ (WordPress leitet ' . 'children). object_id und url sind bei post/taxonomy-Einträgen nur informativ (WordPress leitet '
. 'sie aus dem verlinkten Objekt ab); nur bei custom-Einträgen ist url maßgeblich. Der Baum kann ' . 'sie aus dem verlinkten Objekt ab); nur bei custom-Einträgen ist url maßgeblich. Der Baum kann '
. 'bearbeitet an lit-menu-set-items zurückgegeben werden.'; . 'bearbeitet an lit/menu-set-items zurückgegeben werden.';
} }
public function inputSchema(): array public function inputSchema(): array
@@ -78,7 +78,7 @@ final class MenuGetAbility implements Ability
$menuId = (int) ($input['menu_id'] ?? 0); $menuId = (int) ($input['menu_id'] ?? 0);
$menu = wp_get_nav_menu_object($menuId); $menu = wp_get_nav_menu_object($menuId);
if (! is_object($menu)) { if (! is_object($menu)) {
return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.');
} }
$items = wp_get_nav_menu_items($menuId); $items = wp_get_nav_menu_items($menuId);
@@ -44,7 +44,7 @@ final class MenuSetItemsAbility implements Ability
return 'Setzt die Einträge eines Menüs. Entweder items = vollständiger Zielbaum oder operations = Liste aus ' return 'Setzt die Einträge eines Menüs. Entweder items = vollständiger Zielbaum oder operations = Liste aus '
. '{op: add, item, parent_id?, position?}, {op: move, id, parent_id?, position?}, {op: remove, id} ' . '{op: add, item, parent_id?, position?}, {op: move, id, parent_id?, position?}, {op: remove, id} '
. '(Kinder eines entfernten Eintrags rücken an seine Stelle; position 0-basiert, ohne = ans Ende). ' . '(Kinder eines entfernten Eintrags rücken an seine Stelle; position 0-basiert, ohne = ans Ende). '
. 'Der Baum aus lit-menu-get kann bearbeitet unverändert zurückgegeben werden: bestehende Einträge ' . 'Der Baum aus lit/menu-get kann bearbeitet unverändert zurückgegeben werden: bestehende Einträge '
. 'werden über id erkannt, type/object/object_id und bei post/taxonomy auch url sind nur informativ ' . 'werden über id erkannt, type/object/object_id und bei post/taxonomy auch url sind nur informativ '
. 'und werden ignoriert; title wird nur bei Änderung zur eigenen Beschriftung, url nur bei custom ' . 'und werden ignoriert; title wird nur bei Änderung zur eigenen Beschriftung, url nur bei custom '
. 'geändert. Neue Einträge ohne id: type post|taxonomy mit object_id oder custom mit url (/pfad, ' . 'geändert. Neue Einträge ohne id: type post|taxonomy mit object_id oder custom mit url (/pfad, '
@@ -70,7 +70,7 @@ final class MenuSetItemsAbility implements Ability
'items' => [ 'items' => [
'type' => 'array', 'type' => 'array',
'items' => ['type' => 'object'], 'items' => ['type' => 'object'],
'description' => 'Vollständiger Zielbaum (Knoten wie in lit-menu-get, neue Einträge ohne id).', 'description' => 'Vollständiger Zielbaum (Knoten wie in lit/menu-get, neue Einträge ohne id).',
], ],
'operations' => [ 'operations' => [
'type' => 'array', 'type' => 'array',
@@ -132,7 +132,7 @@ final class MenuSetItemsAbility implements Ability
$menuId = (int) ($input['menu_id'] ?? 0); $menuId = (int) ($input['menu_id'] ?? 0);
$menu = wp_get_nav_menu_object($menuId); $menu = wp_get_nav_menu_object($menuId);
if (! is_object($menu)) { if (! is_object($menu)) {
return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit-menu-list abrufen.'); return Errors::notFound(sprintf('Menü %d nicht gefunden.', $menuId), 'Menüs über lit/menu-list abrufen.');
} }
if (array_key_exists('items', $input) === array_key_exists('operations', $input)) { if (array_key_exists('items', $input) === array_key_exists('operations', $input)) {
@@ -203,7 +203,7 @@ final class MenuSetItemsAbility implements Ability
$after = FollowUp::run( $after = FollowUp::run(
fn () => MenuTree::build(array_values($this->currentItems($menuId))), fn () => MenuTree::build(array_values($this->currentItems($menuId))),
$this->name() . ' re-read', $this->name() . ' re-read',
'Das Menü ist gespeichert, der neue Stand konnte aber nicht gelesen werden; mit lit-menu-get abrufen.', 'Das Menü ist gespeichert, der neue Stand konnte aber nicht gelesen werden; mit lit/menu-get abrufen.',
$warnings $warnings
); );
@@ -462,9 +462,10 @@ class MenuWriter
$cause, $cause,
$detail, $detail,
sprintf('Menü %d ist möglicherweise teilweise geändert worden.', $menuId), sprintf('Menü %d ist möglicherweise teilweise geändert worden.', $menuId),
'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit-menu-get prüfen und ihn ggf. ' 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit/menu-get prüfen und ihn ggf. '
. 'mit lit-menu-set-items anhand von previous_items (Stand vorher) wiederherstellen; entfernte ' . 'mit lit/menu-set-items (beide über mcp-adapter-execute-ability) anhand von previous_items (Stand '
. 'Einträge müssen dabei neu angelegt werden. Dem Nutzer Bescheid geben.', . 'vorher) wiederherstellen; entfernte Einträge müssen dabei neu angelegt werden. Dem Nutzer Bescheid '
. 'geben.',
['menu_id' => $menuId, 'previous_items' => MenuTree::build(array_values($current))] ['menu_id' => $menuId, 'previous_items' => MenuTree::build(array_values($current))]
); );
} }
@@ -146,7 +146,7 @@ final class RevisionRestoreAbility implements Ability
$revisionId = PostGuard::id($input['revision_id'] ?? null); $revisionId = PostGuard::id($input['revision_id'] ?? null);
$revision = $revisionId === null ? null : get_post($revisionId); $revision = $revisionId === null ? null : get_post($revisionId);
if (! $revision instanceof WP_Post || $revision->post_type !== 'revision') { if (! $revision instanceof WP_Post || $revision->post_type !== 'revision') {
return Errors::notFound('Revision nicht gefunden.', 'Revisionen über lit-revisions-list abrufen.'); return Errors::notFound('Revision nicht gefunden.', 'Revisionen über lit/revisions-list abrufen.');
} }
// field 'revision_id': post_parent 0 (an orphaned revision) is the only id this // field 'revision_id': post_parent 0 (an orphaned revision) is the only id this
@@ -41,7 +41,7 @@ final class RevisionsListAbility implements Ability
{ {
return 'Listet die gespeicherten Revisionen eines Eintrags (neueste zuerst) mit Datum, Autor und ' return 'Listet die gespeicherten Revisionen eines Eintrags (neueste zuerst) mit Datum, Autor und '
. 'Textauszug. Revisionen von Divi-Inhalten sind mit divi: true markiert; ihr excerpt ist dann ' . 'Textauszug. Revisionen von Divi-Inhalten sind mit divi: true markiert; ihr excerpt ist dann '
. 'Shortcode-/Block-Markup, kein lesbarer Text. Wiederherstellen mit lit-revision-restore.'; . 'Shortcode-/Block-Markup, kein lesbarer Text. Wiederherstellen mit lit/revision-restore.';
} }
public function inputSchema(): array public function inputSchema(): array
@@ -27,7 +27,7 @@ final class WpSettingsGetAbility implements Ability
public function description(): string public function description(): string
{ {
return 'Liest die freigegebenen WordPress-Einstellungen (Allgemein, Lesen, Diskussion, Permalinks, ' return 'Liest die freigegebenen WordPress-Einstellungen (Allgemein, Lesen, Diskussion, Permalinks, '
. 'Website-Icon) mit Typ und Beschreibung. Ändern mit lit-wp-settings-update.'; . 'Website-Icon) mit Typ und Beschreibung. Ändern mit lit/wp-settings-update.';
} }
public function inputSchema(): array public function inputSchema(): array
@@ -44,7 +44,7 @@ final class WpSettingsUpdateAbility implements Ability
public function description(): string public function description(): string
{ {
return 'Ändert freigegebene WordPress-Einstellungen (Schlüssel siehe lit-wp-settings-get). Alle Werte ' return 'Ändert freigegebene WordPress-Einstellungen (Schlüssel siehe lit/wp-settings-get). Alle Werte '
. 'werden vorher geprüft – bei einem Fehler wird nichts gespeichert. previous enthält die alten Werte. ' . 'werden vorher geprüft – bei einem Fehler wird nichts gespeichert. previous enthält die alten Werte. '
. 'Gesperrt: admin_email, users_can_register, default_role, siteurl, home.'; . 'Gesperrt: admin_email, users_can_register, default_role, siteurl, home.';
} }
@@ -98,7 +98,7 @@ final class WpSettingsUpdateAbility implements Ability
return Errors::invalidInput( return Errors::invalidInput(
'Einstellungen ungültig; nichts wurde gespeichert.', 'Einstellungen ungültig; nichts wurde gespeichert.',
$errors, $errors,
'Werte gemäß fields korrigieren; aktuelle Werte über lit-wp-settings-get.' 'Werte gemäß fields korrigieren; aktuelle Werte über lit/wp-settings-get.'
); );
} }
@@ -255,7 +255,8 @@ final class WpSettingsUpdateAbility implements Ability
$cause, $cause,
$cause->getMessage() . '; not restored: ' . implode(', ', $stuck), $cause->getMessage() . '; not restored: ' . implode(', ', $stuck),
sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)), sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)),
'Aktuelle Werte über lit/wp-settings-get prüfen und ggf. erneut auf die gewünschten Werte setzen.', 'Aktuelle Werte über lit/wp-settings-get (mcp-adapter-execute-ability) prüfen und ggf. erneut auf die '
. 'gewünschten Werte setzen.',
['keys' => $stuck] ['keys' => $stuck]
); );
} }
@@ -41,7 +41,7 @@ final class PostDuplicateAbility implements Ability
{ {
return 'Legt eine Entwurfskopie eines Eintrags an (Inhalt, Felder/Meta, Begriffe, Beitragsbild, Template, ' return 'Legt eine Entwurfskopie eines Eintrags an (Inhalt, Felder/Meta, Begriffe, Beitragsbild, Template, '
. 'Divi-Einstellungen). Typischer Ablauf: Kopie bearbeiten, prüfen, dann mit ' . 'Divi-Einstellungen). Typischer Ablauf: Kopie bearbeiten, prüfen, dann mit '
. 'lit-post-replace-with-copy ins Original übernehmen – so bleibt die veröffentlichte Seite bis dahin ' . 'lit/post-replace-with-copy ins Original übernehmen – so bleibt die veröffentlichte Seite bis dahin '
. 'unverändert. Die Antwort enthält die rev der Kopie für lit-content-update.'; . 'unverändert. Die Antwort enthält die rev der Kopie für lit-content-update.';
} }
@@ -60,7 +60,7 @@ final class PostReplaceWithCopyAbility implements Ability
public function description(): string public function description(): string
{ {
return 'Übernimmt Titel, Inhalt, Felder/Meta und Begriffe einer mit lit-post-duplicate erstellten Kopie ' return 'Übernimmt Titel, Inhalt, Felder/Meta und Begriffe einer mit lit/post-duplicate erstellten Kopie '
. 'ins Original. ID, URL, Status und Menüeinträge des Originals bleiben; vorher entsteht eine ' . 'ins Original. ID, URL, Status und Menüeinträge des Originals bleiben; vorher entsteht eine '
. 'Revision und eine vollständige Sicherung des Originals (im Papierkorb, backup_id), die Kopie kommt ' . 'Revision und eine vollständige Sicherung des Originals (im Papierkorb, backup_id), die Kopie kommt '
. 'danach in den Papierkorb. Die Kopie muss Entwurf oder ausstehend sein. rev des Originals aus ' . 'danach in den Papierkorb. Die Kopie muss Entwurf oder ausstehend sein. rev des Originals aus '
@@ -165,8 +165,8 @@ final class PostReplaceWithCopyAbility implements Ability
if ($copy->post_type !== $original->post_type || $copyOf !== $originalId || $copyId === $originalId) { if ($copy->post_type !== $original->post_type || $copyOf !== $originalId || $copyId === $originalId) {
return Errors::invalidInput( return Errors::invalidInput(
'Die Kopie gehört nicht zu diesem Original.', 'Die Kopie gehört nicht zu diesem Original.',
['copy_id' => sprintf('keine mit lit-post-duplicate erstellte Kopie von %d', $originalId)], ['copy_id' => sprintf('keine mit lit/post-duplicate erstellte Kopie von %d', $originalId)],
'Kopie mit lit-post-duplicate aus diesem Original erstellen.' 'Kopie mit lit/post-duplicate aus diesem Original erstellen.'
); );
} }
@@ -174,7 +174,7 @@ final class PostReplaceWithCopyAbility implements Ability
return Errors::invalidInput( return Errors::invalidInput(
sprintf('Die Kopie hat den Status "%s"; übernommen wird nur ein Entwurf.', $copy->post_status), sprintf('Die Kopie hat den Status "%s"; übernommen wird nur ein Entwurf.', $copy->post_status),
['copy_id' => 'Kopie muss Entwurf oder ausstehend sein'], ['copy_id' => 'Kopie muss Entwurf oder ausstehend sein'],
'Mit lit-post-duplicate eine neue Kopie erstellen (bzw. die Kopie in WordPress wiederherstellen).' 'Mit lit/post-duplicate eine neue Kopie erstellen (bzw. die Kopie in WordPress wiederherstellen).'
); );
} }
@@ -290,9 +290,9 @@ final class PostReplaceWithCopyAbility implements Ability
$backupId $backupId
), ),
'Den Aufruf nicht einfach wiederholen. Zuerst den Stand mit lit-content-get prüfen; Titel, Inhalt ' 'Den Aufruf nicht einfach wiederholen. Zuerst den Stand mit lit-content-get prüfen; Titel, Inhalt '
. 'und Auszug lassen sich mit lit/revision-restore (revision_id) zurückholen, Felder/Meta und ' . 'und Auszug lassen sich mit lit/revision-restore (revision_id, über '
. 'Begriffe stehen in der Sicherung backup_id (in WordPress aus dem Papierkorb holen). Dem ' . 'mcp-adapter-execute-ability) zurückholen, Felder/Meta und Begriffe stehen in der Sicherung '
. 'Nutzer Bescheid geben.', . 'backup_id (in WordPress aus dem Papierkorb holen). Dem Nutzer Bescheid geben.',
$details $details
); );
} }
+2 -1
View File
@@ -76,7 +76,8 @@ final class ErrorsTest extends TestCase
self::assertStringContainsString('Eintrag 5', $payload['message']); self::assertStringContainsString('Eintrag 5', $payload['message']);
self::assertStringContainsString('Revision 811', $payload['message']); self::assertStringContainsString('Revision 811', $payload['message']);
self::assertStringContainsString('lit-content-get', $payload['hint']); self::assertStringContainsString('lit-content-get', $payload['hint']);
self::assertStringContainsString('lit-revisions-list', $payload['hint']); self::assertStringContainsString('lit/revisions-list', $payload['hint']);
self::assertStringContainsString('mcp-adapter-execute-ability', $payload['hint']);
self::assertStringContainsString('nicht einfach wiederholen', $payload['hint']); self::assertStringContainsString('nicht einfach wiederholen', $payload['hint']);
self::assertSame(5, $payload['post_id']); self::assertSame(5, $payload['post_id']);
self::assertSame(811, $payload['revision_id']); self::assertSame(811, $payload['revision_id']);
+2 -2
View File
@@ -36,14 +36,14 @@ final class WriteFailedTest extends TestCase
'Menü nicht gespeichert.', 'Menü nicht gespeichert.',
false, false,
state: 'Das Menü ist teilweise geändert.', state: 'Das Menü ist teilweise geändert.',
hint: 'Mit lit-menu-get prüfen.', hint: 'Mit lit/menu-get prüfen.',
details: ['menu_id' => 3] details: ['menu_id' => 3]
))->toError(); ))->toError();
self::assertSame('lit_partial_write', $error->get_error_code()); self::assertSame('lit_partial_write', $error->get_error_code());
self::assertStringContainsString('Das Menü ist teilweise geändert.', $error->get_error_message()); self::assertStringContainsString('Das Menü ist teilweise geändert.', $error->get_error_message());
self::assertStringNotContainsString('Entwurf', $error->get_error_message()); self::assertStringNotContainsString('Entwurf', $error->get_error_message());
self::assertSame('Mit lit-menu-get prüfen.', $error->get_error_data()['hint']); self::assertSame('Mit lit/menu-get prüfen.', $error->get_error_data()['hint']);
self::assertSame(3, $error->get_error_data()['menu_id']); self::assertSame(3, $error->get_error_data()['menu_id']);
} }
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Guides;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Tests\Unit\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
/**
* Guides, descriptions and hints must name tools the way the client sees them: direct
* tools as lit-<name>, indirect ones as lit/<name> (reached via mcp-adapter-execute-ability),
* and error codes without the internal lit_ prefix of the WP_Error code.
*/
final class ToolNamingTest extends TestCase
{
/** @return array<string, bool> Ability name without "lit/" => isDirect(). */
private static function abilities(): array
{
$root = dirname(__DIR__, 3);
$abilities = [];
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src'));
foreach ($files as $file) {
if (! str_ends_with((string) $file, 'Ability.php')) {
continue;
}
$class = 'LIT\\MCP\\' . str_replace(['/', '.php'], ['\\', ''], substr((string) $file, strlen($root . '/src/')));
$reflection = new ReflectionClass($class);
if ($reflection->isInterface() || ! $reflection->implementsInterface(Ability::class)) {
continue;
}
/** @var Ability $ability */
$ability = $reflection->newInstanceWithoutConstructor();
$abilities[substr($ability->name(), strlen('lit/'))] = $ability->isDirect();
}
ksort($abilities);
return $abilities;
}
/** @return array<string, string> Path => content of the guides and all PHP sources. */
private static function texts(): array
{
$root = dirname(__DIR__, 3);
$texts = [];
foreach (glob($root . '/docs/guides/*.md') ?: [] as $file) {
$texts[$file] = (string) file_get_contents($file);
}
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src'));
foreach ($files as $file) {
if (str_ends_with((string) $file, '.php')) {
$texts[(string) $file] = (string) file_get_contents((string) $file);
}
}
return $texts;
}
public function testIndirectToolsAreNeverNamedLikeDirectOnes(): void
{
$abilities = self::abilities();
self::assertArrayHasKey('menu-set-items', $abilities);
foreach (self::texts() as $path => $text) {
preg_match_all('/\blit-([a-z]+(?:-[a-z]+)*)/', $text, $matches);
foreach ($matches[1] as $name) {
self::assertFalse(
($abilities[$name] ?? true) === false,
sprintf('%s names the indirect tool lit/%s as lit-%s', basename($path), $name, $name)
);
}
preg_match_all('#\blit/([a-z]+(?:-[a-z]+)*)#', $text, $matches);
foreach ($matches[1] as $name) {
self::assertFalse(
($abilities[$name] ?? false) === true && str_ends_with($path, '.md'),
sprintf('%s names the direct tool lit-%s as lit/%s', basename($path), $name, $name)
);
}
}
}
public function testGuidesUseTheErrorCodesTheClientSees(): void
{
foreach (self::texts() as $path => $text) {
if (str_ends_with($path, '.md')) {
self::assertDoesNotMatchRegularExpression('/`lit_(?!mcp)[a-z_]+`/', $text, basename($path));
}
}
}
public function testOverviewListsExactlyTheDirectTools(): void
{
$overview = (string) file_get_contents(dirname(__DIR__, 3) . '/docs/guides/overview.md');
preg_match('/Direkt sichtbar:(.*?)\n\n/s', $overview, $section);
preg_match_all('/`lit-([a-z-]+)`/', $section[1] ?? '', $listed);
$direct = array_keys(array_filter(self::abilities()));
$listedNames = $listed[1];
sort($listedNames);
self::assertSame($direct, $listedNames);
}
}
@@ -360,7 +360,8 @@ final class MenuWriterTest extends TestCase
self::assertSame('lit_partial_write', $error->get_error_code()); self::assertSame('lit_partial_write', $error->get_error_code());
self::assertSame(3, $error->get_error_data()['menu_id']); self::assertSame(3, $error->get_error_data()['menu_id']);
self::assertSame('Home', $error->get_error_data()['previous_items'][0]['title']); self::assertSame('Home', $error->get_error_data()['previous_items'][0]['title']);
self::assertStringContainsString('lit-menu-get', $error->get_error_data()['hint']); self::assertStringContainsString('lit/menu-get', $error->get_error_data()['hint']);
self::assertStringContainsString('mcp-adapter-execute-ability', $error->get_error_data()['hint']);
} }
} }
} }