From 8f831116b93e78ea6463fe1c520f0e1ae2c1dae5 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 11 Dec 2025 19:05:32 +0530 Subject: [PATCH 1/7] WIP Signed-off-by: Pushpak Chhajed --- rector.php | 2 - src/Install/GuidelineComposer.php | 46 +------ src/Mcp/Boost.php | 38 +++--- src/Mcp/Prompts/BladePrompt.php | 38 ++++++ .../Concerns/RendersBladeGuidelines.php | 69 ++++++++++ tests/Feature/Mcp/Prompts/BladePromptTest.php | 123 ++++++++++++++++++ .../Mcp/Prompts/PromptDiscoveryTest.php | 74 +++++++++++ 7 files changed, 323 insertions(+), 67 deletions(-) create mode 100644 src/Mcp/Prompts/BladePrompt.php create mode 100644 src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php create mode 100644 tests/Feature/Mcp/Prompts/BladePromptTest.php create mode 100644 tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php diff --git a/rector.php b/rector.php index b9c1690f..5f495026 100644 --- a/rector.php +++ b/rector.php @@ -3,7 +3,6 @@ declare(strict_types=1); use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector; -use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector; use Rector\Config\RectorConfig; use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; @@ -17,7 +16,6 @@ ReadOnlyPropertyRector::class, EncapsedStringsToSprintfRector::class, DisallowedEmptyRuleFixerRector::class, - FunctionLikeToFirstClassCallableRector::class, ]) ->withPreparedSets( deadCode: true, diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 3f045c29..91f2ee59 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -5,8 +5,8 @@ namespace Laravel\Boost\Install; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Blade; use Illuminate\Support\Str; +use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines; use Laravel\Boost\Support\Composer; use Laravel\Roster\Enums\Packages; use Laravel\Roster\Package; @@ -18,6 +18,8 @@ class GuidelineComposer { + use RendersBladeGuidelines; + protected string $userGuidelineDir = '.ai/guidelines'; /** @var Collection */ @@ -276,31 +278,6 @@ protected function guidelinesDir(string $dirPath, bool $thirdParty = false): arr ->all(); } - protected function renderContent(string $content, string $path): string - { - $isBladeTemplate = str_ends_with($path, '.blade.php'); - - if (! $isBladeTemplate) { - return $content; - } - - // Temporarily replace backticks and PHP opening tags with placeholders before Blade processing - // This prevents Blade from trying to execute PHP code examples and supports inline code - $placeholders = [ - '`' => '___SINGLE_BACKTICK___', - ' '___OPEN_PHP_TAG___', - '@volt' => '___VOLT_DIRECTIVE___', - '@endvolt' => '___ENDVOLT_DIRECTIVE___', - ]; - - $content = str_replace(array_keys($placeholders), array_values($placeholders), $content); - $rendered = Blade::render($content, [ - 'assist' => $this->getGuidelineAssist(), - ]); - - return str_replace(array_values($placeholders), array_keys($placeholders), $rendered); - } - /** * @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool} */ @@ -346,23 +323,6 @@ protected function guideline(string $path, bool $thirdParty = false): array ]; } - private array $storedSnippets = []; - - protected function processBoostSnippets(string $content): string - { - return preg_replace_callback('/(?[\'"])(?P[^\1]*?)\1(?:\s*,\s*(?P[\'"])(?P[^\3]*?)\3)?\s*\)(?P.*?)@endboostsnippet/s', function (array $matches): string { - $name = $matches['name']; - $lang = empty($matches['lang']) ? 'html' : $matches['lang']; - $snippetContent = $matches['content']; - - $placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___'; - - $this->storedSnippets[$placeholder] = ''."\n".$snippetContent."\n".''."\n\n"; - - return $placeholder; - }, $content); - } - protected function getGuidelineAssist(): GuidelineAssist { return new GuidelineAssist($this->roster, $this->config); diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index 5270518d..7aefb285 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -5,8 +5,11 @@ namespace Laravel\Boost\Mcp; use DirectoryIterator; +use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Methods\CallToolWithExecutor; +use Laravel\Boost\Mcp\Prompts\BladePrompt; use Laravel\Boost\Mcp\Resources\ApplicationInfo; +use Laravel\Boost\Support\Composer; use Laravel\Mcp\Server; class Boost extends Server @@ -58,7 +61,8 @@ protected function boot(): void { collect($this->discoverTools())->each(fn (string $tool): string => $this->tools[] = $tool); collect($this->discoverResources())->each(fn (string $resource): string => $this->resources[] = $resource); - collect($this->discoverPrompts())->each(fn (string $prompt): string => $this->prompts[] = $prompt); + + $this->discoverThirdPartyPrompts(); // Override the tools/call method to use our ToolExecutor $this->methods['tools/call'] = CallToolWithExecutor::class; @@ -122,32 +126,22 @@ protected function discoverResources(): array return $resources; } - /** - * @return array> - */ - protected function discoverPrompts(): array + protected function discoverThirdPartyPrompts(): void { - $prompts = []; + $guidelineAssist = app(GuidelineAssist::class); - $excludedPrompts = config('boost.mcp.prompts.exclude', []); - $promptDir = new DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Prompts'); + foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) { + $corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php'; - foreach ($promptDir as $promptFile) { - if ($promptFile->isFile() && $promptFile->getExtension() === 'php') { - $fqdn = 'Laravel\\Boost\\Mcp\\Prompts\\'.$promptFile->getBasename('.php'); - if (class_exists($fqdn) && ! in_array($fqdn, $excludedPrompts, true)) { - $prompts[] = $fqdn; - } - } - } - - $extraPrompts = config('boost.mcp.prompts.include', []); - foreach ($extraPrompts as $promptClass) { - if (class_exists($promptClass)) { - $prompts[] = $promptClass; + if (file_exists($corePath)) { + $this->registerThirdPartyPrompt($package, $corePath, $guidelineAssist); } } + } - return $prompts; + protected function registerThirdPartyPrompt(string $package, string $bladePath, GuidelineAssist $guidelineAssist): void + { + $prompt = new BladePrompt($guidelineAssist, $package, $bladePath); + $this->prompts[] = $prompt; } } diff --git a/src/Mcp/Prompts/BladePrompt.php b/src/Mcp/Prompts/BladePrompt.php new file mode 100644 index 00000000..c3de0ad8 --- /dev/null +++ b/src/Mcp/Prompts/BladePrompt.php @@ -0,0 +1,38 @@ +name = Str::slug(str_replace('/', '-', $packageName)).'-task'; + $this->description = "Guidelines for {$packageName}"; + } + + public function handle(): Response + { + $content = $this->renderBlade($this->bladePath); + + return Response::text($content); + } + + protected function getGuidelineAssist(): GuidelineAssist + { + return $this->guidelineAssist; + } +} diff --git a/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php new file mode 100644 index 00000000..413fcf05 --- /dev/null +++ b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php @@ -0,0 +1,69 @@ + '___SINGLE_BACKTICK___', + ' '___OPEN_PHP_TAG___', + '@volt' => '___VOLT_DIRECTIVE___', + '@endvolt' => '___ENDVOLT_DIRECTIVE___', + ]; + + $content = str_replace(array_keys($placeholders), array_values($placeholders), $content); + $rendered = Blade::render($content, [ + 'assist' => $this->getGuidelineAssist(), + ]); + + return str_replace(array_values($placeholders), array_keys($placeholders), $rendered); + } + + protected function processBoostSnippets(string $content): string + { + return preg_replace_callback('/(?[\'"])(?P[^\1]*?)\1(?:\s*,\s*(?P[\'"])(?P[^\3]*?)\3)?\s*\)(?P.*?)@endboostsnippet/s', function (array $matches): string { + $name = $matches['name']; + $lang = empty($matches['lang']) ? 'html' : $matches['lang']; + $snippetContent = $matches['content']; + + $placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___'; + + $this->storedSnippets[$placeholder] = ''."\n".$snippetContent."\n".''."\n\n"; + + return $placeholder; + }, $content); + } + + protected function renderBlade(string $bladePath): string + { + if (! file_exists($bladePath)) { + return ''; + } + + $content = file_get_contents($bladePath); + $content = $this->processBoostSnippets($content); + + $rendered = $this->renderContent($content, $bladePath); + + return str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered); + } + + abstract protected function getGuidelineAssist(): GuidelineAssist; +} diff --git a/tests/Feature/Mcp/Prompts/BladePromptTest.php b/tests/Feature/Mcp/Prompts/BladePromptTest.php new file mode 100644 index 00000000..0c8aed57 --- /dev/null +++ b/tests/Feature/Mcp/Prompts/BladePromptTest.php @@ -0,0 +1,123 @@ +testBladePath = sys_get_temp_dir().'/test-guideline.blade.php'; + file_put_contents($this->testBladePath, '# Test Guideline + +This is a test guideline for testing. + +## Rules +- Follow best practices +- Write clean code'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $this->guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); +}); + +afterEach(function (): void { + if (file_exists($this->testBladePath)) { + unlink($this->testBladePath); + } +}); + +test('it renders blade file as prompt', function (): void { + $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + + $response = $prompt->handle(); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Test Guideline') + ->toolTextContains('Follow best practices') + ->toolTextContains('Write clean code'); +}); + +test('it generates correct prompt name from package', function (): void { + $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + + expect($prompt->name())->toBe('acme-payments-task'); +}); + +test('it generates correct description from package', function (): void { + $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + + expect($prompt->description())->toBe('Guidelines for acme/payments'); +}); + +test('it handles non-existent blade file gracefully', function (): void { + $prompt = new BladePrompt('acme/test', '/non/existent/path.blade.php', $this->guidelineAssist); + + $response = $prompt->handle(); + + expect($response)->isToolResult() + ->toolHasNoError(); + + expect((string) $response->content())->toBe(''); +}); + +test('it processes backticks in blade content', function (): void { + $bladeContent = '# Guideline + +Use `Model::factory()` to create models. + +```php +User::factory()->create(); +```'; + + file_put_contents($this->testBladePath, $bladeContent); + + $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $response = $prompt->handle(); + + expect($response)->isToolResult() + ->toolTextContains('`Model::factory()`') + ->toolTextContains('```php'); +}); + +test('it processes php tags in blade content', function (): void { + $bladeContent = '# Guideline + +Example code: + +'; + + file_put_contents($this->testBladePath, $bladeContent); + + $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $response = $prompt->handle(); + + expect($response)->isToolResult() + ->toolTextContains('toolTextContains('echo "Hello World"'); +}); + +test('it processes boost snippets', function (): void { + $bladeContent = '# Guideline + +@boostsnippet(\'example\', \'php\') +function example() { + return true; +} +@endboostsnippet'; + + file_put_contents($this->testBladePath, $bladeContent); + + $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $response = $prompt->handle(); + + expect($response)->isToolResult() + ->toolTextContains('') + ->toolTextContains('function example()'); +}); diff --git a/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php b/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php new file mode 100644 index 00000000..ca4462b4 --- /dev/null +++ b/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php @@ -0,0 +1,74 @@ +toBeArray(); +}); + +test('blade prompt can be registered with correct structure', function (): void { + $testBladePath = sys_get_temp_dir().'/test-registration-'.uniqid().'.blade.php'; + file_put_contents($testBladePath, '# Test Guidelines'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); + + $prompt = new BladePrompt('test/package', $testBladePath, $guidelineAssist); + + expect($prompt)->toBeInstanceOf(BladePrompt::class) + ->and($prompt->name())->toBe('test-package-task') + ->and($prompt->description())->toBe('Guidelines for test/package'); + + unlink($testBladePath); +}); + +test('register third party prompt method creates blade prompt', function (): void { + $testBladePath = sys_get_temp_dir().'/test-third-party-'.uniqid().'.blade.php'; + file_put_contents($testBladePath, '# Third Party Guidelines'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); + + $prompt = new BladePrompt($guidelineAssist, 'vendor/package', $testBladePath); + + expect($prompt)->toBeInstanceOf(BladePrompt::class) + ->and($prompt->name())->toContain('vendor-package') + ->and($prompt->description())->toContain('vendor/package'); + + $response = $prompt->handle(); + expect($response)->isToolResult() + ->toolHasNoError(); + + unlink($testBladePath); +}); + +test('discover third party prompts skips non-existent core files', function (): void { + $testPackagePath = sys_get_temp_dir().'/test-skip-'.uniqid(); + $guidelinesPath = $testPackagePath.'/resources/boost/guidelines'; + mkdir($guidelinesPath, 0777, true); + + $otherFile = $guidelinesPath.'/other.blade.php'; + file_put_contents($otherFile, '# Other File'); + + $coreFile = $guidelinesPath.'/core.blade.php'; + + expect(file_exists($coreFile))->toBeFalse() + ->and(file_exists($otherFile))->toBeTrue(); + + unlink($otherFile); + rmdir($guidelinesPath); + rmdir($testPackagePath.'/resources/boost'); + rmdir($testPackagePath.'/resources'); + rmdir($testPackagePath); +}); From 42e84f1431f5df338df7b02e170da126817b78f9 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Mon, 15 Dec 2025 17:36:13 +0530 Subject: [PATCH 2/7] Add ThirdPartyResource and ThirdPartyPrompt classes Signed-off-by: Pushpak Chhajed --- src/Mcp/Boost.php | 43 ++++-- .../{BladePrompt.php => ThirdPartyPrompt.php} | 6 +- src/Mcp/Resources/ThirdPartyResource.php | 37 +++++ tests/Feature/Mcp/Prompts/BladePromptTest.php | 18 +-- .../Mcp/Prompts/PromptDiscoveryTest.php | 16 +-- .../Mcp/Resources/BladeResourceTest.php | 128 ++++++++++++++++++ .../Mcp/Resources/ResourceDiscoveryTest.php | 75 ++++++++++ 7 files changed, 290 insertions(+), 33 deletions(-) rename src/Mcp/Prompts/{BladePrompt.php => ThirdPartyPrompt.php} (84%) create mode 100644 src/Mcp/Resources/ThirdPartyResource.php create mode 100644 tests/Feature/Mcp/Resources/BladeResourceTest.php create mode 100644 tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index 2f45c80a..a07cca21 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -4,9 +4,11 @@ namespace Laravel\Boost\Mcp; +use InvalidArgumentException; use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Methods\CallToolWithExecutor; -use Laravel\Boost\Mcp\Prompts\BladePrompt; +use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; +use Laravel\Boost\Mcp\Resources\ThirdPartyResource; use Laravel\Boost\Mcp\Tools\ApplicationInfo; use Laravel\Boost\Mcp\Tools\BrowserLogs; use Laravel\Boost\Mcp\Tools\DatabaseConnections; @@ -112,9 +114,12 @@ protected function discoverTools(): array */ protected function discoverResources(): array { - return $this->filterPrimitives([ - Resources\ApplicationInfo::class, - ], 'resources'); + return $this->filterPrimitives( + [ + Resources\ApplicationInfo::class, + ...$this->discoverThirdPartyPrimitives(Resource::class)], + 'resources' + ); } /** @@ -123,30 +128,42 @@ protected function discoverResources(): array protected function discoverPrompts(): array { return $this->filterPrimitives( - $this->discoverThirdPartyPrompts(), + $this->discoverThirdPartyPrimitives(Prompt::class), 'prompts' ); } - protected function discoverThirdPartyPrompts(): array + /** + * @template T of Prompt|Resource + * + * @param class-string $primitiveType + * @return array + */ + private function discoverThirdPartyPrimitives(string $primitiveType): array { - $thirdPartyPrompts = []; + $primitiveClass = match ($primitiveType) { + Prompt::class => ThirdPartyPrompt::class, + Resource::class => ThirdPartyResource::class, + default => throw new InvalidArgumentException('Invalid Primitive Type'), + }; + + $primitives = []; $guidelineAssist = app(GuidelineAssist::class); foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) { - $guidelinePath = $path.DIRECTORY_SEPARATOR.'core.blade.php'; + $corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php'; - if (file_exists($guidelinePath)) { - $thirdPartyPrompts[] = new BladePrompt($guidelineAssist, $package, $guidelinePath); + if (file_exists($corePath)) { + $primitives[] = new $primitiveClass($guidelineAssist, $package, $corePath); } } - return $thirdPartyPrompts; + return $primitives; } /** - * @param array $availablePrimitives - * @return array + * @param array $availablePrimitives + * @return array */ private function filterPrimitives(array $availablePrimitives, string $type): array { diff --git a/src/Mcp/Prompts/BladePrompt.php b/src/Mcp/Prompts/ThirdPartyPrompt.php similarity index 84% rename from src/Mcp/Prompts/BladePrompt.php rename to src/Mcp/Prompts/ThirdPartyPrompt.php index c3de0ad8..b42ee93c 100644 --- a/src/Mcp/Prompts/BladePrompt.php +++ b/src/Mcp/Prompts/ThirdPartyPrompt.php @@ -4,13 +4,12 @@ namespace Laravel\Boost\Mcp\Prompts; -use Illuminate\Support\Str; use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines; use Laravel\Mcp\Response; use Laravel\Mcp\Server\Prompt; -class BladePrompt extends Prompt +class ThirdPartyPrompt extends Prompt { use RendersBladeGuidelines; @@ -20,7 +19,8 @@ public function __construct( protected string $bladePath, ) { - $this->name = Str::slug(str_replace('/', '-', $packageName)).'-task'; + $this->name = $this->packageName; + $this->title = $this->packageName; $this->description = "Guidelines for {$packageName}"; } diff --git a/src/Mcp/Resources/ThirdPartyResource.php b/src/Mcp/Resources/ThirdPartyResource.php new file mode 100644 index 00000000..94bf9f5e --- /dev/null +++ b/src/Mcp/Resources/ThirdPartyResource.php @@ -0,0 +1,37 @@ +uri = "file://instructions/{$packageName}.md"; + $this->description = "Guidelines for {$packageName}"; + $this->mimeType = 'text/markdown'; + } + + public function handle(): Response + { + $content = $this->renderBlade($this->bladePath); + + return Response::text($content); + } + + protected function getGuidelineAssist(): GuidelineAssist + { + return $this->guidelineAssist; + } +} diff --git a/tests/Feature/Mcp/Prompts/BladePromptTest.php b/tests/Feature/Mcp/Prompts/BladePromptTest.php index 0c8aed57..8c525a3c 100644 --- a/tests/Feature/Mcp/Prompts/BladePromptTest.php +++ b/tests/Feature/Mcp/Prompts/BladePromptTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Install\GuidelineConfig; use Laravel\Boost\Install\Herd; -use Laravel\Boost\Mcp\Prompts\BladePrompt; +use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; use Laravel\Roster\Roster; beforeEach(function (): void { @@ -31,7 +31,7 @@ }); test('it renders blade file as prompt', function (): void { - $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); $response = $prompt->handle(); @@ -43,19 +43,19 @@ }); test('it generates correct prompt name from package', function (): void { - $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); - expect($prompt->name())->toBe('acme-payments-task'); + expect($prompt->name())->toBe('acme/payments'); }); test('it generates correct description from package', function (): void { - $prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); expect($prompt->description())->toBe('Guidelines for acme/payments'); }); test('it handles non-existent blade file gracefully', function (): void { - $prompt = new BladePrompt('acme/test', '/non/existent/path.blade.php', $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php'); $response = $prompt->handle(); @@ -76,7 +76,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -95,7 +95,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -114,7 +114,7 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist); + $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() diff --git a/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php b/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php index ca4462b4..01f5e5d0 100644 --- a/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php +++ b/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Install\GuidelineConfig; use Laravel\Boost\Install\Herd; -use Laravel\Boost\Mcp\Prompts\BladePrompt; +use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; use Laravel\Boost\Support\Composer; use Laravel\Roster\Roster; @@ -15,7 +15,7 @@ expect($packages)->toBeArray(); }); -test('blade prompt can be registered with correct structure', function (): void { +test('blade prompt can be registered with the correct structure', function (): void { $testBladePath = sys_get_temp_dir().'/test-registration-'.uniqid().'.blade.php'; file_put_contents($testBladePath, '# Test Guidelines'); @@ -23,10 +23,10 @@ $herd = app(Herd::class); $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - $prompt = new BladePrompt('test/package', $testBladePath, $guidelineAssist); + $prompt = new ThirdPartyPrompt($guidelineAssist, 'test/package', $testBladePath); - expect($prompt)->toBeInstanceOf(BladePrompt::class) - ->and($prompt->name())->toBe('test-package-task') + expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class) + ->and($prompt->name())->toBe('test/package') ->and($prompt->description())->toBe('Guidelines for test/package'); unlink($testBladePath); @@ -40,10 +40,10 @@ $herd = app(Herd::class); $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - $prompt = new BladePrompt($guidelineAssist, 'vendor/package', $testBladePath); + $prompt = new ThirdPartyPrompt($guidelineAssist, 'vendor/package', $testBladePath); - expect($prompt)->toBeInstanceOf(BladePrompt::class) - ->and($prompt->name())->toContain('vendor-package') + expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class) + ->and($prompt->name())->toBe('vendor/package') ->and($prompt->description())->toContain('vendor/package'); $response = $prompt->handle(); diff --git a/tests/Feature/Mcp/Resources/BladeResourceTest.php b/tests/Feature/Mcp/Resources/BladeResourceTest.php new file mode 100644 index 00000000..3569df5a --- /dev/null +++ b/tests/Feature/Mcp/Resources/BladeResourceTest.php @@ -0,0 +1,128 @@ +testBladePath = sys_get_temp_dir().'/test-resource-guideline.blade.php'; + file_put_contents($this->testBladePath, '# Test Resource Guideline + +This is a test guideline for resource testing. + +## Rules +- Follow best practices +- Write clean code'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $this->guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); +}); + +afterEach(function (): void { + if (file_exists($this->testBladePath)) { + unlink($this->testBladePath); + } +}); + +test('it renders blade file as resource', function (): void { + $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); + + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Test Resource Guideline') + ->toolTextContains('Follow best practices') + ->toolTextContains('Write clean code'); +}); + +test('it generates correct resource uri from package', function (): void { + $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); + + expect($resource->uri())->toBe('file://instructions/acme/payments.md'); +}); + +test('it generates correct description from package', function (): void { + $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); + + expect($resource->description())->toBe('Guidelines for acme/payments'); +}); + +test('it has correct mime type', function (): void { + $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); + + expect($resource->mimeType())->toBe('text/markdown'); +}); + +test('it handles non-existent blade file gracefully', function (): void { + $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php'); + + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolHasNoError(); + + expect((string) $response->content())->toBe(''); +}); + +test('it processes backticks in blade content', function (): void { + $bladeContent = '# Guideline + +Use `Model::factory()` to create models. + +```php +User::factory()->create(); +```'; + + file_put_contents($this->testBladePath, $bladeContent); + + $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolTextContains('`Model::factory()`') + ->toolTextContains('```php'); +}); + +test('it processes php tags in blade content', function (): void { + $bladeContent = '# Guideline + +Example code: + +'; + + file_put_contents($this->testBladePath, $bladeContent); + + $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolTextContains('toolTextContains('echo "Hello World"'); +}); + +test('it processes boost snippets', function (): void { + $bladeContent = '# Guideline + +@boostsnippet(\'example\', \'php\') +function example() { + return true; +} +@endboostsnippet'; + + file_put_contents($this->testBladePath, $bladeContent); + + $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolTextContains('') + ->toolTextContains('function example()'); +}); diff --git a/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php b/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php new file mode 100644 index 00000000..c3b34b96 --- /dev/null +++ b/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php @@ -0,0 +1,75 @@ +toBeArray(); +}); + +test('blade resource can be registered with correct structure', function (): void { + $testBladePath = sys_get_temp_dir().'/test-resource-registration-'.uniqid().'.blade.php'; + file_put_contents($testBladePath, '# Test Guidelines'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); + + $resource = new ThirdPartyResource($guidelineAssist, 'test/package', $testBladePath); + + expect($resource)->toBeInstanceOf(ThirdPartyResource::class) + ->and($resource->uri())->toBe('file://instructions/test/package.md') + ->and($resource->description())->toBe('Guidelines for test/package') + ->and($resource->mimeType())->toBe('text/markdown'); + + unlink($testBladePath); +}); + +test('register third party resource method creates blade resource', function (): void { + $testBladePath = sys_get_temp_dir().'/test-third-party-resource-'.uniqid().'.blade.php'; + file_put_contents($testBladePath, '# Third Party Resource Guidelines'); + + $roster = app(Roster::class); + $herd = app(Herd::class); + $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); + + $resource = new ThirdPartyResource($guidelineAssist, 'vendor/package', $testBladePath); + + expect($resource)->toBeInstanceOf(ThirdPartyResource::class) + ->and($resource->uri())->toContain('vendor/package') + ->and($resource->description())->toContain('vendor/package'); + + $response = $resource->handle(); + expect($response)->isToolResult() + ->toolHasNoError(); + + unlink($testBladePath); +}); + +test('discover third party resources skips non-existent resource files', function (): void { + $testPackagePath = sys_get_temp_dir().'/test-resource-skip-'.uniqid(); + $guidelinesPath = $testPackagePath.'/resources/boost/guidelines'; + mkdir($guidelinesPath, 0777, true); + + $otherFile = $guidelinesPath.'/other.blade.php'; + file_put_contents($otherFile, '# Other File'); + + $resourceFile = $guidelinesPath.'/resource.blade.php'; + + expect(file_exists($resourceFile))->toBeFalse() + ->and(file_exists($otherFile))->toBeTrue(); + + unlink($otherFile); + rmdir($guidelinesPath); + rmdir($testPackagePath.'/resources/boost'); + rmdir($testPackagePath.'/resources'); + rmdir($testPackagePath); +}); From f1cc9a57ecf99bf7d035e428c6ac59476394d269 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Mon, 15 Dec 2025 19:08:57 +0530 Subject: [PATCH 3/7] Refactor test Signed-off-by: Pushpak Chhajed --- src/BoostServiceProvider.php | 9 ++ src/Mcp/Boost.php | 4 +- .../Concerns/RendersBladeGuidelines.php | 11 ++- src/Mcp/Prompts/ThirdPartyPrompt.php | 7 -- src/Mcp/Resources/ThirdPartyResource.php | 9 +- tests/Feature/Mcp/Prompts/BladePromptTest.php | 79 ++++++++-------- .../Mcp/Prompts/PromptDiscoveryTest.php | 74 --------------- .../Mcp/Resources/ResourceDiscoveryTest.php | 75 ---------------- ...rceTest.php => ThirdPartyResourceTest.php} | 90 +++++++++++-------- 9 files changed, 118 insertions(+), 240 deletions(-) delete mode 100644 tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php delete mode 100644 tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php rename tests/Feature/Mcp/Resources/{BladeResourceTest.php => ThirdPartyResourceTest.php} (53%) diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 32df690d..0cbba269 100644 --- a/src/BoostServiceProvider.php +++ b/src/BoostServiceProvider.php @@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; +use Laravel\Boost\Install\GuidelineAssist; +use Laravel\Boost\Install\GuidelineConfig; use Laravel\Boost\Mcp\Boost; use Laravel\Boost\Middleware\InjectBoost; use Laravel\Mcp\Facades\Mcp; @@ -58,6 +60,13 @@ public function register(): void return $roster; }); + + $this->app->singleton(GuidelineConfig::class, fn (): GuidelineConfig => new GuidelineConfig); + + $this->app->singleton(GuidelineAssist::class, fn ($app): GuidelineAssist => new GuidelineAssist( + $app->make(Roster::class), + $app->make(GuidelineConfig::class) + )); } public function boot(Router $router): void diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index a07cca21..0c65f2e1 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -5,7 +5,6 @@ namespace Laravel\Boost\Mcp; use InvalidArgumentException; -use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Methods\CallToolWithExecutor; use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; use Laravel\Boost\Mcp\Resources\ThirdPartyResource; @@ -148,13 +147,12 @@ private function discoverThirdPartyPrimitives(string $primitiveType): array }; $primitives = []; - $guidelineAssist = app(GuidelineAssist::class); foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) { $corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php'; if (file_exists($corePath)) { - $primitives[] = new $primitiveClass($guidelineAssist, $package, $corePath); + $primitives[] = new $primitiveClass($package, $corePath); } } diff --git a/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php index 413fcf05..c6dc34c1 100644 --- a/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php +++ b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php @@ -62,8 +62,15 @@ protected function renderBlade(string $bladePath): string $rendered = $this->renderContent($content, $bladePath); - return str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered); + $rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered); + + $this->storedSnippets = []; + + return $rendered; } - abstract protected function getGuidelineAssist(): GuidelineAssist; + protected function getGuidelineAssist(): GuidelineAssist + { + return app(GuidelineAssist::class); + } } diff --git a/src/Mcp/Prompts/ThirdPartyPrompt.php b/src/Mcp/Prompts/ThirdPartyPrompt.php index b42ee93c..c50e97cd 100644 --- a/src/Mcp/Prompts/ThirdPartyPrompt.php +++ b/src/Mcp/Prompts/ThirdPartyPrompt.php @@ -4,7 +4,6 @@ namespace Laravel\Boost\Mcp\Prompts; -use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines; use Laravel\Mcp\Response; use Laravel\Mcp\Server\Prompt; @@ -14,7 +13,6 @@ class ThirdPartyPrompt extends Prompt use RendersBladeGuidelines; public function __construct( - protected GuidelineAssist $guidelineAssist, protected string $packageName, protected string $bladePath, ) { @@ -30,9 +28,4 @@ public function handle(): Response return Response::text($content); } - - protected function getGuidelineAssist(): GuidelineAssist - { - return $this->guidelineAssist; - } } diff --git a/src/Mcp/Resources/ThirdPartyResource.php b/src/Mcp/Resources/ThirdPartyResource.php index 94bf9f5e..da1e621e 100644 --- a/src/Mcp/Resources/ThirdPartyResource.php +++ b/src/Mcp/Resources/ThirdPartyResource.php @@ -4,7 +4,6 @@ namespace Laravel\Boost\Mcp\Resources; -use Laravel\Boost\Install\GuidelineAssist; use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines; use Laravel\Mcp\Response; use Laravel\Mcp\Server\Resource; @@ -14,10 +13,11 @@ class ThirdPartyResource extends Resource use RendersBladeGuidelines; public function __construct( - protected GuidelineAssist $guidelineAssist, protected string $packageName, protected string $bladePath, ) { + $this->name = $this->packageName; + $this->title = $this->packageName; $this->uri = "file://instructions/{$packageName}.md"; $this->description = "Guidelines for {$packageName}"; $this->mimeType = 'text/markdown'; @@ -29,9 +29,4 @@ public function handle(): Response return Response::text($content); } - - protected function getGuidelineAssist(): GuidelineAssist - { - return $this->guidelineAssist; - } } diff --git a/tests/Feature/Mcp/Prompts/BladePromptTest.php b/tests/Feature/Mcp/Prompts/BladePromptTest.php index 8c525a3c..458651cb 100644 --- a/tests/Feature/Mcp/Prompts/BladePromptTest.php +++ b/tests/Feature/Mcp/Prompts/BladePromptTest.php @@ -2,26 +2,17 @@ declare(strict_types=1); -use Laravel\Boost\Install\GuidelineAssist; -use Laravel\Boost\Install\GuidelineConfig; -use Laravel\Boost\Install\Herd; use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; -use Laravel\Roster\Roster; beforeEach(function (): void { - // Create a test blade file $this->testBladePath = sys_get_temp_dir().'/test-guideline.blade.php'; file_put_contents($this->testBladePath, '# Test Guideline -This is a test guideline for testing. + This is a test guideline for testing. -## Rules -- Follow best practices -- Write clean code'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $this->guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); + ## Rules + - Follow best practices + - Write clean code'); }); afterEach(function (): void { @@ -30,8 +21,8 @@ } }); -test('it renders blade file as prompt', function (): void { - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); +it('renders a blade file as a prompt', function (): void { + $prompt = new ThirdPartyPrompt('acme/payments', $this->testBladePath); $response = $prompt->handle(); @@ -42,30 +33,24 @@ ->toolTextContains('Write clean code'); }); -test('it generates correct prompt name from package', function (): void { - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); +it('generates correct metadata from the package name', function (): void { + $prompt = new ThirdPartyPrompt('acme/payments', $this->testBladePath); - expect($prompt->name())->toBe('acme/payments'); + expect($prompt->name())->toBe('acme/payments') + ->and($prompt->description())->toBe('Guidelines for acme/payments'); }); -test('it generates correct description from package', function (): void { - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath); - - expect($prompt->description())->toBe('Guidelines for acme/payments'); -}); - -test('it handles non-existent blade file gracefully', function (): void { - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php'); +it('handles a non-existent blade file gracefully', function (): void { + $prompt = new ThirdPartyPrompt('acme/test', '/non/existent/path.blade.php'); $response = $prompt->handle(); - expect($response)->isToolResult() - ->toolHasNoError(); + expect($response)->isToolResult()->toolHasNoError() + ->and((string) $response->content())->toBe(''); - expect((string) $response->content())->toBe(''); }); -test('it processes backticks in blade content', function (): void { +it('processes backticks in blade content', function (): void { $bladeContent = '# Guideline Use `Model::factory()` to create models. @@ -76,7 +61,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); + $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -84,7 +69,7 @@ ->toolTextContains('```php'); }); -test('it processes php tags in blade content', function (): void { +it('processes php tags in blade content', function (): void { $bladeContent = '# Guideline Example code: @@ -95,7 +80,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); + $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -103,7 +88,7 @@ ->toolTextContains('echo "Hello World"'); }); -test('it processes boost snippets', function (): void { +it('processes boost snippets', function (): void { $bladeContent = '# Guideline @boostsnippet(\'example\', \'php\') @@ -114,10 +99,34 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath); + $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() ->toolTextContains('') ->toolTextContains('function example()'); }); + +it('clears stored snippets between multiple handle calls', function (): void { + $bladeContent = '# Guideline + +@boostsnippet(\'example\', \'php\') +function example() { + return true; +} +@endboostsnippet'; + + file_put_contents($this->testBladePath, $bladeContent); + + $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); + + $response1 = $prompt->handle(); + $content1 = (string) $response1->content(); + + $response2 = $prompt->handle(); + $content2 = (string) $response2->content(); + + expect($content1)->toBe($content2) + ->and($content1)->toContain('') + ->and($content1)->not->toContain('___BOOST_SNIPPET_'); +}); diff --git a/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php b/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php deleted file mode 100644 index 01f5e5d0..00000000 --- a/tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php +++ /dev/null @@ -1,74 +0,0 @@ -toBeArray(); -}); - -test('blade prompt can be registered with the correct structure', function (): void { - $testBladePath = sys_get_temp_dir().'/test-registration-'.uniqid().'.blade.php'; - file_put_contents($testBladePath, '# Test Guidelines'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - - $prompt = new ThirdPartyPrompt($guidelineAssist, 'test/package', $testBladePath); - - expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class) - ->and($prompt->name())->toBe('test/package') - ->and($prompt->description())->toBe('Guidelines for test/package'); - - unlink($testBladePath); -}); - -test('register third party prompt method creates blade prompt', function (): void { - $testBladePath = sys_get_temp_dir().'/test-third-party-'.uniqid().'.blade.php'; - file_put_contents($testBladePath, '# Third Party Guidelines'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - - $prompt = new ThirdPartyPrompt($guidelineAssist, 'vendor/package', $testBladePath); - - expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class) - ->and($prompt->name())->toBe('vendor/package') - ->and($prompt->description())->toContain('vendor/package'); - - $response = $prompt->handle(); - expect($response)->isToolResult() - ->toolHasNoError(); - - unlink($testBladePath); -}); - -test('discover third party prompts skips non-existent core files', function (): void { - $testPackagePath = sys_get_temp_dir().'/test-skip-'.uniqid(); - $guidelinesPath = $testPackagePath.'/resources/boost/guidelines'; - mkdir($guidelinesPath, 0777, true); - - $otherFile = $guidelinesPath.'/other.blade.php'; - file_put_contents($otherFile, '# Other File'); - - $coreFile = $guidelinesPath.'/core.blade.php'; - - expect(file_exists($coreFile))->toBeFalse() - ->and(file_exists($otherFile))->toBeTrue(); - - unlink($otherFile); - rmdir($guidelinesPath); - rmdir($testPackagePath.'/resources/boost'); - rmdir($testPackagePath.'/resources'); - rmdir($testPackagePath); -}); diff --git a/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php b/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php deleted file mode 100644 index c3b34b96..00000000 --- a/tests/Feature/Mcp/Resources/ResourceDiscoveryTest.php +++ /dev/null @@ -1,75 +0,0 @@ -toBeArray(); -}); - -test('blade resource can be registered with correct structure', function (): void { - $testBladePath = sys_get_temp_dir().'/test-resource-registration-'.uniqid().'.blade.php'; - file_put_contents($testBladePath, '# Test Guidelines'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - - $resource = new ThirdPartyResource($guidelineAssist, 'test/package', $testBladePath); - - expect($resource)->toBeInstanceOf(ThirdPartyResource::class) - ->and($resource->uri())->toBe('file://instructions/test/package.md') - ->and($resource->description())->toBe('Guidelines for test/package') - ->and($resource->mimeType())->toBe('text/markdown'); - - unlink($testBladePath); -}); - -test('register third party resource method creates blade resource', function (): void { - $testBladePath = sys_get_temp_dir().'/test-third-party-resource-'.uniqid().'.blade.php'; - file_put_contents($testBladePath, '# Third Party Resource Guidelines'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); - - $resource = new ThirdPartyResource($guidelineAssist, 'vendor/package', $testBladePath); - - expect($resource)->toBeInstanceOf(ThirdPartyResource::class) - ->and($resource->uri())->toContain('vendor/package') - ->and($resource->description())->toContain('vendor/package'); - - $response = $resource->handle(); - expect($response)->isToolResult() - ->toolHasNoError(); - - unlink($testBladePath); -}); - -test('discover third party resources skips non-existent resource files', function (): void { - $testPackagePath = sys_get_temp_dir().'/test-resource-skip-'.uniqid(); - $guidelinesPath = $testPackagePath.'/resources/boost/guidelines'; - mkdir($guidelinesPath, 0777, true); - - $otherFile = $guidelinesPath.'/other.blade.php'; - file_put_contents($otherFile, '# Other File'); - - $resourceFile = $guidelinesPath.'/resource.blade.php'; - - expect(file_exists($resourceFile))->toBeFalse() - ->and(file_exists($otherFile))->toBeTrue(); - - unlink($otherFile); - rmdir($guidelinesPath); - rmdir($testPackagePath.'/resources/boost'); - rmdir($testPackagePath.'/resources'); - rmdir($testPackagePath); -}); diff --git a/tests/Feature/Mcp/Resources/BladeResourceTest.php b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php similarity index 53% rename from tests/Feature/Mcp/Resources/BladeResourceTest.php rename to tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php index 3569df5a..82f3efab 100644 --- a/tests/Feature/Mcp/Resources/BladeResourceTest.php +++ b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php @@ -2,11 +2,7 @@ declare(strict_types=1); -use Laravel\Boost\Install\GuidelineAssist; -use Laravel\Boost\Install\GuidelineConfig; -use Laravel\Boost\Install\Herd; use Laravel\Boost\Mcp\Resources\ThirdPartyResource; -use Laravel\Roster\Roster; beforeEach(function (): void { $this->testBladePath = sys_get_temp_dir().'/test-resource-guideline.blade.php'; @@ -17,10 +13,6 @@ ## Rules - Follow best practices - Write clean code'); - - $roster = app(Roster::class); - $herd = app(Herd::class); - $this->guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd); }); afterEach(function (): void { @@ -29,48 +21,37 @@ } }); -test('it renders blade file as resource', function (): void { - $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); - - $response = $resource->handle(); - - expect($response)->isToolResult() - ->toolHasNoError() - ->toolTextContains('Test Resource Guideline') - ->toolTextContains('Follow best practices') - ->toolTextContains('Write clean code'); -}); - -test('it generates correct resource uri from package', function (): void { - $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); +test('it generates resource uri from the package name', function (): void { + $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); expect($resource->uri())->toBe('file://instructions/acme/payments.md'); }); -test('it generates correct description from package', function (): void { - $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); +test('it generates a description from the package name', function (): void { + $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); expect($resource->description())->toBe('Guidelines for acme/payments'); }); -test('it has correct mime type', function (): void { - $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath); +test('it has Markdown mime type', function (): void { + $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); expect($resource->mimeType())->toBe('text/markdown'); }); -test('it handles non-existent blade file gracefully', function (): void { - $resource = new ThirdPartyResource($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php'); +test('it renders blade guideline content', function (): void { + $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() - ->toolHasNoError(); - - expect((string) $response->content())->toBe(''); + ->toolHasNoError() + ->toolTextContains('Test Resource Guideline') + ->toolTextContains('Follow best practices') + ->toolTextContains('Write clean code'); }); -test('it processes backticks in blade content', function (): void { +test('it preserves inline code backticks in content', function (): void { $bladeContent = '# Guideline Use `Model::factory()` to create models. @@ -81,7 +62,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $resource = new ThirdPartyResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() @@ -89,7 +70,7 @@ ->toolTextContains('```php'); }); -test('it processes php tags in blade content', function (): void { +test('it preserves php tags in content', function (): void { $bladeContent = '# Guideline Example code: @@ -100,7 +81,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $resource = new ThirdPartyResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() @@ -108,7 +89,7 @@ ->toolTextContains('echo "Hello World"'); }); -test('it processes boost snippets', function (): void { +test('it processes boost snippet directives', function (): void { $bladeContent = '# Guideline @boostsnippet(\'example\', \'php\') @@ -119,10 +100,45 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath); + $resource = new ThirdPartyResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() ->toolTextContains('') ->toolTextContains('function example()'); }); + +test('it clears stored snippets between multiple handle calls', function (): void { + $bladeContent = '# Guideline + +@boostsnippet(\'example\', \'php\') +function example() { + return true; +} +@endboostsnippet'; + + file_put_contents($this->testBladePath, $bladeContent); + + $resource = new ThirdPartyResource('test/package', $this->testBladePath); + + $response1 = $resource->handle(); + $content1 = (string) $response1->content(); + + $response2 = $resource->handle(); + $content2 = (string) $response2->content(); + + expect($content1)->toBe($content2) + ->and($content1)->toContain('') + ->and($content1)->not->toContain('___BOOST_SNIPPET_'); // No unresolved placeholders +}); + +test('it handles non-existent blade file gracefully', function (): void { + $resource = new ThirdPartyResource('acme/test', '/non/existent/path.blade.php'); + + $response = $resource->handle(); + + expect($response)->isToolResult() + ->toolHasNoError() + ->and((string) $response->content())->toBe(''); + +}); From ba1c52e26a90a1b196d9eef3f0459c570042de85 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Mon, 15 Dec 2025 19:59:30 +0530 Subject: [PATCH 4/7] Formatting Signed-off-by: Pushpak Chhajed --- src/Mcp/Boost.php | 8 ++++---- ...yPrompt.php => PackageGuidelinePrompt.php} | 2 +- ...ource.php => PackageGuidelineResource.php} | 2 +- tests/Feature/Mcp/Prompts/BladePromptTest.php | 16 +++++++-------- .../Mcp/Resources/ThirdPartyResourceTest.php | 20 +++++++++---------- 5 files changed, 24 insertions(+), 24 deletions(-) rename src/Mcp/Prompts/{ThirdPartyPrompt.php => PackageGuidelinePrompt.php} (93%) rename src/Mcp/Resources/{ThirdPartyResource.php => PackageGuidelineResource.php} (94%) diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index 0c65f2e1..1b1c4cdc 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -6,8 +6,8 @@ use InvalidArgumentException; use Laravel\Boost\Mcp\Methods\CallToolWithExecutor; -use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; -use Laravel\Boost\Mcp\Resources\ThirdPartyResource; +use Laravel\Boost\Mcp\Prompts\PackageGuidelinePrompt; +use Laravel\Boost\Mcp\Resources\PackageGuidelineResource; use Laravel\Boost\Mcp\Tools\ApplicationInfo; use Laravel\Boost\Mcp\Tools\BrowserLogs; use Laravel\Boost\Mcp\Tools\DatabaseConnections; @@ -141,8 +141,8 @@ protected function discoverPrompts(): array private function discoverThirdPartyPrimitives(string $primitiveType): array { $primitiveClass = match ($primitiveType) { - Prompt::class => ThirdPartyPrompt::class, - Resource::class => ThirdPartyResource::class, + Prompt::class => PackageGuidelinePrompt::class, + Resource::class => PackageGuidelineResource::class, default => throw new InvalidArgumentException('Invalid Primitive Type'), }; diff --git a/src/Mcp/Prompts/ThirdPartyPrompt.php b/src/Mcp/Prompts/PackageGuidelinePrompt.php similarity index 93% rename from src/Mcp/Prompts/ThirdPartyPrompt.php rename to src/Mcp/Prompts/PackageGuidelinePrompt.php index c50e97cd..7608748d 100644 --- a/src/Mcp/Prompts/ThirdPartyPrompt.php +++ b/src/Mcp/Prompts/PackageGuidelinePrompt.php @@ -8,7 +8,7 @@ use Laravel\Mcp\Response; use Laravel\Mcp\Server\Prompt; -class ThirdPartyPrompt extends Prompt +class PackageGuidelinePrompt extends Prompt { use RendersBladeGuidelines; diff --git a/src/Mcp/Resources/ThirdPartyResource.php b/src/Mcp/Resources/PackageGuidelineResource.php similarity index 94% rename from src/Mcp/Resources/ThirdPartyResource.php rename to src/Mcp/Resources/PackageGuidelineResource.php index da1e621e..f98fbe56 100644 --- a/src/Mcp/Resources/ThirdPartyResource.php +++ b/src/Mcp/Resources/PackageGuidelineResource.php @@ -8,7 +8,7 @@ use Laravel\Mcp\Response; use Laravel\Mcp\Server\Resource; -class ThirdPartyResource extends Resource +class PackageGuidelineResource extends Resource { use RendersBladeGuidelines; diff --git a/tests/Feature/Mcp/Prompts/BladePromptTest.php b/tests/Feature/Mcp/Prompts/BladePromptTest.php index 458651cb..4d696b01 100644 --- a/tests/Feature/Mcp/Prompts/BladePromptTest.php +++ b/tests/Feature/Mcp/Prompts/BladePromptTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt; +use Laravel\Boost\Mcp\Prompts\PackageGuidelinePrompt; beforeEach(function (): void { $this->testBladePath = sys_get_temp_dir().'/test-guideline.blade.php'; @@ -22,7 +22,7 @@ }); it('renders a blade file as a prompt', function (): void { - $prompt = new ThirdPartyPrompt('acme/payments', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('acme/payments', $this->testBladePath); $response = $prompt->handle(); @@ -34,14 +34,14 @@ }); it('generates correct metadata from the package name', function (): void { - $prompt = new ThirdPartyPrompt('acme/payments', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('acme/payments', $this->testBladePath); expect($prompt->name())->toBe('acme/payments') ->and($prompt->description())->toBe('Guidelines for acme/payments'); }); it('handles a non-existent blade file gracefully', function (): void { - $prompt = new ThirdPartyPrompt('acme/test', '/non/existent/path.blade.php'); + $prompt = new PackageGuidelinePrompt('acme/test', '/non/existent/path.blade.php'); $response = $prompt->handle(); @@ -61,7 +61,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -80,7 +80,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -99,7 +99,7 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('test/package', $this->testBladePath); $response = $prompt->handle(); expect($response)->isToolResult() @@ -118,7 +118,7 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $prompt = new ThirdPartyPrompt('test/package', $this->testBladePath); + $prompt = new PackageGuidelinePrompt('test/package', $this->testBladePath); $response1 = $prompt->handle(); $content1 = (string) $response1->content(); diff --git a/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php index 82f3efab..12639334 100644 --- a/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php +++ b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Laravel\Boost\Mcp\Resources\ThirdPartyResource; +use Laravel\Boost\Mcp\Resources\PackageGuidelineResource; beforeEach(function (): void { $this->testBladePath = sys_get_temp_dir().'/test-resource-guideline.blade.php'; @@ -22,25 +22,25 @@ }); test('it generates resource uri from the package name', function (): void { - $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); + $resource = new PackageGuidelineResource('acme/payments', $this->testBladePath); expect($resource->uri())->toBe('file://instructions/acme/payments.md'); }); test('it generates a description from the package name', function (): void { - $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); + $resource = new PackageGuidelineResource('acme/payments', $this->testBladePath); expect($resource->description())->toBe('Guidelines for acme/payments'); }); test('it has Markdown mime type', function (): void { - $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); + $resource = new PackageGuidelineResource('acme/payments', $this->testBladePath); expect($resource->mimeType())->toBe('text/markdown'); }); test('it renders blade guideline content', function (): void { - $resource = new ThirdPartyResource('acme/payments', $this->testBladePath); + $resource = new PackageGuidelineResource('acme/payments', $this->testBladePath); $response = $resource->handle(); @@ -62,7 +62,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource('test/package', $this->testBladePath); + $resource = new PackageGuidelineResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() @@ -81,7 +81,7 @@ file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource('test/package', $this->testBladePath); + $resource = new PackageGuidelineResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() @@ -100,7 +100,7 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource('test/package', $this->testBladePath); + $resource = new PackageGuidelineResource('test/package', $this->testBladePath); $response = $resource->handle(); expect($response)->isToolResult() @@ -119,7 +119,7 @@ function example() { file_put_contents($this->testBladePath, $bladeContent); - $resource = new ThirdPartyResource('test/package', $this->testBladePath); + $resource = new PackageGuidelineResource('test/package', $this->testBladePath); $response1 = $resource->handle(); $content1 = (string) $response1->content(); @@ -133,7 +133,7 @@ function example() { }); test('it handles non-existent blade file gracefully', function (): void { - $resource = new ThirdPartyResource('acme/test', '/non/existent/path.blade.php'); + $resource = new PackageGuidelineResource('acme/test', '/non/existent/path.blade.php'); $response = $resource->handle(); From 9c9c257ca3d5bcf10681aa65b22815079593d05a Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 16 Dec 2025 17:33:04 +0530 Subject: [PATCH 5/7] Formatting Signed-off-by: Pushpak Chhajed --- src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php | 2 +- src/Mcp/Prompts/PackageGuidelinePrompt.php | 3 +-- src/Mcp/Resources/PackageGuidelineResource.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php index c6dc34c1..51a95bf4 100644 --- a/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php +++ b/src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php @@ -51,7 +51,7 @@ protected function processBoostSnippets(string $content): string }, $content); } - protected function renderBlade(string $bladePath): string + protected function renderGuidelineFile(string $bladePath): string { if (! file_exists($bladePath)) { return ''; diff --git a/src/Mcp/Prompts/PackageGuidelinePrompt.php b/src/Mcp/Prompts/PackageGuidelinePrompt.php index 7608748d..b414e4af 100644 --- a/src/Mcp/Prompts/PackageGuidelinePrompt.php +++ b/src/Mcp/Prompts/PackageGuidelinePrompt.php @@ -16,7 +16,6 @@ public function __construct( protected string $packageName, protected string $bladePath, ) { - $this->name = $this->packageName; $this->title = $this->packageName; $this->description = "Guidelines for {$packageName}"; @@ -24,7 +23,7 @@ public function __construct( public function handle(): Response { - $content = $this->renderBlade($this->bladePath); + $content = $this->renderGuidelineFile($this->bladePath); return Response::text($content); } diff --git a/src/Mcp/Resources/PackageGuidelineResource.php b/src/Mcp/Resources/PackageGuidelineResource.php index f98fbe56..a025b103 100644 --- a/src/Mcp/Resources/PackageGuidelineResource.php +++ b/src/Mcp/Resources/PackageGuidelineResource.php @@ -25,7 +25,7 @@ public function __construct( public function handle(): Response { - $content = $this->renderBlade($this->bladePath); + $content = $this->renderGuidelineFile($this->bladePath); return Response::text($content); } From 3fc349942cd7d26c73bc899452053b326323c7ec Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 16 Dec 2025 17:43:16 +0530 Subject: [PATCH 6/7] Formatting Signed-off-by: Pushpak Chhajed --- tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php index 12639334..0716c0ea 100644 --- a/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php +++ b/tests/Feature/Mcp/Resources/ThirdPartyResourceTest.php @@ -129,7 +129,7 @@ function example() { expect($content1)->toBe($content2) ->and($content1)->toContain('') - ->and($content1)->not->toContain('___BOOST_SNIPPET_'); // No unresolved placeholders + ->and($content1)->not->toContain('___BOOST_SNIPPET___'); }); test('it handles non-existent blade file gracefully', function (): void { From 7a5ea6991c8172bbcc05d015d91c9365f730c9f4 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 17 Dec 2025 23:36:20 +0530 Subject: [PATCH 7/7] Formatting Signed-off-by: Pushpak Chhajed --- src/Mcp/Boost.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index 1b1c4cdc..4b48c69b 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -113,12 +113,12 @@ protected function discoverTools(): array */ protected function discoverResources(): array { - return $this->filterPrimitives( - [ - Resources\ApplicationInfo::class, - ...$this->discoverThirdPartyPrimitives(Resource::class)], - 'resources' - ); + $availableResources = [ + Resources\ApplicationInfo::class, + ...$this->discoverThirdPartyPrimitives(Resource::class), + ]; + + return $this->filterPrimitives($availableResources, 'resources'); } /**