Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add ThirdPartyResource and ThirdPartyPrompt classes
Signed-off-by: Pushpak Chhajed <[email protected]>
  • Loading branch information
pushpak1300 committed Dec 15, 2025
commit 42e84f1431f5df338df7b02e170da126817b78f9
43 changes: 30 additions & 13 deletions src/Mcp/Boost.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
);
}

/**
Expand All @@ -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<T> $primitiveType
* @return array<int, T>
*/
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<int, class-string> $availablePrimitives
* @return array<int, class-string>
* @param array<int, Tool|Resource|Prompt|class-string> $availablePrimitives
* @return array<int, Tool|Resource|Prompt|class-string>
*/
private function filterPrimitives(array $availablePrimitives, string $type): array
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}";
}

Expand Down
37 changes: 37 additions & 0 deletions src/Mcp/Resources/ThirdPartyResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

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;

class ThirdPartyResource extends Resource
{
use RendersBladeGuidelines;

public function __construct(
protected GuidelineAssist $guidelineAssist,
protected string $packageName,
protected string $bladePath,
) {
$this->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;
}
}
18 changes: 9 additions & 9 deletions tests/Feature/Mcp/Prompts/BladePromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down
16 changes: 8 additions & 8 deletions tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,18 +15,18 @@
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');

$roster = app(Roster::class);
$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);
Expand All @@ -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();
Expand Down
128 changes: 128 additions & 0 deletions tests/Feature/Mcp/Resources/BladeResourceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

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';
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:

<?php
echo "Hello World";
?>';

file_put_contents($this->testBladePath, $bladeContent);

$resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath);
$response = $resource->handle();

expect($response)->isToolResult()
->toolTextContains('<?php')
->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('<code-snippet name="example" lang="php">')
->toolTextContains('function example()');
});
Loading