Skip to content
26 changes: 13 additions & 13 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@
},
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.9",
"illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"laravel/mcp": "^0.1.1",
"laravel/prompts": "^0.1.9|^0.3",
"laravel/roster": "^0.2.5"
"guzzlehttp/guzzle": "^7.10",
"illuminate/console": "^10.49.0|^11.45.3|^12.28.1",
"illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1",
"illuminate/routing": "^10.49.0|^11.45.3|^12.28.1",
"illuminate/support": "^10.49.0|^11.45.3|^12.28.1",
"laravel/mcp": "dev-main",
"laravel/prompts": "0.1.25|^0.3.6",
"laravel/roster": "^0.2.6"
},
"require-dev": {
"laravel/pint": "^1.14",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^8.22.0|^9.0|^10.0",
"pestphp/pest": "^2.0|^3.0",
"phpstan/phpstan": "^2.0"
"laravel/pint": "1.20",
"mockery/mockery": "^1.6.12",
"orchestra/testbench": "^8.36.0|^9.15.0|^10.6",
"pestphp/pest": "^2.36.0|^3.8.4",
"phpstan/phpstan": "^2.1.27"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion src/BoostServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use Illuminate\View\Compilers\BladeCompiler;
use Laravel\Boost\Mcp\Boost;
use Laravel\Boost\Middleware\InjectBoost;
use Laravel\Mcp\Server\Facades\Mcp;
use Laravel\Mcp\Facades\Mcp;
use Laravel\Roster\Roster;

class BoostServiceProvider extends ServiceProvider
Expand Down
43 changes: 29 additions & 14 deletions src/Console/ExecuteToolCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use Illuminate\Console\Command;
use Laravel\Boost\Mcp\ToolRegistry;
use Laravel\Mcp\Server\Tools\ToolResult;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
use Throwable;

class ExecuteToolCommand extends Command
{
Expand Down Expand Up @@ -36,22 +39,34 @@ public function handle(): int
return 1;
}

try {
// Execute the tool
$tool = app($toolClass);
$result = $tool->handle($arguments ?? []);
/** @var Tool $tool */
$tool = app($toolClass);

// Output the result as JSON for the parent process
echo json_encode($result->toArray());
$request = new Request($arguments ?? []);

return 0;
try {
/** @var Response $response */
$response = $tool->handle($request); // @phpstan-ignore-line
} catch (Throwable $e) {
$errorResult = Response::error("Tool execution failed (E_THROWABLE): {$e->getMessage()}");

$this->error(json_encode([
'isError' => true,
'content' => [
$errorResult->content()->toTool($tool),
],
]));

return static::FAILURE;
}

} catch (\Throwable $e) {
// Output error result
$errorResult = ToolResult::error("Tool execution failed (E_THROWABLE): {$e->getMessage()}");
$this->error(json_encode($errorResult->toArray()));
echo json_encode([
'isError' => $response->isError(),
'content' => [
$response->content()->toTool($tool),
],
]);

return 1;
}
return static::SUCCESS;
}
}
83 changes: 60 additions & 23 deletions src/Mcp/Boost.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,106 +10,143 @@

class Boost extends Server
{
public string $serverName = 'Laravel Boost';
/**
* The MCP server's name.
*/
protected string $name = 'Laravel Boost';

public string $serverVersion = '0.0.1';
/**
* The MCP server's version.
*/
protected string $version = '0.0.1';

public string $instructions = 'Laravel ecosystem MCP server offering database schema access, Artisan commands, error logs, Tinker execution, semantic documentation search and more. Boost helps with code generation.';
/**
* The MCP server's instructions for the LLM.
*/
protected string $instructions = 'Laravel ecosystem MCP server offering database schema access, Artisan commands, error logs, Tinker execution, semantic documentation search and more. Boost helps with code generation.';

/**
* The default pagination length for resources that support pagination.
*/
public int $defaultPaginationLength = 50;

/**
* @var string[]
* The tools registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Tool>>
*/
protected array $tools = [];

/**
* The resources registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Resource>>
*/
public array $resources = [
protected array $resources = [
ApplicationInfo::class,
];

/**
* The prompts registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Prompt>>
*/
protected array $prompts = [];

public function boot(): void
{
$this->discoverTools();
$this->discoverResources();
$this->discoverPrompts();
collect($this->discoverTools())->each(fn (string $tool) => $this->tools[] = $tool);
collect($this->discoverResources())->each(fn (string $resource) => $this->resources[] = $resource);
collect($this->discoverPrompts())->each(fn (string $prompt) => $this->prompts[] = $prompt);

// Override the tools/call method to use our ToolExecutor
$this->methods['tools/call'] = CallToolWithExecutor::class;
}

/**
* @return array<string>
* @return array<int, class-string<\Laravel\Mcp\Server\Tool>>
*/
protected function discoverTools(): array
{
$tools = [];

$excludedTools = config('boost.mcp.tools.exclude', []);
$toolDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Tools');

foreach ($toolDir as $toolFile) {
if ($toolFile->isFile() && $toolFile->getExtension() === 'php') {
$fqdn = 'Laravel\\Boost\\Mcp\\Tools\\'.$toolFile->getBasename('.php');
if (class_exists($fqdn) && ! in_array($fqdn, $excludedTools, true)) {
$this->addTool($fqdn);
$tools[] = $fqdn;
}
}
}

$extraTools = config('boost.mcp.tools.include', []);
foreach ($extraTools as $toolClass) {
if (class_exists($toolClass)) {
$this->addTool($toolClass);
$tools[] = $toolClass;
}
}

return $this->registeredTools;
return $tools;
}

/**
* @return array<string>
* @return array<int, class-string<\Laravel\Mcp\Server\Resource>>
*/
protected function discoverResources(): array
{
$resources = [];

$excludedResources = config('boost.mcp.resources.exclude', []);
$resourceDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Resources');

foreach ($resourceDir as $resourceFile) {
if ($resourceFile->isFile() && $resourceFile->getExtension() === 'php') {
$fqdn = 'Laravel\\Boost\\Mcp\\Resources\\'.$resourceFile;
if (class_exists($fqdn) && ! in_array($fqdn, $excludedResources, true)) {
$this->addResource($fqdn);
$fqdn = 'Laravel\\Boost\\Mcp\\Resources\\'.$resourceFile->getBasename('.php');
if (class_exists($fqdn) && ! in_array($fqdn, $excludedResources, true) && $fqdn !== ApplicationInfo::class) {
$resources[] = $fqdn;
}
}
}

$extraResources = config('boost.mcp.resources.include', []);
foreach ($extraResources as $resourceClass) {
if (class_exists($resourceClass)) {
$this->addResource($resourceClass);
$resources[] = $resourceClass;
}
}

return $this->registeredResources;
return $resources;
}

/**
* @return array<string>
* @return array<int, class-string<\Laravel\Mcp\Server\Prompt>>
*/
protected function discoverPrompts(): array
{
$prompts = [];

$excludedPrompts = config('boost.mcp.prompts.exclude', []);
$promptDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Prompts');

foreach ($promptDir as $promptFile) {
if ($promptFile->isFile() && $promptFile->getExtension() === 'php') {
$fqdn = 'Laravel\\Boost\\Mcp\\Prompts\\'.$promptFile;
$fqdn = 'Laravel\\Boost\\Mcp\\Prompts\\'.$promptFile->getBasename('.php');
if (class_exists($fqdn) && ! in_array($fqdn, $excludedPrompts, true)) {
$this->addPrompt($fqdn);
$prompts[] = $fqdn;
}
}
}

$extraPrompts = config('boost.mcp.prompts.include', []);
foreach ($extraPrompts as $promptClass) {
if (class_exists($promptClass)) {
$this->addResource($promptClass);
$prompts[] = $promptClass;
}
}

return $this->registeredPrompts;
return $prompts;
}
}
70 changes: 38 additions & 32 deletions src/Mcp/Methods/CallToolWithExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,63 @@

namespace Laravel\Boost\Mcp\Methods;

use Illuminate\Support\ItemNotFoundException;
use Laravel\Boost\Mcp\ToolExecutor;
use Laravel\Mcp\Server\Contracts\Methods\Method;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Contracts\Errable;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Tools\ToolResult;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use Throwable;

class CallToolWithExecutor implements Method
class CallToolWithExecutor implements Method, Errable
{
use InteractsWithResponses;

public function __construct(protected ToolExecutor $executor)
{
//
}

/**
* Handle the JSON-RPC tool/call request with process isolation.
*
* @param JsonRpcRequest $request
* @param ServerContext $context
* @return JsonRpcResponse
*/
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
try {
$tool = $context->tools()->firstOrFail(fn ($tool) => $tool->name() === $request->params['name']);
} catch (ItemNotFoundException) {
return JsonRpcResponse::create(
$request->id,
ToolResult::error('Tool not found')
);
} catch (Throwable $e) {
return JsonRpcResponse::create(
if (is_null($request->get('name'))) {
throw new JsonRpcException(
'Missing [name] parameter.',
-32602,
$request->id,
ToolResult::error('Error finding tool: '.$e->getMessage())
);
}

try {
$executor = app(ToolExecutor::class);

$arguments = [];
if (isset($request->params['arguments']) && is_array($request->params['arguments'])) {
$arguments = $request->params['arguments'];
}
$tool = $context
->tools($request->toRequest())
->first(
fn ($tool): bool => $tool->name() === $request->params['name'],
fn () => throw new JsonRpcException(
"Tool [{$request->params['name']}] not found.",
-32602,
$request->id,
));

$result = $executor->execute(get_class($tool), $arguments);

return JsonRpcResponse::create($request->id, $result);
$arguments = [];
if (isset($request->params['arguments']) && is_array($request->params['arguments'])) {
$arguments = $request->params['arguments'];
}

try {
$response = $this->executor->execute(get_class($tool), $arguments);
} catch (Throwable $e) {
return JsonRpcResponse::create(
$request->id,
ToolResult::error('Tool execution error: '.$e->getMessage())
);
$response = Response::error('Tool execution error: '.$e->getMessage());
}

return $this->toJsonRpcResponse($request, $response, fn ($responses) => [
'content' => $responses->map(fn ($response) => $response->content()->toTool($tool))->all(),
'isError' => $responses->contains(fn ($response) => $response->isError()),
]);
}
}
Loading
Loading