feat: content-types tool and WordPress integration

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-25 22:21:29 +02:00
co-authored by Claude Opus 5.5
parent 4a652240a3
commit 98a910f056
7 changed files with 296 additions and 7 deletions
+19 -5
View File
@@ -13,8 +13,12 @@ use LIT\MCP\Integrations\IntegrationRegistry;
*/
final class InstructionsBuilder
{
public function __construct(private IntegrationRegistry $registry)
/** @var (callable(): list<string>)|null */
private $postTypeNames;
public function __construct(private IntegrationRegistry $registry, ?callable $postTypeNames = null)
{
$this->postTypeNames = $postTypeNames;
}
public function build(): string
@@ -24,17 +28,27 @@ final class InstructionsBuilder
$this->registry->available()
));
return implode("\n", [
$lines = [
'LIT MCP pflegt diese WordPress-Website (Inhalte, Medien, Divi 5, Pods).',
'Aktive Integrationen: ' . $active . '.',
];
if ($this->postTypeNames !== null) {
$lines[] = 'Inhaltstypen: ' . implode(', ', ($this->postTypeNames)())
. ' (Felder und Taxonomien: lit-content-types).';
}
return implode("\n", array_merge($lines, [
'Regeln:',
'- Vor dem ersten Arbeitsschritt lit-guide (ohne Topic) aufrufen; Details je Thema über lit-guide '
. 'mit topic.',
'- Änderungen erzeugen native Divi-Module; Variablen und Presets vor festen Werten; Code-Modul nur, '
. 'wenn kein natives Modul passt.',
'- Den Status (Entwurf/veröffentlicht) nie ändern, außer der Nutzer verlangt es ausdrücklich.',
'- Schreib-Tools verlangen den aktuellen rev-Wert aus dem letzten Lesevorgang.',
'- Den Status (Entwurf/veröffentlicht) nie ändern, außer der Nutzer verlangt es ausdrücklich; '
. 'neue Inhalte entstehen als Entwurf.',
'- Beim Bearbeiten immer den rev-Wert aus dem letzten Lesevorgang mitgeben (Pflicht).',
'- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).',
'- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.',
]);
]));
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Content;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
final class ContentTypesAbility implements Ability
{
public function __construct(private ContentSchema $schema)
{
}
public function name(): string
{
return 'lit/content-types';
}
public function label(): string
{
return 'Inhaltstypen';
}
public function description(): string
{
return 'Listet die Inhaltstypen (Post-Typen) dieser Website mit Taxonomien, Anzahl und Divi-Fähigkeit. '
. 'Mit post_type zusätzlich alle Felder (Pods, WordPress-Meta) mit Typ, Pflicht, Auswahlwerten, '
. 'Beziehungsziel und Schreibstatus (writable/readonly/unknown).';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'post_type' => [
'type' => 'string',
'description' => 'Optional: nur diesen Inhaltstyp, dafür mit allen Feldern.',
],
],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return [
'type' => 'object',
'properties' => [
'post_types' => ['type' => 'array', 'items' => ['type' => 'object']],
],
'required' => ['post_types'],
];
}
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
{
$postType = isset($input['post_type']) && is_string($input['post_type']) ? $input['post_type'] : '';
if ($postType === '') {
return ['post_types' => $this->schema->describePostTypes()];
}
$description = $this->schema->describe($postType, true);
if ($description === null) {
return Errors::notFound(
sprintf('Inhaltstyp "%s" ist nicht verfügbar.', $postType),
'Verfügbare Inhaltstypen: ' . implode(', ', $this->schema->postTypeNames()) . '.'
);
}
return ['post_types' => [$description]];
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\WordPress\Content\ContentTypesAbility;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
/**
* WordPress basics (spec §6): content, status, copies, revisions, menus, settings.
* Always available.
*/
final class WordPressIntegration implements Integration
{
public function __construct(private ContentSchema $schema, private ChangeLog $log)
{
}
public function id(): string
{
return 'wordpress';
}
public function isAvailable(): bool
{
return true;
}
public function abilities(): array
{
return [
new ContentTypesAbility($this->schema),
];
}
}
+22 -2
View File
@@ -11,6 +11,11 @@ use LIT\MCP\ChangeLog\ChangeLogInstaller;
use LIT\MCP\Guides\InstructionsBuilder;
use LIT\MCP\Integrations\Core\CoreIntegration;
use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Integrations\Pods\PodsFieldSource;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
use LIT\MCP\Integrations\WordPress\Schema\SampledMetaSource;
use LIT\MCP\Integrations\WordPress\WordPressIntegration;
use LIT\MCP\Server\RequestContext;
use LIT\MCP\Server\ServerRegistrar;
@@ -40,12 +45,17 @@ final class Plugin
(new ChangeLogInstaller($changeLog))->maybeInstall();
(new ChangeLogCleanup($changeLog, new Limits()))->register();
$schema = self::contentSchema();
$registry = new IntegrationRegistry(
new CoreIntegration(self::pluginDir() . '/docs/guides')
new CoreIntegration(self::pluginDir() . '/docs/guides'),
new WordPressIntegration($schema, $changeLog)
);
(new AbilityRegistrar($registry))->register();
(new ServerRegistrar($registry, new InstructionsBuilder($registry)))->register();
(new ServerRegistrar(
$registry,
new InstructionsBuilder($registry, static fn (): array => $schema->postTypeNames())
))->register();
\WPMedia\MCP\OAuth\Bootstrap::instance();
}
@@ -71,4 +81,14 @@ final class Plugin
);
});
}
private static function contentSchema(): ContentSchema
{
$sources = [new RegisteredMetaSource(), new SampledMetaSource()];
if (PodsFieldSource::isAvailable()) {
array_unshift($sources, new PodsFieldSource());
}
return new ContentSchema($sources);
}
}