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:
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Executable
+145
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user