Skip to content

Commit 42e84f1

Browse files
committed
Add ThirdPartyResource and ThirdPartyPrompt classes
Signed-off-by: Pushpak Chhajed <[email protected]>
1 parent 1732169 commit 42e84f1

File tree

7 files changed

+290
-33
lines changed

7 files changed

+290
-33
lines changed

src/Mcp/Boost.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
namespace Laravel\Boost\Mcp;
66

7+
use InvalidArgumentException;
78
use Laravel\Boost\Install\GuidelineAssist;
89
use Laravel\Boost\Mcp\Methods\CallToolWithExecutor;
9-
use Laravel\Boost\Mcp\Prompts\BladePrompt;
10+
use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt;
11+
use Laravel\Boost\Mcp\Resources\ThirdPartyResource;
1012
use Laravel\Boost\Mcp\Tools\ApplicationInfo;
1113
use Laravel\Boost\Mcp\Tools\BrowserLogs;
1214
use Laravel\Boost\Mcp\Tools\DatabaseConnections;
@@ -112,9 +114,12 @@ protected function discoverTools(): array
112114
*/
113115
protected function discoverResources(): array
114116
{
115-
return $this->filterPrimitives([
116-
Resources\ApplicationInfo::class,
117-
], 'resources');
117+
return $this->filterPrimitives(
118+
[
119+
Resources\ApplicationInfo::class,
120+
...$this->discoverThirdPartyPrimitives(Resource::class)],
121+
'resources'
122+
);
118123
}
119124

120125
/**
@@ -123,30 +128,42 @@ protected function discoverResources(): array
123128
protected function discoverPrompts(): array
124129
{
125130
return $this->filterPrimitives(
126-
$this->discoverThirdPartyPrompts(),
131+
$this->discoverThirdPartyPrimitives(Prompt::class),
127132
'prompts'
128133
);
129134
}
130135

131-
protected function discoverThirdPartyPrompts(): array
136+
/**
137+
* @template T of Prompt|Resource
138+
*
139+
* @param class-string<T> $primitiveType
140+
* @return array<int, T>
141+
*/
142+
private function discoverThirdPartyPrimitives(string $primitiveType): array
132143
{
133-
$thirdPartyPrompts = [];
144+
$primitiveClass = match ($primitiveType) {
145+
Prompt::class => ThirdPartyPrompt::class,
146+
Resource::class => ThirdPartyResource::class,
147+
default => throw new InvalidArgumentException('Invalid Primitive Type'),
148+
};
149+
150+
$primitives = [];
134151
$guidelineAssist = app(GuidelineAssist::class);
135152

136153
foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) {
137-
$guidelinePath = $path.DIRECTORY_SEPARATOR.'core.blade.php';
154+
$corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php';
138155

139-
if (file_exists($guidelinePath)) {
140-
$thirdPartyPrompts[] = new BladePrompt($guidelineAssist, $package, $guidelinePath);
156+
if (file_exists($corePath)) {
157+
$primitives[] = new $primitiveClass($guidelineAssist, $package, $corePath);
141158
}
142159
}
143160

144-
return $thirdPartyPrompts;
161+
return $primitives;
145162
}
146163

147164
/**
148-
* @param array<int, class-string> $availablePrimitives
149-
* @return array<int, class-string>
165+
* @param array<int, Tool|Resource|Prompt|class-string> $availablePrimitives
166+
* @return array<int, Tool|Resource|Prompt|class-string>
150167
*/
151168
private function filterPrimitives(array $availablePrimitives, string $type): array
152169
{
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44

55
namespace Laravel\Boost\Mcp\Prompts;
66

7-
use Illuminate\Support\Str;
87
use Laravel\Boost\Install\GuidelineAssist;
98
use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
109
use Laravel\Mcp\Response;
1110
use Laravel\Mcp\Server\Prompt;
1211

13-
class BladePrompt extends Prompt
12+
class ThirdPartyPrompt extends Prompt
1413
{
1514
use RendersBladeGuidelines;
1615

@@ -20,7 +19,8 @@ public function __construct(
2019
protected string $bladePath,
2120
) {
2221

23-
$this->name = Str::slug(str_replace('/', '-', $packageName)).'-task';
22+
$this->name = $this->packageName;
23+
$this->title = $this->packageName;
2424
$this->description = "Guidelines for {$packageName}";
2525
}
2626

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Mcp\Resources;
6+
7+
use Laravel\Boost\Install\GuidelineAssist;
8+
use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
9+
use Laravel\Mcp\Response;
10+
use Laravel\Mcp\Server\Resource;
11+
12+
class ThirdPartyResource extends Resource
13+
{
14+
use RendersBladeGuidelines;
15+
16+
public function __construct(
17+
protected GuidelineAssist $guidelineAssist,
18+
protected string $packageName,
19+
protected string $bladePath,
20+
) {
21+
$this->uri = "file://instructions/{$packageName}.md";
22+
$this->description = "Guidelines for {$packageName}";
23+
$this->mimeType = 'text/markdown';
24+
}
25+
26+
public function handle(): Response
27+
{
28+
$content = $this->renderBlade($this->bladePath);
29+
30+
return Response::text($content);
31+
}
32+
33+
protected function getGuidelineAssist(): GuidelineAssist
34+
{
35+
return $this->guidelineAssist;
36+
}
37+
}

tests/Feature/Mcp/Prompts/BladePromptTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Laravel\Boost\Install\GuidelineAssist;
66
use Laravel\Boost\Install\GuidelineConfig;
77
use Laravel\Boost\Install\Herd;
8-
use Laravel\Boost\Mcp\Prompts\BladePrompt;
8+
use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt;
99
use Laravel\Roster\Roster;
1010

1111
beforeEach(function (): void {
@@ -31,7 +31,7 @@
3131
});
3232

3333
test('it renders blade file as prompt', function (): void {
34-
$prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist);
34+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath);
3535

3636
$response = $prompt->handle();
3737

@@ -43,19 +43,19 @@
4343
});
4444

4545
test('it generates correct prompt name from package', function (): void {
46-
$prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist);
46+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath);
4747

48-
expect($prompt->name())->toBe('acme-payments-task');
48+
expect($prompt->name())->toBe('acme/payments');
4949
});
5050

5151
test('it generates correct description from package', function (): void {
52-
$prompt = new BladePrompt('acme/payments', $this->testBladePath, $this->guidelineAssist);
52+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/payments', $this->testBladePath);
5353

5454
expect($prompt->description())->toBe('Guidelines for acme/payments');
5555
});
5656

5757
test('it handles non-existent blade file gracefully', function (): void {
58-
$prompt = new BladePrompt('acme/test', '/non/existent/path.blade.php', $this->guidelineAssist);
58+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php');
5959

6060
$response = $prompt->handle();
6161

@@ -76,7 +76,7 @@
7676

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

79-
$prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist);
79+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath);
8080
$response = $prompt->handle();
8181

8282
expect($response)->isToolResult()
@@ -95,7 +95,7 @@
9595

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

98-
$prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist);
98+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath);
9999
$response = $prompt->handle();
100100

101101
expect($response)->isToolResult()
@@ -114,7 +114,7 @@ function example() {
114114

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

117-
$prompt = new BladePrompt('test/package', $this->testBladePath, $this->guidelineAssist);
117+
$prompt = new ThirdPartyPrompt($this->guidelineAssist, 'test/package', $this->testBladePath);
118118
$response = $prompt->handle();
119119

120120
expect($response)->isToolResult()

tests/Feature/Mcp/Prompts/PromptDiscoveryTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Laravel\Boost\Install\GuidelineAssist;
66
use Laravel\Boost\Install\GuidelineConfig;
77
use Laravel\Boost\Install\Herd;
8-
use Laravel\Boost\Mcp\Prompts\BladePrompt;
8+
use Laravel\Boost\Mcp\Prompts\ThirdPartyPrompt;
99
use Laravel\Boost\Support\Composer;
1010
use Laravel\Roster\Roster;
1111

@@ -15,18 +15,18 @@
1515
expect($packages)->toBeArray();
1616
});
1717

18-
test('blade prompt can be registered with correct structure', function (): void {
18+
test('blade prompt can be registered with the correct structure', function (): void {
1919
$testBladePath = sys_get_temp_dir().'/test-registration-'.uniqid().'.blade.php';
2020
file_put_contents($testBladePath, '# Test Guidelines');
2121

2222
$roster = app(Roster::class);
2323
$herd = app(Herd::class);
2424
$guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd);
2525

26-
$prompt = new BladePrompt('test/package', $testBladePath, $guidelineAssist);
26+
$prompt = new ThirdPartyPrompt($guidelineAssist, 'test/package', $testBladePath);
2727

28-
expect($prompt)->toBeInstanceOf(BladePrompt::class)
29-
->and($prompt->name())->toBe('test-package-task')
28+
expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class)
29+
->and($prompt->name())->toBe('test/package')
3030
->and($prompt->description())->toBe('Guidelines for test/package');
3131

3232
unlink($testBladePath);
@@ -40,10 +40,10 @@
4040
$herd = app(Herd::class);
4141
$guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd);
4242

43-
$prompt = new BladePrompt($guidelineAssist, 'vendor/package', $testBladePath);
43+
$prompt = new ThirdPartyPrompt($guidelineAssist, 'vendor/package', $testBladePath);
4444

45-
expect($prompt)->toBeInstanceOf(BladePrompt::class)
46-
->and($prompt->name())->toContain('vendor-package')
45+
expect($prompt)->toBeInstanceOf(ThirdPartyPrompt::class)
46+
->and($prompt->name())->toBe('vendor/package')
4747
->and($prompt->description())->toContain('vendor/package');
4848

4949
$response = $prompt->handle();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Boost\Install\GuidelineAssist;
6+
use Laravel\Boost\Install\GuidelineConfig;
7+
use Laravel\Boost\Install\Herd;
8+
use Laravel\Boost\Mcp\Resources\ThirdPartyResource;
9+
use Laravel\Roster\Roster;
10+
11+
beforeEach(function (): void {
12+
$this->testBladePath = sys_get_temp_dir().'/test-resource-guideline.blade.php';
13+
file_put_contents($this->testBladePath, '# Test Resource Guideline
14+
15+
This is a test guideline for resource testing.
16+
17+
## Rules
18+
- Follow best practices
19+
- Write clean code');
20+
21+
$roster = app(Roster::class);
22+
$herd = app(Herd::class);
23+
$this->guidelineAssist = new GuidelineAssist($roster, new GuidelineConfig, $herd);
24+
});
25+
26+
afterEach(function (): void {
27+
if (file_exists($this->testBladePath)) {
28+
unlink($this->testBladePath);
29+
}
30+
});
31+
32+
test('it renders blade file as resource', function (): void {
33+
$resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath);
34+
35+
$response = $resource->handle();
36+
37+
expect($response)->isToolResult()
38+
->toolHasNoError()
39+
->toolTextContains('Test Resource Guideline')
40+
->toolTextContains('Follow best practices')
41+
->toolTextContains('Write clean code');
42+
});
43+
44+
test('it generates correct resource uri from package', function (): void {
45+
$resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath);
46+
47+
expect($resource->uri())->toBe('file://instructions/acme/payments.md');
48+
});
49+
50+
test('it generates correct description from package', function (): void {
51+
$resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath);
52+
53+
expect($resource->description())->toBe('Guidelines for acme/payments');
54+
});
55+
56+
test('it has correct mime type', function (): void {
57+
$resource = new ThirdPartyResource($this->guidelineAssist, 'acme/payments', $this->testBladePath);
58+
59+
expect($resource->mimeType())->toBe('text/markdown');
60+
});
61+
62+
test('it handles non-existent blade file gracefully', function (): void {
63+
$resource = new ThirdPartyResource($this->guidelineAssist, 'acme/test', '/non/existent/path.blade.php');
64+
65+
$response = $resource->handle();
66+
67+
expect($response)->isToolResult()
68+
->toolHasNoError();
69+
70+
expect((string) $response->content())->toBe('');
71+
});
72+
73+
test('it processes backticks in blade content', function (): void {
74+
$bladeContent = '# Guideline
75+
76+
Use `Model::factory()` to create models.
77+
78+
```php
79+
User::factory()->create();
80+
```';
81+
82+
file_put_contents($this->testBladePath, $bladeContent);
83+
84+
$resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath);
85+
$response = $resource->handle();
86+
87+
expect($response)->isToolResult()
88+
->toolTextContains('`Model::factory()`')
89+
->toolTextContains('```php');
90+
});
91+
92+
test('it processes php tags in blade content', function (): void {
93+
$bladeContent = '# Guideline
94+
95+
Example code:
96+
97+
<?php
98+
echo "Hello World";
99+
?>';
100+
101+
file_put_contents($this->testBladePath, $bladeContent);
102+
103+
$resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath);
104+
$response = $resource->handle();
105+
106+
expect($response)->isToolResult()
107+
->toolTextContains('<?php')
108+
->toolTextContains('echo "Hello World"');
109+
});
110+
111+
test('it processes boost snippets', function (): void {
112+
$bladeContent = '# Guideline
113+
114+
@boostsnippet(\'example\', \'php\')
115+
function example() {
116+
return true;
117+
}
118+
@endboostsnippet';
119+
120+
file_put_contents($this->testBladePath, $bladeContent);
121+
122+
$resource = new ThirdPartyResource($this->guidelineAssist, 'test/package', $this->testBladePath);
123+
$response = $resource->handle();
124+
125+
expect($response)->isToolResult()
126+
->toolTextContains('<code-snippet name="example" lang="php">')
127+
->toolTextContains('function example()');
128+
});

0 commit comments

Comments
 (0)