From 7f58301a71e879342de332cfda591c7da42e9f57 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 04:50:51 +0200 Subject: [PATCH] docs: readme with site-wide effects, media guide; test: media/admin end-to-end; version 0.2.0 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- docs/guides/media.md | 61 ++++++++ docs/guides/overview.md | 5 +- .../notes/2026-09-26-etappe-1b-abnahme.md | 61 ++++++++ readme.txt | 97 ++++++++++++ src/Plugin.php | 2 +- tests/Integration/admin-render.php | 46 ++++++ tests/Integration/media-admin-smoke.sh | 145 ++++++++++++++++++ wp-lit-mcp.php | 2 +- 8 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 docs/guides/media.md create mode 100644 docs/superpowers/notes/2026-09-26-etappe-1b-abnahme.md create mode 100644 readme.txt create mode 100644 tests/Integration/admin-render.php create mode 100755 tests/Integration/media-admin-smoke.sh diff --git a/docs/guides/media.md b/docs/guides/media.md new file mode 100644 index 0000000..4a4772d --- /dev/null +++ b/docs/guides/media.md @@ -0,0 +1,61 @@ +# Medien + +Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities, die über +`mcp-adapter-execute-ability` (Name in `ability_name`, Eingaben in `parameters`) aufgerufen werden. Welche +Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (siehe `overview`); +`lit-media-preview` und `lit-media-download` lassen sich dort nur auf „direkt“ oder „deaktiviert“ stellen, +nie auf „indirekt“ – ihr Bild- bzw. Dateiergebnis wird nur bei direktem Aufruf zu einem sichtbaren Inhalt. + +## Suchen und ansehen + +- `lit-media-search`: Text (Titel, Dateiname, Beschreibung), `mime` (z. B. `image` oder `application/pdf`), + Zeitraum (`date_after`/`date_before`), `missing_alt: true` (Bilder ohne Alt-Text; `mime` muss dabei leer + oder ein Bild-Typ sein) und `unused: true` (keine gefundene Verwendung). Geliefert werden nur reguläre + Mediathek-Einträge (kein Papierkorb), die der Nutzer bearbeiten darf. `unused` prüft je Aufruf höchstens 20 + Medien; wird die Prüfung dadurch unterbrochen, folgt eine Warnung und mit `next_cursor` geht es weiter – + „keine Verwendung gefunden“ ist dabei keine Garantie (Details je Medium über `lit/media-get`). +- `lit-media-preview`: verkleinertes Bild ansehen (längste Kante `max_size`, Standard 512 px) – z. B. um + passende Alt-Texte zu schreiben. +- `lit/media-get`: alle Metadaten, Bildgrößen und Verwendungsorte (Beitragsbild, Inhalte inkl. Divi, + Bildfelder, Website-Icon, Logo, Header-/Hintergrundbild, Widgets). `certainty`: „sicher“ = per ID + eingebunden, „möglicherweise“ = nur die Datei-URL gefunden (kann zu einer anderen Kopie der Datei gehören). + Einträge, die der Nutzer nicht lesen darf, erscheinen als „(nicht lesbar)“. `usages_complete: false` heißt, + die Inhaltssuche wurde begrenzt – weitere Verwendungen sind möglich; `usages_truncated: true` heißt, es + gibt mehr Verwendungen als gelistet. + +## Hochladen + +- `lit-media-upload-from-url` (direkt): lädt eine öffentlich erreichbare `http(s)`-Datei; interne oder + lokale Adressen werden abgelehnt. +- `lit/media-upload` (indirekt): lädt eine Datei als Base64. +- Beide: optional `title`, `alt`, `post_id` (Eintrag, dem das Medium zugeordnet wird – braucht + Bearbeitungsrecht); erlaubte Dateitypen jpg, png, gif, webp, avif, pdf, mp4, mp3; Größenlimit siehe die + Tool-Beschreibung (Standard 5 MB, einstellbar unter Einstellungen → LIT MCP → Limits). Beide legen bei + jedem Aufruf ein neues Medium an (nicht idempotent). + +## Ändern, herunterladen, löschen + +- `lit/media-update`: Alt-Text, Titel, Bildunterschrift, Beschreibung für bis zu 50 Medien in einem Aufruf – + alles oder nichts: Ist ein Eintrag ungültig (unbekannte ID, keine Berechtigung, Papierkorb, zu lang, + nichts zu ändern, veraltetes `modified`), wird nichts gespeichert und `failures` nennt jeden fehlerhaften + Eintrag. Optional pro Eintrag `modified` (Wert aus `lit/media-get` oder `lit-media-search`) als Schutz vor + parallelen Änderungen mitsenden – `modified` ändert sich bei Titel, Bildunterschrift und Beschreibung, + nicht bei einer reinen Alt-Text-Änderung. Bei Erfolg meldet `results` jeden Eintrag einzeln (mit neuem + `modified`). +- `lit-media-download`: Datei (Original oder eine Bildgröße aus `sizes` in `lit/media-get`) als eingebettete + Ressource, nur bis zum Upload-Limit – darüber die URL aus `lit/media-get` verwenden. +- `lit/media-delete`: nur nach Rückfrage (`confirm: true`). Wird das Medium verwendet (auch nur + „möglicherweise“), kommt `in_use` mit der Liste der Verwendungen – erst nach ausdrücklicher Zustimmung des + Nutzers mit `force: true` löschen. Medien im Papierkorb werden nicht gelöscht. Ohne aktivierten + Medien-Papierkorb (WordPress-Standard) ist das Löschen endgültig und nicht rückgängig zu machen. Die + Verwendungsprüfung findet NICHT: Term-Meta, Benutzer-Meta, Pods-Tabellen (`pods_*`, `podsrel`), + ACF-Felder (außer als Bildfeld im Schema mit einer ID pro Meta-Eintrag), serialisierte oder + kommagetrennte ID-Listen in Meta-Feldern und Optionen anderer Plugins – im Zweifel den Nutzer fragen, statt + sich auf „keine Verwendung gefunden“ zu verlassen. + +Alt-Texte beschreiben den Bildinhalt knapp und sachlich (keine „Bild von …“-Floskeln). + +## Wenn ein Schreibvorgang fehlschlägt + +Wie bei Inhalten (siehe `overview`): `code` = `write_failed` (alles wurde zurückgenommen) oder +`partial_write` (die Rücknahme war unvollständig, `hint` folgen statt zu wiederholen). diff --git a/docs/guides/overview.md b/docs/guides/overview.md index 8baa106..15df9ad 100644 --- a/docs/guides/overview.md +++ b/docs/guides/overview.md @@ -29,9 +29,10 @@ Hinweisen immer so geschrieben): 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/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`), -Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`). +Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`, +`lit/media-update`, `lit/media-delete`). -Weitere Themen: `lit-guide` mit `topic` = `content-types` oder `workflows`. +Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows` oder `media`. ## Rückmeldungen diff --git a/docs/superpowers/notes/2026-09-26-etappe-1b-abnahme.md b/docs/superpowers/notes/2026-09-26-etappe-1b-abnahme.md new file mode 100644 index 0000000..46d058d --- /dev/null +++ b/docs/superpowers/notes/2026-09-26-etappe-1b-abnahme.md @@ -0,0 +1,61 @@ +# Etappe 1b – Manuelle Abnahme (Browser) + +Vorbereitet von Task 13 (Doku/Version/E2E). Automatisiert geprüft ist bereits: alle Unit-Tests +(`composer test`), Lint (`composer lint`) und die drei Integrationsskripte +(`tests/Integration/mcp-smoke.sh`, `tests/Integration/wp-basis-smoke.sh`, +`tests/Integration/media-admin-smoke.sh`) – Details im Report von Task 13 +(`.superpowers/sdd/2026-09-25-etappe-1b-medien-admin/task-13-report.md`). + +Die folgenden Punkte lassen sich nicht automatisiert oder nur mit einem echten claude.ai-Connector +prüfen; das führt Jan-Niclas selbst im Browser durch. Aktueller Stand: Version 0.2.0, Branch `etappe-1`. + +## 1. Einstellungen → LIT MCP: alle Tabs + +- [ ] Aufrufen: WordPress-Adminbereich → Einstellungen → LIT MCP. +- [ ] Alle fünf Tabs öffnen: Verbindung, Zugriff, Tools, Protokoll, Limits. Jeder Tab lädt ohne + PHP-Fehler/-Warnung (WP_DEBUG ist an) und zeigt sinnvolle Inhalte. +- [ ] Im Tab „Zugriff“: Rolle „Redakteur“ freigeben, speichern; ein Redakteur-Konto kann sich danach + verbinden (Access-Capability wirkt sofort, kein Cache). Freigabe wieder entfernen, speichern. +- [ ] Im Tab „Tools“: ein Tool (z. B. `lit/content-query`, Standard „direkt“) auf „indirekt“ stellen, + speichern – der Hinweistext dort weist bereits darauf hin, dass die Serveranweisungen erst ab + der nächsten Verbindung gelten. Danach wieder auf „Standard“ zurückstellen, speichern. + +## 2. claude.ai-Connector verbinden und Widerruf + +- [ ] Mit claude.ai verbinden (Staging-Domain oder `ddev share`), OAuth-Flow durchlaufen. +- [ ] Die neue Verbindung erscheint unter Einstellungen → LIT MCP → Zugriff („Aktive Verbindungen“) + mit Client-Namen und Zeitpunkt. +- [ ] Dort „Widerrufen“ klicken. Der nächste Tool-Aufruf in claude.ai scheitert (401) und claude.ai + verlangt eine neue Anmeldung, bevor Tools wieder nutzbar sind. + +## 3. Medien-Workflow in claude.ai + +- [ ] In claude.ai anfragen: „Suche Bilder ohne Alt-Text, zeig mir eins und schlag einen Alt-Text + vor.“ Erwartet: `lit-media-search` mit `missing_alt: true` liefert Treffer, `lit-media-preview` + zeigt ein sichtbares Bild in der Unterhaltung (nicht nur eine Textbeschreibung). +- [ ] Optional weiterführen: Alt-Text mit `lit/media-update` speichern lassen und in der WordPress- + Mediathek prüfen, dass er übernommen wurde. + +## 4. Werkzeuge → Website-Zustand + +- [ ] WordPress-Adminbereich → Werkzeuge → Website-Zustand → Reiter „Info“ bzw. die Testübersicht. +- [ ] Der Test „LIT MCP Endpunkt“ ist grün, wenn der Server erreichbar und `/.well-known/` durchgereicht + wird – sonst zeigt er verständlich, was fehlt (z. B. nginx-Sperre für Punkt-Pfade). Bei diesem + DDEV-Setup hat `mcp-smoke.sh` bereits eine WARN dazu ausgegeben (`/.well-known/` von der + nginx-Konfiguration blockiert) – das hier im Site-Health-Test bestätigen und für die + Staging-/Produktions-nginx-Konfiguration eine Ausnahme für `/.well-known/` einplanen. + +## 5. Protokoll-Tab + +- [ ] Einstellungen → LIT MCP → Protokoll: Einträge der eben durchgeführten Tests (Task 13 und die + manuellen Schritte hier) sind sichtbar. +- [ ] Filter nach Tool/Ability (z. B. `lit/media-update`) funktioniert und schränkt die Liste ein. +- [ ] Ein Eintrag mit Revisions-Link öffnet den WordPress-Revisionsvergleich (z. B. nach einer + `lit-content-update`- oder `lit/post-replace-with-copy`-Änderung). +- [ ] „Protokoll leeren“ leert die Liste (Bestätigungsdialog beachten) und zeigt danach die + Erfolgsmeldung „Protokoll geleert.“. + +## Ergebnis + +Kurze Rückmeldung an den Ausführenden (Task 13 / Controller): welche Punkte grün sind und was ggf. +als Folgeaufgabe (Bugfix oder Doku-Korrektur) aufgenommen werden muss. diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..44b6196 --- /dev/null +++ b/readme.txt @@ -0,0 +1,97 @@ +=== WP-LIT-MCP === +Contributors: loosenit +Tags: mcp, claude, divi, pods, ai +Requires at least: 6.9 +Tested up to: 7.0 +Requires PHP: 8.1 +Stable tag: 0.2.0 +License: GPL-2.0-or-later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +MCP-Server für WordPress, Divi 5 und Pods – mit OAuth für claude.ai. + +== Description == + +WP-LIT-MCP stellt einen MCP-Server bereit, über den Claude (claude.ai-Connector) diese Website pflegt: +Inhalte aller Inhaltstypen inkl. Pods-Feldern, Status, Kopien, Revisionen, Menüs, freigegebene +WordPress-Einstellungen und die Mediathek. Jede Änderung läuft mit den Rechten des verbundenen +WordPress-Benutzers und wird protokolliert. + +Einstellungen: Einstellungen → LIT MCP (Verbindung, Zugriff, Tools, Protokoll, Limits). + +== Auswirkungen auf die gesamte Website == + +* Der Standard-MCP-Server von wordpress/mcp-adapter (REST-Route /wp-json/mcp/mcp-adapter-default-server) + wird abgeschaltet (Filter mcp_adapter_create_default_server). Andere Plugins, die diesen ungeschützten + Server erwarten, finden ihn nicht mehr. +* Es gibt genau einen MCP-Endpunkt: /wp-json/mcp/mcp-oauth-server (OAuth 2.1 über die eingebundene + Bibliothek wp-media/mcp-oauth). Anfragen ohne gültiges Token erhalten 401 mit WWW-Authenticate. +* mcp-oauth registriert Rewrite-Regeln für /oauth/… und /.well-known/oauth-… sowie einen + Website-Zustand-Test. Der Webserver muss /.well-known/ an WordPress durchreichen (bei nginx-Regeln, + die Punkt-Pfade sperren, eine Ausnahme für /.well-known/ anlegen). +* Jede Verbindung ist ein Anwendungspasswort des Benutzers (Name = Client, z. B. „Claude“). Es ist im + Benutzerprofil sichtbar und unter Einstellungen → LIT MCP → Zugriff widerrufbar. +* Die Capability lit_mcp_use wird zur Laufzeit an die freigegebenen Rollen vergeben; Rollen in der + Datenbank werden nicht verändert. Nicht freigegebene Benutzer sehen beim Verbinden weder die Werkzeugliste + noch Angaben zur Website – nur den Hinweis, dass ihr Konto nicht freigegeben ist. +* Welche Werkzeuge direkt sichtbar, nur über die Meta-Werkzeuge erreichbar oder abgeschaltet sind, lässt + sich unter Einstellungen → LIT MCP → Tools ändern. +* Eigene Daten: Optionen lit_mcp_allowed_roles, lit_mcp_exposure, lit_mcp_limits, lit_mcp_db_version, + Tabelle {prefix}lit_mcp_log (nur Verweise, keine Inhalte), täglicher Cron-Job lit_mcp_log_cleanup, + Transient lit_mcp_endpoint_check. +* Uploads über LIT MCP: nur Bilder (jpg, png, gif, webp, avif), PDF, MP4, MP3; Größenlimit einstellbar + (Standard 5 MB); Import per URL nur von öffentlich erreichbaren Adressen (interne/lokale Adressen werden + abgelehnt). + +== Rückgängig machen == + +Es gibt keine allgemeine "Rückgängig"-Funktion; was sich wie zurücksetzen lässt, hängt vom Werkzeug ab: + +* Inhalte (Titel, Text, Auszug): über WordPress-Revisionen (Werkzeuge lit/revisions-list, + lit/revision-restore). Felder und Begriffe sind in Revisionen nicht enthalten. +* Original durch eine Kopie ersetzen (lit/post-replace-with-copy): vorher legt das Werkzeug eine Revision + und eine vollständige Sicherung des bisherigen Originals im Papierkorb an (backup_id in der Antwort) – + von dort in WordPress wiederherstellbar. +* Medien löschen (lit/media-delete): ohne aktivierten Medien-Papierkorb (WordPress-Standard, Filter + MEDIA_TRASH) endgültig und nicht rückgängig zu machen; mit aktiviertem Medien-Papierkorb landet das + Medium dort. +* Menüs (lit/menu-set-items): die Antwort enthält previous_items (Stand vorher), mit dem sich der + vorherige Baum nachbauen lässt – neu angelegte Einträge bekommen dabei eine neue ID, CSS-Klassen, + Linkziel und Beschreibung gehen verloren. +* WordPress-Einstellungen (lit/wp-settings-update): die Antwort enthält previous mit den alten Werten. +* Alle genannten Änderungen stehen zusätzlich im Änderungsprotokoll (Einstellungen → LIT MCP → + Protokoll), das bei Inhalten, Medien und Menüs auf den passenden Bearbeiten-/Revisions-/Mediathek-Link + verweist. +* Alles andere (z. B. neu angelegte Entwürfe, hochgeladene Medien, neue Menüs) lässt sich nur über die + üblichen WordPress-Wege rückgängig machen (Papierkorb, manuelles Löschen) – nicht über ein eigenes + Undo-Werkzeug von LIT MCP. + +== Deaktivieren und Löschen == + +* Deaktivieren entfernt den Cron-Job und lässt WordPress die Rewrite-Regeln neu erzeugen; der + MCP-Endpunkt und die OAuth-Adressen sind danach nicht mehr erreichbar. Einstellungen und Protokoll + bleiben erhalten. +* Löschen entfernt zusätzlich alle eigenen Optionen und die Protokolltabelle (auf Multisite je Website). +* Nicht entfernt werden die Daten von mcp-oauth (Option mcp_jwt_secret, Option + wpmedia_mcp_oauth_rewrite_version, Benutzer-Meta mcp_refresh_jti_*, Anwendungspasswörter der + Verbindungen), weil diese Bibliothek auch von anderen Plugins genutzt werden kann. Verbindungen + deshalb vor dem Löschen unter Einstellungen → LIT MCP → Zugriff widerrufen; nicht mehr benötigte + Anwendungspasswörter lassen sich im Benutzerprofil entfernen. + +== Multisite == + +Das Plugin wird je Website aktiviert und konfiguriert; Rollenfreigaben, Tool-Einstellungen und +Protokoll gelten pro Website. Der Standard-MCP-Server von mcp-adapter wird ebenfalls website-weit +abgeschaltet. + +== Changelog == + += 0.2.0 = +* WordPress-Basis: Schema-Erkennung inkl. Pods, Inhalte lesen/anlegen/ändern, Status, Kopie und + Austausch, Revisionen, Menüs, WordPress-Einstellungen, Mediathek (Suche, Vorschau, Metadaten, + Hochladen, Herunterladen, Löschen mit Verwendungsprüfung), Änderungsprotokoll, Einstellungsseite + (Verbindung, Zugriff, Tools, Protokoll, Limits), konfigurierbare Tool-Exposition, Aufräumen bei + Deaktivierung/Deinstallation. + += 0.1.0 = +* Anbindung: OAuth-geschützter MCP-Server, Rollen-Gate, lit-guide, lit-site-status. diff --git a/src/Plugin.php b/src/Plugin.php index 08e6482..99127ff 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -37,7 +37,7 @@ use LIT\MCP\Server\VisibilityFilter; */ final class Plugin { - public const VERSION = '0.1.0'; + public const VERSION = '0.2.0'; private static string $pluginFile = ''; private static ?SettingsPage $settingsPage = null; diff --git a/tests/Integration/admin-render.php b/tests/Integration/admin-render.php new file mode 100644 index 0000000..f5abf3e --- /dev/null +++ b/tests/Integration/admin-render.php @@ -0,0 +1,46 @@ + ok " or " FAIL ". + * + * Usage (inside DDEV): wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/admin-render.php + * + * No strict_types: wp eval-file runs this through eval(). + */ + +defined('WP_CLI') && WP_CLI || exit; + +require_once ABSPATH . 'wp-admin/includes/admin.php'; + +$lit_admins = get_users(['role' => 'administrator', 'number' => 1]); +wp_set_current_user($lit_admins[0]->ID); +set_current_screen('settings_page_lit-mcp'); + +// wp eval-file runs without an HTTP request: WP_List_Table (used by the "Protokoll" tab) +// reads $_SERVER['HTTP_HOST']/['REQUEST_URI'] directly (no isset() guard in WordPress core), +// so a CLI run needs both set to render it, exactly as a real admin request would have them. +$_SERVER['HTTP_HOST'] = (string) wp_parse_url(home_url(), PHP_URL_HOST); + +$lit_page = \LIT\MCP\Plugin::settingsPage(); +if ($lit_page === null) { + echo "FAIL settings page not built\n"; + exit(1); +} + +foreach ($lit_page->tabSlugs() as $lit_slug) { + $_GET['tab'] = $lit_slug; + $_SERVER['REQUEST_URI'] = '/wp-admin/options-general.php?page=lit-mcp&tab=' . rawurlencode($lit_slug); + ob_start(); + try { + $lit_page->render(); + $lit_html = (string) ob_get_clean(); + } catch (\Throwable $e) { + ob_end_clean(); + echo $lit_slug . ' FAIL ' . $e->getMessage() . "\n"; + continue; + } + + $lit_ok = str_contains($lit_html, 'nav-tab-active') && str_contains($lit_html, 'class="wrap"'); + echo $lit_slug . ($lit_ok ? ' ok ' : ' FAIL ') . strlen($lit_html) . "\n"; +} diff --git a/tests/Integration/media-admin-smoke.sh b/tests/Integration/media-admin-smoke.sh new file mode 100755 index 0000000..413b016 --- /dev/null +++ b/tests/Integration/media-admin-smoke.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# End-to-end checks of Etappe 1b: media tools, exposure policy, visibility for users +# without access, settings page rendering, change log. +# Runs INSIDE the DDEV web container: +# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/media-admin-smoke.sh +set -euo pipefail +cd /var/www/html +# shellcheck source=lib.sh +source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh + +ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1) +ORIGINAL_EXPOSURE=$(wp option get lit_mcp_exposure --format=json 2>/dev/null || true) +CREATED_POSTS=() +# 1x1 PNG +PNG='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' + +cleanup() { + for id in "${CREATED_POSTS[@]}"; do + wp post delete "$id" --force >/dev/null 2>&1 || true + done + if [ -n "$ORIGINAL_EXPOSURE" ]; then + wp option update lit_mcp_exposure "$ORIGINAL_EXPOSURE" --format=json >/dev/null 2>&1 || true + else + wp option delete lit_mcp_exposure >/dev/null 2>&1 || true + fi + cleanup_app_passwords_for "$ADMIN_LOGIN" + cleanup_app_passwords_for lit-smoke-sub2 + wp user delete lit-smoke-sub2 --yes >/dev/null 2>&1 || true +} +trap cleanup EXIT + +connect "$ADMIN_LOGIN" + +# 1. Direct media tools; image tool without outputSchema +LIST=$(rpc tools/list '{}') +for t in lit-media-search lit-media-preview lit-media-download lit-media-upload-from-url; do + echo "$LIST" | jq -e --arg t "$t" '.result.tools[] | select(.name == $t)' >/dev/null || fail "tools/list lacks $t" +done +echo "$LIST" | jq -e '.result.tools[] | select(.name == "lit-media-preview") | has("outputSchema") | not' >/dev/null \ + || fail "lit-media-preview must not declare an outputSchema" +pass "direct media tools listed" + +# 2. Base64 upload, search, preview, metadata update, download +UPLOAD=$(ability lit/media-upload "{\"filename\":\"lit-smoke.png\",\"data\":\"$PNG\",\"title\":\"LIT Smoke Bild\"}") +MEDIA_ID=$(echo "$UPLOAD" | jq -r '.attachment.id // empty') +[ -n "$MEDIA_ID" ] || fail "media-upload: $UPLOAD" +CREATED_POSTS+=("$MEDIA_ID") +tool lit-media-search '{"search":"LIT Smoke","missing_alt":true}' \ + | jq -e --argjson id "$MEDIA_ID" '.items[] | select(.id == $id)' >/dev/null || fail "missing_alt search" +PREVIEW=$(rpc tools/call "{\"name\":\"lit-media-preview\",\"arguments\":{\"id\":$MEDIA_ID}}") +echo "$PREVIEW" | jq -e '.result.content[0].type == "image" and (.result.content[0].data | length) > 0' >/dev/null \ + || fail "media-preview: $PREVIEW" +# media-update is all-or-nothing (spec/task 5): a valid single item succeeds and is stored... +UPDATE=$(ability lit/media-update "{\"items\":[{\"id\":$MEDIA_ID,\"alt\":\"Testbild\"}]}") +echo "$UPDATE" | jq -e '.results[0].ok == true' >/dev/null || fail "media-update: $UPDATE" +[ "$(wp post meta get "$MEDIA_ID" _wp_attachment_image_alt)" = "Testbild" ] || fail "alt text not stored" +# ...but a batch with one unknown id refuses the WHOLE batch (nothing partially applied). +BATCH_FAIL=$(ability lit/media-update "{\"items\":[{\"id\":$MEDIA_ID,\"alt\":\"geaendert\"},{\"id\":999999,\"alt\":\"x\"}]}") +[ "$(error_code "$BATCH_FAIL")" = "not_found" ] || fail "media-update batch with unknown id should refuse entirely: $BATCH_FAIL" +[ "$(wp post meta get "$MEDIA_ID" _wp_attachment_image_alt)" = "Testbild" ] \ + || fail "media-update batch refusal must not have changed anything" +ability lit/media-download "{\"id\":$MEDIA_ID}" | jq -e '.type == "resource" and .resource.mimeType == "image/png"' >/dev/null \ + || fail "media-download" +pass "upload, search, preview, update (single ok, mixed batch refused), download" + +# 3. Import protection +SSRF=$(tool lit-media-upload-from-url '{"url":"http://127.0.0.1/wp-login.php"}') +[ "$(error_code "$SSRF")" = "validation_failed" ] || fail "internal URL accepted: $SSRF" +FAKE=$(ability lit/media-upload "{\"filename\":\"evil.png\",\"data\":\"$(printf '/dev/null \ + || fail "usage not found" +NOCONFIRM=$(ability lit/media-delete "{\"id\":$MEDIA_ID}") +[ "$(error_code "$NOCONFIRM")" = "confirm_required" ] || fail "delete without confirm: $NOCONFIRM" +INUSE=$(ability lit/media-delete "{\"id\":$MEDIA_ID,\"confirm\":true}") +[ "$(error_code "$INUSE")" = "in_use" ] || fail "delete of used media without force: $INUSE" +ability lit/media-delete "{\"id\":$MEDIA_ID,\"confirm\":true,\"force\":true}" | jq -e '.ok == true' >/dev/null \ + || fail "forced delete" +pass "media delete: confirm, in_use, force" + +# 5. Exposure policy +wp option update lit_mcp_exposure '{"lit/content-query":"indirect","lit/menu-list":"disabled"}' --format=json >/dev/null +LIST2=$(rpc tools/list '{}') +echo "$LIST2" | jq -e '.result.tools[] | select(.name == "lit-content-query")' >/dev/null \ + && fail "lit-content-query still direct" +ability lit/content-query '{"post_type":"page","limit":1}' | jq -e 'has("items")' >/dev/null \ + || fail "indirect content-query not executable" +ability lit/menu-list '{}' | jq -e 'has("__error")' >/dev/null || fail "disabled ability still executable" +wp option delete lit_mcp_exposure >/dev/null +pass "exposure policy: indirect and disabled" + +# 6. Users without access see neither tools nor site details +wp user create lit-smoke-sub2 lit-smoke-sub2@example.invalid --role=subscriber >/dev/null +ADMIN_TOKEN=$TOKEN +ADMIN_SESSION=$SESSION +connect lit-smoke-sub2 +rpc tools/list '{}' | jq -e '.result.tools | length == 0' >/dev/null || fail "subscriber sees tools" +echo "$INIT_RESPONSE" | grep -q 'nicht freigegeben' || fail "subscriber instructions lack the access hint" +echo "$INIT_RESPONSE" | grep -q 'Inhaltstypen' && fail "subscriber instructions leak site details" +TOKEN=$ADMIN_TOKEN +SESSION=$ADMIN_SESSION +pass "no tools and no site details for users without access" + +# 7. Settings page renders every tab +RENDER=$(wp eval-file "$PLUGIN/tests/Integration/admin-render.php") +for tab in verbindung zugriff tools protokoll limits; do + echo "$RENDER" | grep -q "^$tab ok" || fail "settings tab $tab: $RENDER" +done +pass "settings page tabs render" + +# 8. Change log +# Not `wp db query` here (as in wp-basis-smoke.sh): its write/read detection greps the raw SQL +# text for keywords like UPDATE/DELETE with no word boundary, so it misfires on the *values* +# 'lit/media-update' and 'lit/media-delete' and returns "Rows affected: -1" instead of the +# SELECT result. `wp eval` with a prepared $wpdb query sidesteps that entirely. +ROWS=$(wp eval ' +global $wpdb; +$table = $wpdb->prefix . "lit_mcp_log"; +echo (int) $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$table} WHERE ability IN (%s, %s) AND client = %s" + . " AND created_at > (UTC_TIMESTAMP() - INTERVAL 10 MINUTE)", + "lit/media-upload", + "lit/media-delete", + "lit-mcp-integration-test" +)); +') +[ "$ROWS" -ge 2 ] || fail "media changes not logged (found $ROWS)" +pass "media changes logged" + +echo "ALL CHECKS PASSED" diff --git a/wp-lit-mcp.php b/wp-lit-mcp.php index 7968e5b..55a52d0 100644 --- a/wp-lit-mcp.php +++ b/wp-lit-mcp.php @@ -3,7 +3,7 @@ /** * Plugin Name: WP-LIT-MCP * Description: MCP-Server für WordPress, Divi 5 und Pods – mit OAuth für claude.ai. - * Version: 0.1.0 + * Version: 0.2.0 * Requires at least: 6.9 * Requires PHP: 8.1 * Tested up to: 7.0