Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions src/BoostServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
46 changes: 3 additions & 43 deletions src/Install/GuidelineComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +18,8 @@

class GuidelineComposer
{
use RendersBladeGuidelines;

protected string $userGuidelineDir = '.ai/guidelines';

/** @var Collection<string, array> */
Expand Down Expand Up @@ -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___',
'<?php' => '___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}
*/
Expand Down Expand Up @@ -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('/(?<!@)@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 {
$name = $matches['name'];
$lang = empty($matches['lang']) ? 'html' : $matches['lang'];
$snippetContent = $matches['content'];

$placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___';

$this->storedSnippets[$placeholder] = '<code-snippet name="'.$name.'" lang="'.$lang.'">'."\n".$snippetContent."\n".'</code-snippet>'."\n\n";

return $placeholder;
}, $content);
}

protected function getGuidelineAssist(): GuidelineAssist
{
return new GuidelineAssist($this->roster, $this->config);
Expand Down
77 changes: 57 additions & 20 deletions src/Mcp/Boost.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

namespace Laravel\Boost\Mcp;

use InvalidArgumentException;
use Laravel\Boost\Mcp\Methods\CallToolWithExecutor;
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;
Expand All @@ -21,6 +24,7 @@
use Laravel\Boost\Mcp\Tools\ReportFeedback;
use Laravel\Boost\Mcp\Tools\SearchDocs;
use Laravel\Boost\Mcp\Tools\Tinker;
use Laravel\Boost\Support\Composer;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
Expand Down Expand Up @@ -79,28 +83,12 @@ protected function boot(): void
$this->methods['tools/call'] = CallToolWithExecutor::class;
}

/**
* @param array<int, class-string> $availablePrimitives
* @return array<int, class-string>
*/
private function discoverPrimitives(array $availablePrimitives, string $type): array
{
return collect($availablePrimitives)
->diff(config("boost.mcp.{$type}.exclude", []))
->merge(
collect(config("boost.mcp.{$type}.include", []))
->filter(fn (string $class): bool => class_exists($class))
)
->values()
->all();
}

/**
* @return array<int, class-string<Tool>>
*/
protected function discoverTools(): array
{
return $this->discoverPrimitives([
return $this->filterPrimitives([
ApplicationInfo::class,
BrowserLogs::class,
DatabaseConnections::class,
Expand All @@ -125,16 +113,65 @@ protected function discoverTools(): array
*/
protected function discoverResources(): array
{
return $this->discoverPrimitives([
$availableResources = [
Resources\ApplicationInfo::class,
], 'resources');
...$this->discoverThirdPartyPrimitives(Resource::class),
];

return $this->filterPrimitives($availableResources, 'resources');
}

/**
* @return array<int, class-string<Prompt>>
*/
protected function discoverPrompts(): array
{
return $this->discoverPrimitives([], 'prompts');
return $this->filterPrimitives(
$this->discoverThirdPartyPrimitives(Prompt::class),
'prompts'
);
}

/**
* @template T of Prompt|Resource
*
* @param class-string<T> $primitiveType
* @return array<int, T>
*/
private function discoverThirdPartyPrimitives(string $primitiveType): array
{
$primitiveClass = match ($primitiveType) {
Prompt::class => PackageGuidelinePrompt::class,
Resource::class => PackageGuidelineResource::class,
default => throw new InvalidArgumentException('Invalid Primitive Type'),
};

$primitives = [];

foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) {
$corePath = $path.DIRECTORY_SEPARATOR.'core.blade.php';

if (file_exists($corePath)) {
$primitives[] = new $primitiveClass($package, $corePath);
}
}

return $primitives;
}

/**
* @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
{
return collect($availablePrimitives)
->diff(config("boost.mcp.{$type}.exclude", []))
->merge(
collect(config("boost.mcp.{$type}.include", []))
->filter(fn (string $class): bool => class_exists($class))
)
->values()
->all();
}
}
76 changes: 76 additions & 0 deletions src/Mcp/Prompts/Concerns/RendersBladeGuidelines.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost\Mcp\Prompts\Concerns;

use Illuminate\Support\Facades\Blade;
use Laravel\Boost\Install\GuidelineAssist;

trait RendersBladeGuidelines
{
private array $storedSnippets = [];

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___',
'<?php' => '___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('/(?<!@)@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 {
$name = $matches['name'];
$lang = empty($matches['lang']) ? 'html' : $matches['lang'];
$snippetContent = $matches['content'];

$placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___';

$this->storedSnippets[$placeholder] = '<code-snippet name="'.$name.'" lang="'.$lang.'">'."\n".$snippetContent."\n".'</code-snippet>'."\n\n";

return $placeholder;
}, $content);
}

protected function renderGuidelineFile(string $bladePath): string
{
if (! file_exists($bladePath)) {
return '';
}

$content = file_get_contents($bladePath);
$content = $this->processBoostSnippets($content);

$rendered = $this->renderContent($content, $bladePath);

$rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered);

$this->storedSnippets = [];

return $rendered;
}

protected function getGuidelineAssist(): GuidelineAssist
{
return app(GuidelineAssist::class);
}
}
30 changes: 30 additions & 0 deletions src/Mcp/Prompts/PackageGuidelinePrompt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost\Mcp\Prompts;

use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class PackageGuidelinePrompt extends Prompt
{
use RendersBladeGuidelines;

public function __construct(
protected string $packageName,
protected string $bladePath,
) {
$this->name = $this->packageName;
$this->title = $this->packageName;
$this->description = "Guidelines for {$packageName}";
}

public function handle(): Response
{
$content = $this->renderGuidelineFile($this->bladePath);

return Response::text($content);
}
}
32 changes: 32 additions & 0 deletions src/Mcp/Resources/PackageGuidelineResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost\Mcp\Resources;

use Laravel\Boost\Mcp\Prompts\Concerns\RendersBladeGuidelines;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;

class PackageGuidelineResource extends Resource
{
use RendersBladeGuidelines;

public function __construct(
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';
}

public function handle(): Response
{
$content = $this->renderGuidelineFile($this->bladePath);

return Response::text($content);
}
}
Loading