netzhirsch / contao-mcp-bundle
Connects Claude and other AI agents to Contao 5 via the Model Context Protocol — 175 MCP tools for pages, articles, news, files, templates, members, forms, newsletters and more, secured with OAuth 2.1 (PKCE + Dynamic Client Registration). Commercial extension: 30-day trial, then a subscription per C
Package info
github.com/Netzhirsch/contao-mcp-bundle
Type:contao-bundle
pkg:composer/netzhirsch/contao-mcp-bundle
Requires
- php: ^8.1
- ext-sodium: *
- contao/core-bundle: ^5.3
- league/oauth2-server: ^8.5
- nyholm/psr7: ^1.8
- php-mcp/server: ^3.3
- symfony/config: ^6.4 || ^7.0
- symfony/console: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/framework-bundle: ^6.4 || ^7.0
- symfony/http-foundation: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- symfony/psr-http-message-bridge: ^2.3 || ^7.0
- symfony/routing: ^6.4 || ^7.0
- symfony/yaml: ^6.4 || ^7.0
Requires (Dev)
- contao/calendar-bundle: ^5.3
- contao/comments-bundle: ^5.3
- contao/faq-bundle: ^5.3
- contao/manager-plugin: ^2.0
- contao/news-bundle: ^5.3
- contao/newsletter-bundle: ^5.3
- friendsofsymfony/http-cache-bundle: ^3.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
- terminal42/contao-leads: ^3.3
- terminal42/contao-url-rewrite: ^1.8
Suggests
- contao/calendar-bundle: Required for calendar_* and calendar_event_* tools.
- contao/comments-bundle: Required for comment_* tools.
- contao/faq-bundle: Required for faq_* tools.
- contao/news-bundle: Required for news_* tools.
- contao/newsletter-bundle: Required for newsletter_* tools.
- terminal42/contao-leads: Required for leads_list + lead_get (read-only access to form submissions).
- terminal42/contao-url-rewrite: Required for url_rewrite_* tools.
README
🇬🇧 English version — diese deutsche Fassung ist die Referenz.
Status: Stable — v1.7.0
Lizenz: proprietär, kommerziell lizenziert — 30 Tage kostenlos testen,
danach 49 €/Monat je Contao-Instanz (siehe Lizenz & Testphase
und LICENSE)
Ein Model Context Protocol-Server als Contao-5-Bundle. Verbindet Claude Desktop, Claude in der API, Claude Code, MCP Inspector oder jede andere MCP-fähige KI direkt mit dem Contao-Backend — ohne eigene REST-Endpunkte, ohne Middleware, ohne Port.
Statt jeder KI-Aufgabe einen eigenen API-Endpunkt nachzuziehen, bekommt die KI-Session strukturierten Zugriff auf den gesamten DCA-Stack: Redakteure können per natürlichsprachlichem Auftrag Inhalte anlegen, Pipelines können Seiten vollautomatisch aus Drittsystemen befüllen, Entwickler können Strukturmigrationen skripten — alles über dieselben 175 Tools, abgesichert mit denselben Backend-Benutzerrechten wie beim manuellen Bearbeiten.
Unterstützte Entitäten: News, Seiten, Artikel, Kalender, FAQ, Mitglieder, Formulare, Newsletter, Kommentare, Themes, Layouts, Module, Bildgrößen, Templates, Dateien, URL-Rewrites, Formular-Leads, Wartung + System-Einstellungen.
Was drin ist
- 175 Tools über Contao-Kernentitäten + populäre Extensions.
- Lazy-Mode-Discovery: drei Meta-Tools (
contao_search_tools,contao_describe_tool,contao_call) verstecken die übrigen vortools/list— spart bei Claude Desktop ~12 KB System-Prompt-Overhead pro Turn. - OAuth 2.1 mit PKCE + Dynamic Client Registration (RFC 7591) inkl.
Initial-Access-Token-Gate für
restrictedmode. - Volltextsuche über die Website:
search_querydurchsucht Contaos Suchindex (tl_search) — findet also auch Text, der aus Modulen, Includes oder Erweiterungen stammt und über die CRUD-Tools nicht auffindbar wäre. Geschützte Seiten bleiben außen vor;search_index_statuszeigt, ob der Index überhaupt befüllt ist. - Filesystem-Suche:
files_search(rekursive Glob-Suche im Upload-Tree, POSIX-Syntax +**-Erweiterung, basename-Match bei Patterns ohne Slash) - Site-Building-Helfer:
entity_move,page_cache_invalidate,system_settings_update,insert_tags_list,page_preview,maintenance_run,dbafs_sync(Reconciletl_files↔ Disk). - Optionale Extension-Tools: erscheinen automatisch, sobald das jeweilige
Bundle installiert ist (sonst sauberer
extension_not_available-Fehler): Newsletter, Kommentare,url_rewrite_*(terminal42) und lesendleads_list+lead_getfür Formular-Einsendungen (terminal42/contao-leads). - Author-Pass-Through: Writes laufen unter dem echten OAuth-User in
tl_log+tl_version. - Löschungen sind rückholbar: Was die KI löscht, landet inklusive
Kind-Datensätzen in
tl_undo— wiederherstellbar über Contaos normales „Rückgängig" im Backend. Wiederherstellen bleibt bewusst Handarbeit: Die KI kann löschen, aber nichts stillschweigend zurückholen. - Löschungen, die etwas kaputt machen würden, werden blockiert:
usage_findbeantwortet „wo wird das benutzt?" für Seiten, Dateien, Bilder, Artikel, Module, Formulare, Templates, Bildgrößen und alles Weitere — und derselbe Check läuft automatisch vor jedem*_delete. Gefunden wird an vier Stellen: DB-Felder (aus der DCA abgeleitet, also inkl. Extension-Feldern), Insert-Tags in beliebigen Textspalten ({{link::42}}, auch per Alias,{{file::…}},{{insert_module::…}}), in Dateien selbst —@import/url()in SCSS/CSS, hartcodierte Pfade in Templates — und bei Templates jedecustomTpl/…Tpl-Spalte, die darauf zeigt, plus{% extends %}/$this->extend()aus anderen Templates. Damit fallen auch die Fälle auf, die keine Datenbankabfrage sieht:_colors.scsswird als@import 'colors'eingebunden, und ein gelöschtesce_text_customändert stillschweigend, wie ein Content-Element rendert. Blockiert wird nur, was beweisbar und schädlich ist; Backend-Rechte-Mounts und bloße Namensnennungen werden berichtet, halten aber nichts auf. Überschreiben mitignore_references=true(landet intl_log). - Umbenennen und Verschieben werden mitgeprüft — aber nur, wo es wirklich
bricht:
file_rename,file_moveundtemplate_renamelaufen durch denselben Check. Contao behält beim Umbenennen Zeile, ID und UUID und schreibt nurtl_files.pathneu — also überlebensingleSRC = <uuid>und{{file::<uuid>}}das problemlos, während{{file::files/x.svg}}, ein SCSS-@importund ein hartcodierter Template-Pfad brechen. Blockiert wird deshalb nur, was an diesem Pfad bzw. Namen hängt; alles UUID-/ID-basierte wird gezeigt, hält aber nichts auf. Ein.html5-Template in einen anderen Ordner zu verschieben ist folgerichtig gar nicht blockiert: Contao findet es über den Basisnamen, der sich dabei nicht ändert. - Backend-Modul „MCP-Server" mit vier Bereichen: Status (Lizenz + Testphase/ Abo starten, OAuth-Clients, IATs), Konfiguration, Aktivitätslog, Tool-Panel (jedes Tool einzeln abschaltbar) — nur für Contao-Administratoren.
- Linux + Windows getestet (Laragon dev, Debian production).
Installation
1. Composer
composer require netzhirsch/contao-mcp-bundle
Mehr ist nicht nötig — kein repositories-Eintrag, kein Patch-Block, kein
allow-plugins. Das Bundle liegt auf
Packagist.
Alternativ im Contao Manager nach „Contao MCP Bundle" suchen und installieren.
2. Bundle registrieren
Auto-Discovery über das Contao Manager Plugin — kein manuelles Eintragen in
config/bundles.php nötig.
3. Schema-Migrationen + erste Konfig
vendor/bin/contao-console contao:migrate --env=prod
Legt die OAuth-Tabellen an (tl_mcp_oauth_*) und ergänzt die
External-ID-Spalten auf 24 Entity-Tabellen. Standardkonfig läuft
unauthentifiziert — für Production unbedingt auth_mode=oauth einschalten
(siehe Backend-Modul oder var/mcp/config.json).
Der MCP-Endpoint ist nach der Migration sofort live unter
https://<backend_url>/mcp — Apache/PHP-FPM serviert ihn wie jede andere
Symfony-Route. Kein Daemon, kein Port, kein Reverse-Proxy nötig.
4. Lizenz aktivieren (30 Tage kostenlos)
Ohne aktive Lizenz antworten alle Tools mit license_inactive — Contao selbst
läuft normal weiter. Im Backend unter MCP-Server → Status oben auf
„Testphase starten" klicken: 30 Tage, ohne Zahlungsdaten. Details siehe
Lizenz & Testphase.
5. In Claude Desktop / Cowork einbinden
Anleitungen im Repo: docs/installation.md (Client anbinden, online + lokal) und docs/dokumentation.md (vollständige Funktionsreferenz). Im Backend selbst gibt es keinen Doku-Tab mehr.
Client verbinden bei
oauth_registration_mode: restricted(Default): Claude,mcp-remote& Co. können bei der Registrierung keinen Initial Access Token mitschicken — der IAT-Button verbindet sie also nicht, der ist für Skripte. Der Weg ist MCP-Server → Status → „Registrierung für 15 Minuten öffnen". Das Fenster bleibt die vollen 15 Minuten offen, egal wie viele Versuche das kostet (bis 1.4.0 schloss es nach der ersten erfolgreichen Registrierung — daher scheiterten Retrys und ein zweiter Client). Abgewiesene Versuche stehen mit Grund und IP unter MCP-Server → Aktivität.
Schritt-für-Schritt-Anleitung für die lokale Connector-Einrichtung
(mcp-remote-Bridge, claude_desktop_config.json, OAuth, Schema-Cache +
Stolperfallen): docs/mcp-client-lokal-einrichten.md.
Kurzfassung der genauen Config-Werte auch im Backend-Doku-Tab des MCP-Server-Moduls.
Lizenz & Testphase
Das Bundle ist kommerziell lizenziert. Der Tool-Layer ist lizenzgeschützt:
ohne gültige Lizenz liefert jeder tools/call einen license_inactive-Fehler
(Ausnahme: ping). Contao selbst ist nie betroffen — Frontend, Backend und
alle anderen Erweiterungen laufen unverändert weiter.
| Testphase | 30 Tage, ohne Zahlungsdaten, eine je Domain/Konto |
| Preis | 49 €/Monat oder 539 €/Jahr (12 für 11), netto zzgl. MwSt. |
| Einheit | pro Contao-Instanz — unabhängig davon, wie viele Front-End-Domains sie bedient |
| Zahlung | Karte oder SEPA-Lastschrift, ausschließlich auf Stripe-gehosteten Seiten |
| Staging/Dev | kostenlos (lokale Hosts sowie Subdomains einer bezahlten Domain) |
Bestellen im Backend — alles unter MCP-Server → Status, Buttonleiste oben:
- „Testphase starten" → schaltet die Tools für 30 Tage frei.
- „Abonnieren" → öffnet die Stripe-Bezahlseite. Karten-/SEPA-Daten werden nur bei Stripe eingegeben, nie in Contao gespeichert.
- „Abo verwalten" → Stripe-Kundenportal (Zahlungsmittel, Rechnungen, Kündigung).
Alternativ per CLI:
vendor/bin/contao-console contao:mcp:license status # aktueller Zustand vendor/bin/contao-console contao:mcp:license trial <email> # Testphase starten vendor/bin/contao-console contao:mcp:license activate <token> # Token einspielen
Verlängerung läuft automatisch. Der Cron LicenseRenewalCron (stündlich,
gedrosselt) erneuert das Token; die Prüfung selbst ist offline (Ed25519).
Ein Ausfall des Lizenzservers sperrt daher niemanden aus — zusätzlich gelten
3 Tage Kulanz nach Ablauf. Voraussetzung ist ein laufender Contao-Cron.
Verbindung zum Lizenzserver:
https://license.netzhirsch.de, fest im Bundle hinterlegt — nichts zu konfigurieren. Übertragen werden nur Domain, Produkt und die E-Mail des bestellenden Backend-Users.
Anforderungen
- PHP
^8.1mit Extensions:openssl,sodium,pdo_mysql,mbstring,intl(sodiumist für die Lizenzprüfung zwingend — fehlt es, bleiben alle Tools gesperrt; der Code ist 8.1-sauber, die 8.1-Untergrenze deckt Contao-5.3-Installationen ab) - Contao ≥ 5.3 (Smoke-Test läuft gegen 5.3 und 5.7)
- Symfony ≥ 6.4 oder 7.x
- MySQL ≥ 8.0 oder MariaDB ≥ 10.6 (strict mode unterstützt)
Smoke-Test
vendor/bin/contao-console contao:mcp:smoke-test --env=dev
Geht ~200 Asserts gegen den Tool-Layer durch (CRUD auf Member/Group/Form/ Newsletter/Comments/Theme/Layout/Templates/Maintenance + External-ID + Audit-Regressions + Key-Rotation + Rate-Limit + MCP-Activity-Log), erstellt eigene Testdaten, räumt am Ende wieder auf. Soll grün durchlaufen.
Zusätzlich gibt es eine isolierte PHPUnit-Suite (vendor/bin/phpunit)
für OAuth-Crypto-Edge-Cases (dual-key Rotation, IAT single-use,
HMAC-Pepper) die der Smoke-Test als End-to-End nicht erreicht.
Lokale Entwicklung & HTTPS
Das Bundle terminiert kein TLS — HTTPS liefert der Webserver davor
(lokal Laragon, produktiv z.B. Plesk/Let's-Encrypt). Die extern beworbenen
OAuth-Endpunkte baut das Bundle aus dem konfigurierten backend_url, nicht
aus dem Request-Schema — dadurch ist es reverse-proxy-robust.
Für lokale MCP-Tests reicht meist backend_url: "http://localhost"
(Loopback ist von der Redirect-URI-Whitelist und der HTTPS-Warnung
ausgenommen) — kein Zertifikat nötig. Echtes lokales HTTPS
(https://<host>.test) inkl. der Node-/CA-Stolperfalle bei MCP-Clients:
siehe docs/lokales-https.md.
Health-Check vor Production-Deploy
// MCP-Call {"tool": "system_health_check"}
Returnt eine strukturierte Liste über PHP-Setup, var/mcp/-Permissions,
OAuth-Konfig + warnings: [...] mit konkreten Fix-Befehlen. Vor jedem
Site-Move oder Server-Wechsel laufen lassen.
Konfiguration
Datei: var/mcp/config.json (wird beim ersten Backend-Aufruf des Moduls
angelegt).
Die vier MCP-Server-Backendmodule sind Administratoren vorbehalten — sie schalten
auth_mode(und damit die komplette Rechteprüfung), vergeben OAuth-Registrierungen, widerrufen Clients und schließen kostenpflichtige Abos ab. Ein Nicht-Admin bekommt auch mit gesetztem Modulrecht „Zugriff verweigert".
Felder:
| Key | Default | Bedeutung |
|---|---|---|
path |
mcp |
URL-Pfad (ohne führenden Slash) |
pagination_limit |
500 |
Max Tools pro tools/list (irrelevant in Lazy-Mode) |
auth_mode |
none |
none oder oauth |
backend_url |
"" |
Public Base-URL des Contao-Backends (Pflicht bei OAuth) |
oauth_registration_mode |
restricted |
restricted (Registrierung nur im Pairing-Fenster) oder open |
lazy_mode |
false |
Wenn true: nur 6 Discovery-Tools in tools/list |
Bundle-eigene Konfig in config/packages/netzhirsch_contao_mcp.yaml:
netzhirsch_contao_mcp: write: default_author_id: 1 # Fallback wenn auth_mode=none preview: # Nur nötig, wenn die Instanz hinter HTTP-Basic-Auth liegt. # Default ist die Env-Variable; ohne sie bleibt alles wie bisher. basic_auth: '%env(default::MCP_PREVIEW_BASIC_AUTH)%'
page_preview holt die Seite über ihre öffentliche URL — steht davor ein
Basic-Auth-Schutz (typisch auf Staging), antwortet der Webserver mit 401, bevor
Contao überhaupt läuft. Dann in der .env.local der Instanz:
MCP_PREVIEW_BASIC_AUTH="user:pass"
Das Tool weist bei 401/403 selbst darauf hin. Die Zugangsdaten stehen nur in der
.env.local, nie in der Antwort oder im Log.
Bekannte Einschränkungen
Stand v1.7.0:
- PHPUnit-Coverage deckt OAuth-Crypto, die Permission-Map und den Usage-Scanner ab. Der Tool-Layer wird stattdessen end-to-end vom Smoke-Test exerziert.
- Encryption-Key-Rotation ist NICHT implementiert. Der
var/mcp/oauth/encryption.keyschützt Refresh-Token-Payloads at rest — Rotation würde alle Refresh-Tokens invalidieren. (Die RSA-Signing-Keys lassen sich dagegen rotieren, siehecontao:mcp:oauth:rotate-keysunter „Wartung".) - Lizenz-Domainbindung wertet die konfigurierte
backend_urlaus. Das ist eine kaufmännische, keine kryptografische Grenze — sie hält ehrliche Installationen sauber getrennt, ist aber vom Betreiber der Instanz beeinflussbar.
Voller Audit-Stand: CHANGELOG.md.
Backup-Strategie
Das Bundle persistiert vier separate Daten-Surfaces. Ein vollständiger Restore braucht alle vier — sonst bleiben entweder OAuth-Tokens ungültig (Keys weg) oder Tool-Calls können keine externen Referenzen zuordnen (External-IDs weg).
| Surface | Pfad | Restore-Verhalten |
|---|---|---|
| OAuth-RSA-Keys + Encryption-Key | var/mcp/oauth/*.pem, var/mcp/oauth/encryption.key |
Pflicht. Fehlt → alle Refresh-Tokens ungültig, alle Access-Tokens müssen neu ausgestellt werden. Mode 0600 zwingend. |
| Bundle-Config | var/mcp/config.json |
Optional. Fehlt → Defaults greifen, Operator muss auth_mode=oauth manuell aktivieren. |
| OAuth-Tabellen | tl_mcp_oauth_client, tl_mcp_oauth_access_token, tl_mcp_oauth_refresh_token, tl_mcp_oauth_auth_code, tl_mcp_oauth_iat |
Pflicht für nahtlose Migration. Fehlt → Clients müssen sich neu registrieren (DCR). |
| External-ID-Spalten | external_id_namespace + external_id_key auf 24 Entity-Tabellen |
Pflicht für Skill-2-Integrationen. Fehlt → Updates müssen via Contao-PK statt externer Referenz erfolgen, schmerzhafte Doppel-Pflege. |
Empfehlung: tar über var/mcp/ + mysqldump auf die fünf
tl_mcp_oauth_*-Tabellen + ein DB-Dump des kompletten Contao-Schemas
(External-ID-Spalten leben auf Entity-Tabellen, kein eigener Backup-
Container möglich).
Entwicklung: Prüfkette vor einem Release
composer verify
Bündelt PHPStan + PHPUnit — genau das, was die CI fährt. Einmal pro Klon
composer setup-hooks ausführen: Danach lehnt ein pre-push-Hook einen Push ab,
der die CI rot machen würde (git push --no-verify umgeht ihn im Notfall).
Der Smoke-Test braucht ein laufendes Contao samt Datenbank und ist deshalb nicht Teil davon — er gehört vor jeden Release-Tag:
vendor/bin/contao-console contao:mcp:smoke-test --env=dev
Reihenfolge für einen Release: composer verify → Smoke-Test → committen →
pushen → CI grün abwarten → erst dann taggen.
Sicherheitslücken melden
Bitte nicht über ein öffentliches Issue, sondern über die Security-Policy (GitHub Security Advisory oder kalus@netzhirsch.de).
Bug-Reports
Issues / Findings bitte ins Repo, plus Anhang:
- Output von
system_health_check - Backend-User-Rolle + Contao-Version
- Relevante Einträge aus
var/log/prod.log(Symfony-Standard-Log)
Update von einer Version ≤ 1.4.0
Nichts zu tun — composer update netzhirsch/contao-mcp-bundle läuft durch,
auch wenn in der Root-composer.json noch der frühere Patch-Block steht. Die
patches/-Dateien liegen dafür bis 2.0.0 weiter im Paket; angewendet werden sie
von nichts mehr.
Wer aufräumen will (empfohlen, aber nicht dringend): extra.patches,
cweagans/composer-patches aus require und den allow-plugins-Eintrag aus der
Root-composer.json löschen, dann composer update. Der Vendor bleibt danach
gepatcht — das Plugin installiert php-mcp/server bei geschrumpfter Patch-Liste
nicht von sich aus neu. Folgenlos, weil ContaoDispatcher die betroffenen
Methoden überschreibt; wer es sauber will, hängt ein
composer reinstall php-mcp/server an. Details:
patches/README.md.
Wartung
Composer-Updates des Bundles:
composer update netzhirsch/contao-mcp-bundle
Es werden keine Vendor-Patches mehr angewendet: was das Bundle am
Dispatcher braucht (Lazy-Mode-Filter, Post-Call-Cleanup), liegt in
Server\ContaoDispatcher als Subklasse. Bei einem php-mcp/server-Major-Bump
dort prüfen, ob handleToolList()/handleToolCall() noch passen.
Console-Kommandos
| Kommando | Zweck | Empfohlener Rhythmus |
|---|---|---|
contao:mcp:license status|trial|activate|renew |
Lizenz/Testphase verwalten | bei Bedarf (Verlängerung läuft per Cron automatisch) |
contao:mcp:oauth:cleanup |
abgelaufene Auth-Codes, Tokens, IATs purgen | täglich als Cron |
contao:mcp:oauth:rotate-keys |
OAuth-RSA-Signing-Keys rotieren (Dual-Key, ohne Ausloggen) | monatlich |
contao:mcp:permission-debug |
nachvollziehen, warum ein Backend-User ein Tool (nicht) darf | zur Fehlersuche |
contao:mcp:smoke-test |
End-to-End-Selbsttest des Tool-Layers | nach Updates/Serverumzug |
Der Contao-Cron muss laufen (contao:cron bzw. der Web-Cron) — daran hängt
auch die automatische Lizenzverlängerung.
Maintainer: Jan-Philipp Kalus <kalus@netzhirsch.de> — Netzhirsch