Skip to content

Commit bc7372a

Browse files
committed
feat: add horrible but working MCP installation
1 parent 90d715d commit bc7372a

File tree

13 files changed

+235
-70
lines changed

13 files changed

+235
-70
lines changed

.ai/enforce-tests.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
- Every new feature must be tested. Write a new test, or update an existing test, then run the tests to make sure they pass.
1+
- Every change must be programmatically tested. Write a new test, or update an existing test, then run the tests to make sure they pass.
2+
- Run the individual tests edited to

.ai/inertia-react/1/.gitkeep

Whitespace-only changes.

.ai/inertia-react/2/.gitkeep

Whitespace-only changes.

.ai/inertia-react/core.md

Whitespace-only changes.

.ai/tailwindcss/3/.gitkeep

Whitespace-only changes.

.ai/tailwindcss/4/.gitkeep

Whitespace-only changes.

.ai/tailwindcss/core.md

Whitespace-only changes.

src/Console/InstallCommand.php

Lines changed: 159 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use function Laravel\Prompts\intro;
2020
use function Laravel\Prompts\multiselect;
21+
use function Laravel\Prompts\outro;
2122
use function Laravel\Prompts\select;
2223
use function Laravel\Prompts\text;
2324

@@ -39,7 +40,8 @@ class InstallCommand extends Command
3940

4041
protected bool $enforceTests = true;
4142

42-
protected array $idesToInstallTo = ['other'];
43+
/** @var Collection<int, \Laravel\Boost\Contracts\Ide> */
44+
protected Collection $idesToInstallTo;
4345

4446
protected array $boostToInstall = [];
4547

@@ -55,6 +57,7 @@ class InstallCommand extends Command
5557
public function handle(Roster $roster): void
5658
{
5759
$this->agentsToInstallTo = collect();
60+
$this->idesToInstallTo = collect();
5861
$this->roster = $roster;
5962
$this->colors = new class
6063
{
@@ -65,16 +68,18 @@ public function handle(Roster $roster): void
6568

6669
$this->intro();
6770
$this->detect();
68-
// TODO: We see these packages installed, we have rules for X, so we'll add them
6971
$this->query();
7072
$this->enact();
73+
$this->outro();
7174
}
7275

7376
protected function detect()
7477
{
7578
$this->installedIdes = $this->detectInstalledIdes();
7679
$this->detectedProjectIdes = $this->detectIdesUsedInProject();
7780
// $this->detectedProjectAgents = $this->detectProjectAgents(); // TODO: Roo, Cline, Copilot
81+
// TODO: Should we create all agents to start, add a 'detected' prop to them that's set on construct
82+
// Maybe add a trait 'DetectsInstalled' and 'DetectsUsed' (in this project)
7883
}
7984

8085
protected function query()
@@ -90,29 +95,26 @@ protected function query()
9095
$this->agentsToInstallTo = $this->agentsToInstallTo(); // AI Guidelines, which file do they go, are they separated, or all in one file?
9196
}
9297

93-
protected function enact()
98+
protected function enact(): void
9499
{
95100
if ($this->installingGuidelines() && ! empty($this->agentsToInstallTo)) {
96-
$this->enactGuidelines($this->compose());
101+
$this->enactGuidelines($this->findGuidelines());
97102
}
98103

99-
if ($this->installingMcp() && ! empty($this->idesToInstallTo)) {
100-
echo "\ninstalling mcps now to: ";
101-
dump($this->idesToInstallTo);
104+
if (($this->installingMcp() || $this->installingHerdMcp()) && $this->idesToInstallTo->isNotEmpty()) {
105+
$this->enactMcpServers();
102106
}
103107

104-
if ($this->installingHerdMcp() && ! empty($this->idesToInstallTo)) {
105-
echo "\ninstalling herd mcp now to: ";
106-
dump($this->idesToInstallTo);
107-
}
108+
// Check if any of the selected IDEs is an "other" type IDE
109+
$hasOtherIde = true;
108110

109-
if (in_array('other', $this->idesToInstallTo)) {
111+
if ($hasOtherIde) {
110112
$this->newLine();
111113
$this->line('Add to your mcp file: ./artisan boost:mcp'); // some ides require absolute
112114
}
113115
}
114116

115-
protected function compose(): string
117+
protected function findGuidelines(): Collection
116118
{
117119
// TODO: Just move to blade views and compact public properties?
118120
$composed = collect(['core' => $this->guideline('core.md', [
@@ -132,14 +134,11 @@ protected function compose(): string
132134
}
133135

134136
// Add all core.md and version specific docs for Roster supported packages
135-
// We don't add guidelines for packages not supported by Roster right now
137+
// We don't add guidelines for packages unsupported by Roster right now
136138
foreach ($this->roster->packages() as $package) {
137139
$guidelineDir = str_replace('_', '-', strtolower($package->name()));
138-
$coreGuidelines = $this->guideline($guidelineDir.'/core.md'); // Add core
139-
if ($coreGuidelines) {
140-
$composed->put($guidelineDir.'/core', $coreGuidelines);
141-
}
142140

141+
$composed->put($guidelineDir.'/core', $this->guideline($guidelineDir.'/core.md')); // Add core
143142
$composed->put(
144143
$guidelineDir.'/v'.$package->majorVersion(),
145144
$this->guidelines($guidelineDir.'/'.$package->majorVersion())
@@ -150,7 +149,14 @@ protected function compose(): string
150149
$composed->put('tests', $this->guideline('enforce-tests.md'));
151150
}
152151

153-
return $composed->whereNotNull()->map(fn ($content, $key) => "# {$key}\n{$content}\n")
152+
return $composed
153+
->whereNotNull();
154+
}
155+
156+
protected function compose(Collection $composed): string
157+
{
158+
return $composed
159+
->map(fn ($content, $key) => "# {$key}\n{$content}\n")
154160
->join("\n\n====\n\n");
155161
}
156162

@@ -240,7 +246,7 @@ protected function detectIdesUsedInProject(): array
240246
}
241247

242248
if (file_exists(base_path('CLAUDE.md')) || is_dir(base_path('.claude'))) {
243-
$detected[] = 'claude_code';
249+
$detected[] = 'claudecode';
244250
}
245251

246252
$detected[] = 'other';
@@ -294,14 +300,19 @@ protected function isHerdInstalled(): bool
294300
}
295301

296302
protected function isHerdMCPAvailable(): bool
303+
{
304+
return file_exists($this->herdMcpPath());
305+
}
306+
307+
protected function herdMcpPath(): string
297308
{
298309
$isWindows = PHP_OS_FAMILY === 'Windows';
299310

300311
if ($isWindows) {
301-
return file_exists($this->getHomePath().'/.config/herd/bin/herd-mcp.phar');
312+
return $this->getHomePath().'/.config/herd/bin/herd-mcp.phar';
302313
}
303314

304-
return file_exists($this->getHomePath().'/Library/Application Support/Herd/bin/herd-mcp.phar');
315+
return $this->getHomePath().'/Library/Application Support/Herd/bin/herd-mcp.phar';
305316
}
306317

307318
/*
@@ -331,6 +342,11 @@ private function intro()
331342
$this->line(' Let\'s give '.$this->colors->bgYellow($this->colors->black($this->projectName)).' a Boost');
332343
}
333344

345+
private function outro()
346+
{
347+
outro('All done. Enjoy the boost 🚀');
348+
}
349+
334350
protected function projectPurpose(): string
335351
{
336352
return text(
@@ -365,30 +381,6 @@ protected function shouldEnforceTests(bool $ask = true): bool
365381
return $enforce;
366382
}
367383

368-
protected function idesToInstallTo(): array
369-
{
370-
// Limit our surface area for launch. We can support more after
371-
$ideOptions = [
372-
'claude_code' => 'Claude Code',
373-
'cursor' => 'Cursor',
374-
'phpstorm' => 'PHPStorm',
375-
'vscode' => 'VSCode',
376-
'other' => 'Other',
377-
];
378-
379-
// Tell API which ones?
380-
$autoDetectedIdesString = Arr::join(array_map(fn (string $ideKey) => $ideOptions[$ideKey] ?? '', $this->detectedProjectIdes), ', ', ' & ');
381-
382-
return multiselect(
383-
label: sprintf('Which IDEs do you use in %s? (space to select)', $this->projectName),
384-
options: $ideOptions,
385-
default: $this->detectedProjectIdes,
386-
scroll: 5,
387-
required: true,
388-
hint: sprintf('Auto-detected %s for you', $autoDetectedIdesString)
389-
);
390-
}
391-
392384
protected function boostToInstall(): array
393385
{
394386
$defaultToInstallOptions = ['mcp_server', 'ai_guidelines'];
@@ -427,6 +419,61 @@ protected function detectProjectAgents(): array
427419
return [];
428420
}
429421

422+
/**
423+
* @return Collection<int, \Laravel\Boost\Contracts\Ide>
424+
*/
425+
protected function idesToInstallTo(): Collection
426+
{
427+
$ides = [];
428+
if (! $this->installingMcp() && ! $this->installingHerdMcp()) {
429+
return collect();
430+
}
431+
432+
$agentDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Install', 'Agents']);
433+
434+
$finder = Finder::create()
435+
->in($agentDir)
436+
->files()
437+
->name('*.php');
438+
439+
foreach ($finder as $ideFile) {
440+
$className = 'Laravel\\Boost\\Install\\Agents\\'.$ideFile->getBasename('.php');
441+
442+
if (class_exists($className)) {
443+
$reflection = new \ReflectionClass($className);
444+
445+
if ($reflection->implementsInterface(\Laravel\Boost\Contracts\Ide::class) && ! $reflection->isAbstract()) {
446+
$ides[$className] = Str::headline($ideFile->getBasename('.php'));
447+
}
448+
}
449+
}
450+
451+
ksort($ides);
452+
// $ides['other'] = 'Other'; // TODO: Make 'Other' work now we are working with classes not strings
453+
454+
// Map detected IDE keys to class names
455+
$detectedClasses = [];
456+
foreach ($this->detectedProjectIdes as $ideKey) {
457+
foreach ($ides as $className => $displayName) {
458+
if (strtolower($ideKey) === strtolower(class_basename($className))) {
459+
$detectedClasses[] = $className;
460+
break;
461+
}
462+
}
463+
}
464+
465+
$selectedIdeClasses = collect(multiselect(
466+
label: sprintf('Which IDEs do you use in %s? (space to select)', $this->projectName),
467+
options: $ides,
468+
default: $detectedClasses,
469+
scroll: 5,
470+
required: true,
471+
hint: sprintf('Auto-detected %s for you', Arr::join(array_map(fn ($c) => class_basename($c), $detectedClasses), ', ', ' & '))
472+
));
473+
474+
return $selectedIdeClasses->map(fn ($ideClass) => new $ideClass);
475+
}
476+
430477
/**
431478
* @return Collection<int, \Laravel\Boost\Contracts\Agent>
432479
*/
@@ -468,7 +515,7 @@ protected function agentsToInstallTo(): Collection
468515
return $selectedAgentClasses->map(fn ($agentClass) => new $agentClass);
469516
}
470517

471-
protected function enactGuidelines(string $composedAiGuidelines): void
518+
protected function enactGuidelines(Collection $composed): void
472519
{
473520
if (! $this->installingGuidelines()) {
474521
return;
@@ -481,11 +528,13 @@ protected function enactGuidelines(string $composedAiGuidelines): void
481528
}
482529

483530
$this->newLine();
484-
$this->info('Installing AI guidelines to selected agents...');
531+
$this->info(sprintf('Found %d guidelines and adding to your selected agents', $composed->count()));
532+
$this->line($composed->keys()->join(', ', ' & '));
485533
$this->newLine();
486534

487535
$successful = [];
488536
$failed = [];
537+
$composedAiGuidelines = $this->compose($composed);
489538

490539
foreach ($this->agentsToInstallTo as $agent) {
491540
$agentName = class_basename($agent);
@@ -505,13 +554,6 @@ protected function enactGuidelines(string $composedAiGuidelines): void
505554

506555
$this->newLine();
507556

508-
if (count($successful) > 0) {
509-
$this->info(sprintf('✓ Successfully installed guidelines to %d agent%s',
510-
count($successful),
511-
count($successful) === 1 ? '' : 's'
512-
));
513-
}
514-
515557
if (count($failed) > 0) {
516558
$this->error(sprintf('✗ Failed to install guidelines to %d agent%s:',
517559
count($failed),
@@ -542,4 +584,66 @@ protected function installingHerdMcp(): bool
542584
{
543585
return in_array('herd_mcp', $this->boostToInstall, true);
544586
}
587+
588+
protected function enactMcpServers(): void
589+
{
590+
$this->newLine();
591+
$this->info('Installing MCP servers to your selected IDEs');
592+
$this->newLine();
593+
594+
$failed = [];
595+
596+
foreach ($this->idesToInstallTo as $ide) {
597+
$ideName = class_basename($ide);
598+
$this->output->write(" {$ideName}... ");
599+
$results = [];
600+
601+
// Install Laravel Boost MCP if enabled
602+
if ($this->installingMcp()) {
603+
try {
604+
$result = $ide->installMcp('laravel-boost', base_path('artisan'), ['boost:mcp']);
605+
606+
if ($result) {
607+
$results[] = '✓ Boost';
608+
} else {
609+
$results[] = '✗ Boost';
610+
$failed[$ideName]['boost'] = 'Failed to write configuration';
611+
}
612+
} catch (\Exception $e) {
613+
$results[] = '✗ Boost';
614+
$failed[$ideName]['boost'] = $e->getMessage();
615+
}
616+
}
617+
618+
// Install Herd MCP if enabled
619+
if ($this->installingHerdMcp()) {
620+
try {
621+
$result = $ide->installMcp('herd', PHP_BINARY, [$this->herdMcpPath()]);
622+
623+
if ($result) {
624+
$results[] = '✓ Herd';
625+
} else {
626+
$results[] = '✗ Herd';
627+
$failed[$ideName]['herd'] = 'Failed to write configuration';
628+
}
629+
} catch (\Exception $e) {
630+
$results[] = '✗ Herd';
631+
$failed[$ideName]['herd'] = $e->getMessage();
632+
}
633+
}
634+
635+
$this->line(implode(' ', $results));
636+
}
637+
638+
$this->newLine();
639+
640+
if (count($failed) > 0) {
641+
$this->error(sprintf('✗ Some MCP servers failed to install:'));
642+
foreach ($failed as $ideName => $errors) {
643+
foreach ($errors as $server => $error) {
644+
$this->line(" - {$ideName} ({$server}): {$error}");
645+
}
646+
}
647+
}
648+
}
545649
}

src/Install/Agents/ClaudeCode.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class ClaudeCode extends ShellMcpIde implements Agent
88
{
9-
protected string $shellCommand = 'claude mcp add laravel-boost {command} {args}';
9+
protected string $shellCommand = 'claude mcp add {key} {command} {args}';
1010

1111
public function guidelinesPath(): string
1212
{

0 commit comments

Comments
 (0)