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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 04:51:11 +02:00
co-authored by Claude Opus 5.5
parent 251934ce53
commit 7f58301a71
8 changed files with 415 additions and 4 deletions
+61
View File
@@ -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).
+3 -2
View File
@@ -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
@@ -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.
+97
View File
@@ -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.
+1 -1
View File
@@ -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;
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* Renders every tab of "Einstellungen → LIT MCP" server-side as an administrator and
* prints one line per tab: "<slug> ok <bytes>" or "<slug> FAIL <reason>".
*
* 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";
}
+145
View File
@@ -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 '<?php echo 1;' | base64 -w0)\"}")
[ "$(error_code "$FAKE")" = "validation_failed" ] || fail "disguised PHP accepted: $FAKE"
EXTERNAL=$(tool lit-media-upload-from-url '{"url":"https://s.w.org/images/wmark.png","title":"LIT Smoke Extern"}')
EXTERNAL_ID=$(echo "$EXTERNAL" | jq -r '.attachment.id // empty')
if [ -n "$EXTERNAL_ID" ]; then
CREATED_POSTS+=("$EXTERNAL_ID")
pass "import from public URL"
else
echo "WARN - import from public URL failed (no internet in container?): $EXTERNAL"
fi
pass "internal URLs and disguised files rejected"
# 4. Usage check before delete
PAGE=$(tool lit-content-create "{\"post_type\":\"page\",\"title\":\"LIT Smoke Medien\",\"featured_image\":$MEDIA_ID}")
PAGE_ID=$(echo "$PAGE" | jq -r '.changed[0].id // empty')
[ -n "$PAGE_ID" ] || fail "page with featured image: $PAGE"
CREATED_POSTS+=("$PAGE_ID")
ability lit/media-get "{\"id\":$MEDIA_ID}" | jq -e '.usages[] | select(.where == "featured")' >/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"
+1 -1
View File
@@ -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