feat: Divi module catalog, attribute paths, short form and lit/divi-catalog

Adds per-path attribute kind (text/richtext/code/url/image/color/enum/
onoff/number/list/icon), derived from Divi's field component metadata
where an attribute path names one unambiguously (module.paths() itself
is unchanged; kinds() is a new, separate lookup exposed as path_kinds
on lit/divi-catalog's module detail). This is not in the task-4 brief;
it is added because 2b's sanitization routing (ruling R3) needs a
metadata-derived field kind per attribute path. Verified live against
the full 100-module catalog (564/4358 paths resolve a kind; the rest
default to "unknown" and fall back to 2b's key-name routing).

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 11:48:27 +02:00
co-authored by Claude Opus 5.5
parent 08d70abfc7
commit 2f4dcf9ce4
14 changed files with 5227 additions and 2 deletions
@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Schema;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\JsonMap;
use WP_Error;
/**
* lit/divi-catalog: the modules of this Divi installation with their short-form fields;
* with "module" the details of one module incl. all known attribute paths (spec §5.7).
*/
final class DiviCatalogAbility implements Ability
{
public const CODE_NOTE = 'Code-Modul (code) nur verwenden, wenn kein natives Modul passt – es ist im '
. 'Divi-Builder nicht visuell bearbeitbar.';
public function __construct(private DiviModules $modules)
{
}
public function name(): string
{
return 'lit/divi-catalog';
}
public function label(): string
{
return 'Divi-Modulkatalog';
}
public function description(): string
{
return 'Listet die Divi-Module dieser Website (Name, Titel, Art, erlaubte Kind-Module) und welche eine '
. 'Kurzform haben. Mit module: Kurzform-Felder und alle gültigen Attributpfade des Moduls für die '
. 'Vollform, je Pfad nach Möglichkeit mit Feldart (path_kinds).';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'module' => ['type' => 'string', 'description' => 'Modul, z. B. "button" oder "divi/button".'],
],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return [
'type' => 'object',
'properties' => [
'modules' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'title' => ['type' => 'string'],
'category' => ['type' => 'string'],
'children' => ['type' => 'array', 'items' => ['type' => 'string']],
'shortform' => ['type' => 'boolean'],
],
'required' => ['name', 'title', 'category', 'children', 'shortform'],
],
],
'module' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'title' => ['type' => 'string'],
'category' => ['type' => 'string'],
'children' => ['type' => 'array', 'items' => ['type' => 'string']],
'item_type' => ['type' => ['string', 'null']],
'shortform' => ['type' => 'object'],
'paths' => ['type' => 'array', 'items' => ['type' => 'string']],
'path_kinds' => [
'type' => 'object',
'description' => 'Bekannter Attributpfad => Feldart (text, richtext, code, url, '
. 'image, color, enum, onoff, number, list, icon), soweit aus den Divi-Metadaten '
. 'ableitbar; fehlende Pfade gelten als unbekannt.',
],
],
'required' => ['name', 'title', 'category', 'children', 'shortform', 'paths', 'path_kinds'],
],
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
],
'required' => ['notes'],
];
}
public function annotations(): array
{
return ['readonly' => true, 'destructive' => false, 'idempotent' => true];
}
public function isDirect(): bool
{
return true;
}
public function permission(array $input): bool|WP_Error
{
return current_user_can('edit_posts')
? true
: Errors::forbidden('Keine Berechtigung, Inhalte zu bearbeiten.');
}
public function execute(array $input): mixed
{
$catalog = $this->modules->catalog();
$notes = [self::CODE_NOTE, 'Details zur Vollform: lit-guide mit topic "divi-attrs".'];
if (! is_string($input['module'] ?? null) || $input['module'] === '') {
$modules = [];
foreach ($catalog->summary() as $module) {
$modules[] = $module + ['shortform' => ShortformMap::has(substr($module['name'], strlen('divi/')))];
}
return ['modules' => $modules, 'notes' => $notes];
}
$name = ModuleCatalog::normalize($input['module']);
if (! $catalog->has($name)) {
return Errors::notFound(
sprintf('Modul "%s" gibt es auf dieser Website nicht.', $input['module']),
'Verfügbare Module mit lit-divi-catalog ohne module abrufen.'
);
}
$short = substr($name, strlen('divi/'));
$item = ShortformMap::childType($short);
return [
'module' => [
'name' => $name,
'title' => $catalog->title($name),
'category' => $catalog->category($name),
'children' => $catalog->childrenOf($name),
'item_type' => $item,
'shortform' => JsonMap::of(ShortformMap::fields($short)),
'paths' => $catalog->paths($name),
'path_kinds' => JsonMap::of($catalog->kinds($name)),
],
'notes' => $notes,
];
}
}