Skip to content

Commit ef7af75

Browse files
authored
Add Resource and Prompts for Package Guidelines (#389)
* WIP Signed-off-by: Pushpak Chhajed <[email protected]> * Add ThirdPartyResource and ThirdPartyPrompt classes Signed-off-by: Pushpak Chhajed <[email protected]> * Refactor test Signed-off-by: Pushpak Chhajed <[email protected]> * Formatting Signed-off-by: Pushpak Chhajed <[email protected]> * Formatting Signed-off-by: Pushpak Chhajed <[email protected]> * Formatting Signed-off-by: Pushpak Chhajed <[email protected]> * Formatting Signed-off-by: Pushpak Chhajed <[email protected]> --------- Signed-off-by: Pushpak Chhajed <[email protected]>
1 parent b87236e commit ef7af75

File tree

8 files changed

+483
-63
lines changed

8 files changed

+483
-63
lines changed

src/BoostServiceProvider.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Illuminate\Support\Facades\Route;
1313
use Illuminate\Support\ServiceProvider;
1414
use Illuminate\View\Compilers\BladeCompiler;
15+
use Laravel\Boost\Install\GuidelineAssist;
16+
use Laravel\Boost\Install\GuidelineConfig;
1517
use Laravel\Boost\Mcp\Boost;
1618
use Laravel\Boost\Middleware\InjectBoost;
1719
use Laravel\Mcp\Facades\Mcp;
@@ -58,6 +60,13 @@ public function register(): void
5860

5961
return $roster;
6062
});
63+
64+
$this->app->singleton(GuidelineConfig::class, fn (): GuidelineConfig => new GuidelineConfig);
65+
66+
$this->app->singleton(GuidelineAssist::class, fn ($app): GuidelineAssist => new GuidelineAssist(
67+
$app->make(Roster::class),
68+
$app->make(GuidelineConfig::class)
69+
));
6170
}
6271

6372
public function boot(Router $router): void

src/Install/GuidelineComposer.php

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace Laravel\Boost\Install;
66

77
use Illuminate\Support\Collection;
8-
use Illuminate\Support\Facades\Blade;
98
use Illuminate\Support\Str;
9+
use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
1010
use Laravel\Boost\Support\Composer;
1111
use Laravel\Roster\Enums\Packages;
1212
use Laravel\Roster\Package;
@@ -18,6 +18,8 @@
1818

1919
class GuidelineComposer
2020
{
21+
use RendersBladeGuidelines;
22+
2123
protected string $userGuidelineDir = '.ai/guidelines';
2224

2325
/** @var Collection<string, array> */
@@ -276,31 +278,6 @@ protected function guidelinesDir(string $dirPath, bool $thirdParty = false): arr
276278
->all();
277279
}
278280

279-
protected function renderContent(string $content, string $path): string
280-
{
281-
$isBladeTemplate = str_ends_with($path, '.blade.php');
282-
283-
if (! $isBladeTemplate) {
284-
return $content;
285-
}
286-
287-
// Temporarily replace backticks and PHP opening tags with placeholders before Blade processing
288-
// This prevents Blade from trying to execute PHP code examples and supports inline code
289-
$placeholders = [
290-
'`' => '___SINGLE_BACKTICK___',
291-
'<?php' => '___OPEN_PHP_TAG___',
292-
'@volt' => '___VOLT_DIRECTIVE___',
293-
'@endvolt' => '___ENDVOLT_DIRECTIVE___',
294-
];
295-
296-
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
297-
$rendered = Blade::render($content, [
298-
'assist' => $this->getGuidelineAssist(),
299-
]);
300-
301-
return str_replace(array_values($placeholders), array_keys($placeholders), $rendered);
302-
}
303-
304281
/**
305282
* @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool}
306283
*/
@@ -346,23 +323,6 @@ protected function guideline(string $path, bool $thirdParty = false): array
346323
];
347324
}
348325

349-
private array $storedSnippets = [];
350-
351-
protected function processBoostSnippets(string $content): string
352-
{
353-
return preg_replace_callback('/(?<!@)@boostsnippet\(\s*(?P<nameQuote>[\'"])(?P<name>[^\1]*?)\1(?:\s*,\s*(?P<langQuote>[\'"])(?P<lang>[^\3]*?)\3)?\s*\)(?P<content>.*?)@endboostsnippet/s', function (array $matches): string {
354-
$name = $matches['name'];
355-
$lang = empty($matches['lang']) ? 'html' : $matches['lang'];
356-
$snippetContent = $matches['content'];
357-
358-
$placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___';
359-
360-
$this->storedSnippets[$placeholder] = '<code-snippet name="'.$name.'" lang="'.$lang.'">'."\n".$snippetContent."\n".'</code-snippet>'."\n\n";
361-
362-
return $placeholder;
363-
}, $content);
364-
}
365-
366326
protected function getGuidelineAssist(): GuidelineAssist
367327
{
368328
return new GuidelineAssist($this->roster, $this->config);

src/Mcp/Boost.php

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Laravel\Boost\Mcp;
66

7+
use InvalidArgumentException;
78
use Laravel\Boost\Mcp\Methods\CallToolWithExecutor;
9+
use Laravel\Boost\Mcp\Prompts\PackageGuidelinePrompt;
10+
use Laravel\Boost\Mcp\Resources\PackageGuidelineResource;
811
use Laravel\Boost\Mcp\Tools\ApplicationInfo;
912
use Laravel\Boost\Mcp\Tools\BrowserLogs;
1013
use Laravel\Boost\Mcp\Tools\DatabaseConnections;
@@ -21,6 +24,7 @@
2124
use Laravel\Boost\Mcp\Tools\ReportFeedback;
2225
use Laravel\Boost\Mcp\Tools\SearchDocs;
2326
use Laravel\Boost\Mcp\Tools\Tinker;
27+
use Laravel\Boost\Support\Composer;
2428
use Laravel\Mcp\Server;
2529
use Laravel\Mcp\Server\Prompt;
2630
use Laravel\Mcp\Server\Resource;
@@ -79,28 +83,12 @@ protected function boot(): void
7983
$this->methods['tools/call'] = CallToolWithExecutor::class;
8084
}
8185

82-
/**
83-
* @param array<int, class-string> $availablePrimitives
84-
* @return array<int, class-string>
85-
*/
86-
private function discoverPrimitives(array $availablePrimitives, string $type): array
87-
{
88-
return collect($availablePrimitives)
89-
->diff(config("boost.mcp.{$type}.exclude", []))
90-
->merge(
91-
collect(config("boost.mcp.{$type}.include", []))
92-
->filter(fn (string $class): bool => class_exists($class))
93-
)
94-
->values()
95-
->all();
96-
}
97-
9886
/**
9987
* @return array<int, class-string<Tool>>
10088
*/
10189
protected function discoverTools(): array
10290
{
103-
return $this->discoverPrimitives([
91+
return $this->filterPrimitives([
10492
ApplicationInfo::class,
10593
BrowserLogs::class,
10694
DatabaseConnections::class,
@@ -125,16 +113,65 @@ protected function discoverTools(): array
125113
*/
126114
protected function discoverResources(): array
127115
{
128-
return $this->discoverPrimitives([
116+
$availableResources = [
129117
Resources\ApplicationInfo::class,
130-
], 'resources');
118+
...$this->discoverThirdPartyPrimitives(Resource::class),
119+
];
120+
121+
return $this->filterPrimitives($availableResources, 'resources');
131122
}
132123

133124
/**
134125
* @return array<int, class-string<Prompt>>
135126
*/
136127
protected function discoverPrompts(): array
137128
{
138-
return $this->discoverPrimitives([], 'prompts');
129+
return $this->filterPrimitives(
130+
$this->discoverThirdPartyPrimitives(Prompt::class),
131+
'prompts'
132+
);
133+
}
134+
135+
/**
136+
* @template T of Prompt|Resource
137+
*
138+
* @param class-string<T> $primitiveType
139+
* @return array<int, T>
140+
*/
141+
private function discoverThirdPartyPrimitives(string $primitiveType): array
142+
{
143+
$primitiveClass = match ($primitiveType) {
144+
Prompt::class => PackageGuidelinePrompt::class,
145+
Resource::class => PackageGuidelineResource::class,
146+
default => throw new InvalidArgumentException('Invalid Primitive Type'),
147+
};
148+
149+
$primitives = [];
150+
151+
foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) {
152+
$corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php';
153+
154+
if (file_exists($corePath)) {
155+
$primitives[] = new $primitiveClass($package, $corePath);
156+
}
157+
}
158+
159+
return $primitives;
160+
}
161+
162+
/**
163+
* @param array<int, Tool|Resource|Prompt|class-string> $availablePrimitives
164+
* @return array<int, Tool|Resource|Prompt|class-string>
165+
*/
166+
private function filterPrimitives(array $availablePrimitives, string $type): array
167+
{
168+
return collect($availablePrimitives)
169+
->diff(config("boost.mcp.{$type}.exclude", []))
170+
->merge(
171+
collect(config("boost.mcp.{$type}.include", []))
172+
->filter(fn (string $class): bool => class_exists($class))
173+
)
174+
->values()
175+
->all();
139176
}
140177
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Mcp\Prompts\Concerns;
6+
7+
use Illuminate\Support\Facades\Blade;
8+
use Laravel\Boost\Install\GuidelineAssist;
9+
10+
trait RendersBladeGuidelines
11+
{
12+
private array $storedSnippets = [];
13+
14+
protected function renderContent(string $content, string $path): string
15+
{
16+
$isBladeTemplate = str_ends_with($path, '.blade.php');
17+
18+
if (! $isBladeTemplate) {
19+
return $content;
20+
}
21+
22+
// Temporarily replace backticks and PHP opening tags with placeholders before Blade processing
23+
// This prevents Blade from trying to execute PHP code examples and supports inline code
24+
$placeholders = [
25+
'`' => '___SINGLE_BACKTICK___',
26+
'<?php' => '___OPEN_PHP_TAG___',
27+
'@volt' => '___VOLT_DIRECTIVE___',
28+
'@endvolt' => '___ENDVOLT_DIRECTIVE___',
29+
];
30+
31+
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
32+
$rendered = Blade::render($content, [
33+
'assist' => $this->getGuidelineAssist(),
34+
]);
35+
36+
return str_replace(array_values($placeholders), array_keys($placeholders), $rendered);
37+
}
38+
39+
protected function processBoostSnippets(string $content): string
40+
{
41+
return preg_replace_callback('/(?<!@)@boostsnippet\(\s*(?P<nameQuote>[\'"])(?P<name>[^\1]*?)\1(?:\s*,\s*(?P<langQuote>[\'"])(?P<lang>[^\3]*?)\3)?\s*\)(?P<content>.*?)@endboostsnippet/s', function (array $matches): string {
42+
$name = $matches['name'];
43+
$lang = empty($matches['lang']) ? 'html' : $matches['lang'];
44+
$snippetContent = $matches['content'];
45+
46+
$placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___';
47+
48+
$this->storedSnippets[$placeholder] = '<code-snippet name="'.$name.'" lang="'.$lang.'">'."\n".$snippetContent."\n".'</code-snippet>'."\n\n";
49+
50+
return $placeholder;
51+
}, $content);
52+
}
53+
54+
protected function renderGuidelineFile(string $bladePath): string
55+
{
56+
if (! file_exists($bladePath)) {
57+
return '';
58+
}
59+
60+
$content = file_get_contents($bladePath);
61+
$content = $this->processBoostSnippets($content);
62+
63+
$rendered = $this->renderContent($content, $bladePath);
64+
65+
$rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered);
66+
67+
$this->storedSnippets = [];
68+
69+
return $rendered;
70+
}
71+
72+
protected function getGuidelineAssist(): GuidelineAssist
73+
{
74+
return app(GuidelineAssist::class);
75+
}
76+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Mcp\Prompts;
6+
7+
use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
8+
use Laravel\Mcp\Response;
9+
use Laravel\Mcp\Server\Prompt;
10+
11+
class PackageGuidelinePrompt extends Prompt
12+
{
13+
use RendersBladeGuidelines;
14+
15+
public function __construct(
16+
protected string $packageName,
17+
protected string $bladePath,
18+
) {
19+
$this->name = $this->packageName;
20+
$this->title = $this->packageName;
21+
$this->description = "Guidelines for {$packageName}";
22+
}
23+
24+
public function handle(): Response
25+
{
26+
$content = $this->renderGuidelineFile($this->bladePath);
27+
28+
return Response::text($content);
29+
}
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Mcp\Resources;
6+
7+
use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
8+
use Laravel\Mcp\Response;
9+
use Laravel\Mcp\Server\Resource;
10+
11+
class PackageGuidelineResource extends Resource
12+
{
13+
use RendersBladeGuidelines;
14+
15+
public function __construct(
16+
protected string $packageName,
17+
protected string $bladePath,
18+
) {
19+
$this->name = $this->packageName;
20+
$this->title = $this->packageName;
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->renderGuidelineFile($this->bladePath);
29+
30+
return Response::text($content);
31+
}
32+
}

0 commit comments

Comments
 (0)