feat: content-query and content-get tools

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:39:43 +02:00
co-authored by Claude Opus 5.5
parent 95aae74466
commit d68e9b1f04
10 changed files with 806 additions and 4 deletions
@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Content;
use LIT\MCP\Abilities\JsonMap;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Post;
/**
* Output format of an entry for the content tools. Not final so abilities can be
* unit-tested with a mock.
*/
class PostPresenter
{
public const DIVI_HINT = 'Dieser Inhalt ist mit Divi gebaut. Die Divi-Struktur wird über lit-divi-outline '
. 'gelesen und mit den lit-divi-*-Tools bearbeitet; fehlen diese Tools, kann der Divi-Inhalt hier nicht '
. 'bearbeitet werden. content wird deshalb nicht ausgegeben; Titel, Auszug, Felder und Begriffe sind '
. 'über lit-content-update änderbar.';
public function __construct(private ContentSchema $schema)
{
}
/** @return array<string, mixed> */
public function summary(WP_Post $post): array
{
return [
'id' => (int) $post->ID,
'type' => (string) $post->post_type,
'status' => (string) $post->post_status,
'title' => (string) $post->post_title,
'slug' => (string) $post->post_name,
'parent' => (int) $post->post_parent,
'date' => (string) $post->post_date,
'modified' => (string) $post->post_modified,
'url' => (string) get_permalink($post),
'edit_url' => (string) get_edit_post_link($post->ID, 'raw'),
'divi' => self::usesDivi($post),
];
}
/** @return array<string, mixed> */
public function full(WP_Post $post): array
{
$divi = self::usesDivi($post);
return array_merge($this->summary($post), [
'excerpt' => (string) $post->post_excerpt,
'content' => $divi ? null : (string) $post->post_content,
'menu_order' => (int) $post->menu_order,
'author' => [
'id' => (int) $post->post_author,
'name' => (string) get_the_author_meta('display_name', (int) $post->post_author),
],
'template' => (string) get_page_template_slug($post),
'preview_url' => (string) get_preview_post_link($post),
'terms' => JsonMap::of($this->terms($post)),
'fields' => JsonMap::of($this->fields($post)),
'featured_image' => $this->featuredImage($post),
'rev' => Rev::of((string) $post->post_content),
'hints' => $divi ? [self::DIVI_HINT] : [],
]);
}
public static function usesDivi(WP_Post $post): bool
{
return get_post_meta((int) $post->ID, '_et_pb_use_builder', true) === 'on'
|| str_contains((string) $post->post_content, '<!-- wp:divi/');
}
/** @return array<string, mixed> */
public static function summarySchema(): array
{
return [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'type' => ['type' => 'string'],
'status' => ['type' => 'string'],
'title' => ['type' => 'string'],
'slug' => ['type' => 'string'],
'parent' => ['type' => 'integer'],
'date' => ['type' => 'string'],
'modified' => ['type' => 'string'],
'url' => ['type' => 'string'],
'edit_url' => ['type' => 'string'],
'divi' => ['type' => 'boolean'],
],
'required' => ['id', 'type', 'status', 'title', 'url', 'divi'],
];
}
/** @return array<string, mixed> */
public static function fullSchema(): array
{
$schema = self::summarySchema();
$schema['properties'] = array_merge($schema['properties'], [
'excerpt' => ['type' => 'string'],
'content' => ['type' => ['string', 'null']],
'menu_order' => ['type' => 'integer'],
'author' => ['type' => 'object'],
'template' => ['type' => 'string'],
'preview_url' => ['type' => 'string'],
'terms' => ['type' => 'object'],
'fields' => ['type' => 'object'],
'featured_image' => ['type' => ['object', 'null']],
'rev' => ['type' => 'string'],
'hints' => ['type' => 'array', 'items' => ['type' => 'string']],
]);
$schema['required'] = array_merge($schema['required'], ['content', 'terms', 'fields', 'rev', 'hints']);
return $schema;
}
/** @return array<string, list<array{id: int, name: string, slug: string}>> */
private function terms(WP_Post $post): array
{
$result = [];
foreach ($this->schema->taxonomies((string) $post->post_type) as $taxonomy) {
$terms = wp_get_post_terms((int) $post->ID, $taxonomy['name']);
if (! is_array($terms)) {
continue;
}
$result[$taxonomy['name']] = array_map(
static fn (object $term): array => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
],
$terms
);
}
return $result;
}
/** @return array<string, mixed> */
private function fields(WP_Post $post): array
{
$result = [];
foreach ($this->schema->fields((string) $post->post_type) as $key => $field) {
$result[$key] = $this->schema->sourceOf($field)->read((int) $post->ID, $field);
}
return $result;
}
/** @return array{id: int, url: string, alt: string}|null */
private function featuredImage(WP_Post $post): ?array
{
$id = (int) get_post_thumbnail_id($post);
if ($id === 0) {
return null;
}
return [
'id' => $id,
'url' => (string) wp_get_attachment_url($id),
'alt' => (string) get_post_meta($id, '_wp_attachment_image_alt', true),
];
}
}