From 858aa6a182097240322c3845dd535aea7598c999 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sun, 27 Sep 2026 00:39:14 +0200 Subject: [PATCH] feat: lit-guide topic all returns every guide in one call Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- src/Integrations/Core/GuideAbility.php | 55 ++++++++++++++++++- .../Integrations/Core/GuideAbilityTest.php | 28 ++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/Integrations/Core/GuideAbility.php b/src/Integrations/Core/GuideAbility.php index 8cd7800..b9aaa28 100644 --- a/src/Integrations/Core/GuideAbility.php +++ b/src/Integrations/Core/GuideAbility.php @@ -16,6 +16,12 @@ final class GuideAbility implements Ability { private const DEFAULT_TOPIC = 'overview'; + /** topic = all: every topic in one call (overview first). */ + public const ALL = 'all'; + + /** Upper bound of the combined text of topic = all (bytes). */ + public const ALL_LIMIT = 100000; + public function __construct(private GuideRepository $guides) { } @@ -32,7 +38,8 @@ final class GuideAbility implements Ability public function description(): string { - return 'Liefert Anleitungen zur Bedienung dieses MCP-Servers. Ohne topic: Übersicht und Liste aller Themen.'; + return 'Liefert Anleitungen zur Bedienung dieses MCP-Servers. Ohne topic: Übersicht und Liste aller Themen; ' + . 'topic = all: alle Themen in einem Aufruf (gut für den Einstieg).'; } public function inputSchema(): array @@ -42,7 +49,7 @@ final class GuideAbility implements Ability 'properties' => [ 'topic' => [ 'type' => 'string', - 'description' => 'Thema, z. B. "overview" oder "divi-shortform".', + 'description' => 'Thema, z. B. "overview" oder "divi-shortform"; "all" = alle Themen.', ], ], 'additionalProperties' => false, @@ -83,6 +90,10 @@ final class GuideAbility implements Ability ? $input['topic'] : self::DEFAULT_TOPIC; $topics = $this->guides->topics(); + if ($topic === self::ALL) { + return ['topic' => self::ALL, 'topics' => $topics, 'content' => $this->all($topics)]; + } + $content = $this->guides->get($topic); if ($content === null) { @@ -94,4 +105,44 @@ final class GuideAbility implements Ability return ['topic' => $topic, 'topics' => $topics, 'content' => $content]; } + + /** + * Every topic, overview first, separated and labelled; stops before ALL_LIMIT bytes and names + * the topics left out. + * + * @param list $topics + */ + private function all(array $topics): string + { + $ordered = in_array(self::DEFAULT_TOPIC, $topics, true) + ? array_merge([self::DEFAULT_TOPIC], array_values(array_diff($topics, [self::DEFAULT_TOPIC]))) + : $topics; + $parts = []; + $size = 0; + $left = []; + foreach ($ordered as $topic) { + $content = $this->guides->get($topic); + if ($content === null) { + continue; + } + + $part = sprintf("---\nThema `%s`:\n\n%s", $topic, rtrim($content)); + if ($size + strlen($part) > self::ALL_LIMIT) { + $left[] = $topic; + continue; + } + + $parts[] = $part; + $size += strlen($part) + 2; + } + + if ($left !== []) { + $parts[] = sprintf( + 'Nicht enthalten (Größenlimit): %s – einzeln mit topic abrufen.', + implode(', ', $left) + ); + } + + return implode("\n\n", $parts); + } } diff --git a/tests/Unit/Integrations/Core/GuideAbilityTest.php b/tests/Unit/Integrations/Core/GuideAbilityTest.php index a669e86..10d30dd 100644 --- a/tests/Unit/Integrations/Core/GuideAbilityTest.php +++ b/tests/Unit/Integrations/Core/GuideAbilityTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Core; +use Brain\Monkey\Filters; use LIT\MCP\Guides\GuideRepository; use LIT\MCP\Integrations\Core\GuideAbility; use LIT\MCP\Tests\Unit\TestCase; @@ -50,4 +51,31 @@ final class GuideAbilityTest extends TestCase self::assertTrue($this->ability->isDirect()); self::assertTrue($this->ability->annotations()['readonly']); } + + public function testAllTopicsInOneCallOverviewFirst(): void + { + $result = $this->ability->execute(['topic' => 'all']); + + self::assertSame('all', $result['topic']); + self::assertSame(['divi-shortform', 'overview'], $result['topics']); + $overview = strpos($result['content'], 'Thema `overview`'); + $shortform = strpos($result['content'], 'Thema `divi-shortform`'); + self::assertIsInt($overview); + self::assertIsInt($shortform); + self::assertLessThan($shortform, $overview); + self::assertStringNotContainsString('Nicht enthalten', $result['content']); + } + + public function testAllTopicsStopAtTheSizeLimitAndNameTheRest(): void + { + $big = str_repeat('x', GuideAbility::ALL_LIMIT); + // Brain Monkey owns apply_filters(): answer the guide filter through its hooks API. + Filters\expectApplied('lit_mcp_guides')->andReturn(['gross' => $big]); + $ability = new GuideAbility(new GuideRepository(dirname(__DIR__, 3) . '/Fixtures/guides')); + + $content = $ability->execute(['topic' => 'all'])['content']; + + self::assertLessThanOrEqual(GuideAbility::ALL_LIMIT + 200, strlen($content)); + self::assertStringContainsString('Nicht enthalten (Größenlimit): gross', $content); + } }