diff --git a/src/Mcp/Methods/CallToolWithExecutor.php b/src/Mcp/Methods/CallToolWithExecutor.php index 54769ed..1d2baf5 100644 --- a/src/Mcp/Methods/CallToolWithExecutor.php +++ b/src/Mcp/Methods/CallToolWithExecutor.php @@ -4,34 +4,34 @@ namespace Laravel\Boost\Mcp\Methods; -use Generator; use Illuminate\Support\ItemNotFoundException; use Laravel\Boost\Mcp\ToolExecutor; use Laravel\Mcp\Server\Contracts\Methods\Method; use Laravel\Mcp\Server\ServerContext; use Laravel\Mcp\Server\Tools\ToolResult; -use Laravel\Mcp\Server\Transport\JsonRpcNotification; use Laravel\Mcp\Server\Transport\JsonRpcRequest; use Laravel\Mcp\Server\Transport\JsonRpcResponse; +use Throwable; class CallToolWithExecutor implements Method { /** * Handle the JSON-RPC tool/call request with process isolation. * - * @return JsonRpcResponse|Generator + * @param JsonRpcRequest $request + * @param ServerContext $context + * @return JsonRpcResponse */ - public function handle(JsonRpcRequest $request, ServerContext $context) + public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse { try { - $tool = $context->tools() - ->firstOrFail(fn ($tool) => $tool->name() === $request->params['name']); - } catch (ItemNotFoundException $e) { + $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) { + } catch (Throwable $e) { return JsonRpcResponse::create( $request->id, ToolResult::error('Error finding tool: '.$e->getMessage()) @@ -39,10 +39,8 @@ public function handle(JsonRpcRequest $request, ServerContext $context) } try { - // Use ToolExecutor instead of calling tool directly $executor = app(ToolExecutor::class); - // Safely get arguments $arguments = []; if (isset($request->params['arguments']) && is_array($request->params['arguments'])) { $arguments = $request->params['arguments']; @@ -52,7 +50,7 @@ public function handle(JsonRpcRequest $request, ServerContext $context) return JsonRpcResponse::create($request->id, $result); - } catch (\Throwable $e) { + } catch (Throwable $e) { return JsonRpcResponse::create( $request->id, ToolResult::error('Tool execution error: '.$e->getMessage()) diff --git a/src/Mcp/Tools/BrowserLogs.php b/src/Mcp/Tools/BrowserLogs.php index 423ec05..205715a 100644 --- a/src/Mcp/Tools/BrowserLogs.php +++ b/src/Mcp/Tools/BrowserLogs.php @@ -4,14 +4,13 @@ namespace Laravel\Boost\Mcp\Tools; -use Illuminate\Support\Facades\Log; use Laravel\Boost\Concerns\ReadsLogs; use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class BrowserLogs extends Tool { use ReadsLogs; @@ -41,7 +40,7 @@ public function handle(array $arguments): ToolResult return ToolResult::error('The "entries" argument must be greater than 0.'); } - // Locate the correct log file using shared helper. + // Locate the correct log file using the shared helper. $logFile = storage_path('logs/browser.log'); if (! file_exists($logFile)) { diff --git a/src/Mcp/Tools/DatabaseConnections.php b/src/Mcp/Tools/DatabaseConnections.php index 050e5c3..6217662 100644 --- a/src/Mcp/Tools/DatabaseConnections.php +++ b/src/Mcp/Tools/DatabaseConnections.php @@ -9,7 +9,7 @@ use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class DatabaseConnections extends Tool { public function description(): string @@ -19,7 +19,6 @@ public function description(): string public function schema(ToolInputSchema $schema): ToolInputSchema { - // No inputs required for this tool. return $schema; } diff --git a/src/Mcp/Tools/DatabaseQuery.php b/src/Mcp/Tools/DatabaseQuery.php index ff8c439..caa28a1 100644 --- a/src/Mcp/Tools/DatabaseQuery.php +++ b/src/Mcp/Tools/DatabaseQuery.php @@ -9,8 +9,9 @@ use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; +use Throwable; -#[IsReadOnly()] +#[IsReadOnly] class DatabaseQuery extends Tool { /** @@ -56,9 +57,9 @@ public function handle(array $arguments): ToolResult 'EXPLAIN', 'DESCRIBE', 'DESC', - 'WITH', // Common-table expressions, must be followed by SELECT + 'WITH', // SELECT must follow Common-table expressions 'VALUES', // Returns literal values - 'TABLE', // PostgreSQL shorthand for SELECT * + 'TABLE', // PostgresSQL shorthand for SELECT * ]; $isReadOnly = in_array($firstWord, $allowList, true); @@ -80,7 +81,7 @@ public function handle(array $arguments): ToolResult return ToolResult::json( DB::connection($connectionName)->select($query) ); - } catch (\Throwable $e) { + } catch (Throwable $e) { return ToolResult::error('Query failed: '.$e->getMessage()); } } diff --git a/src/Mcp/Tools/DatabaseSchema.php b/src/Mcp/Tools/DatabaseSchema.php index d607c79..b0589e0 100644 --- a/src/Mcp/Tools/DatabaseSchema.php +++ b/src/Mcp/Tools/DatabaseSchema.php @@ -4,6 +4,8 @@ namespace Laravel\Boost\Mcp\Tools; +use Exception; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -14,7 +16,7 @@ use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class DatabaseSchema extends Tool { public function description(): string @@ -42,7 +44,7 @@ public function handle(array $arguments): ToolResult { $connection = $arguments['database'] ?? config('database.default'); $filter = $arguments['filter'] ?? ''; - $cacheKey = "boost:mcp:database-schema:{$connection}:{$filter}"; + $cacheKey = "boost:mcp:database-schema:$connection:$filter"; $schema = Cache::remember($cacheKey, 20, function () use ($connection, $filter) { return $this->getDatabaseStructure($connection, $filter); @@ -53,13 +55,11 @@ public function handle(array $arguments): ToolResult protected function getDatabaseStructure(?string $connection, string $filter = ''): array { - $structure = [ + return [ 'engine' => DB::connection($connection)->getDriverName(), 'tables' => $this->getAllTablesStructure($connection, $filter), 'global' => $this->getGlobalStructure($connection), ]; - - return $structure; } protected function getAllTablesStructure(?string $connection, string $filter = ''): array @@ -102,7 +102,7 @@ protected function getTableStructure(?string $connection, string $tableName): ar 'triggers' => $triggers, 'check_constraints' => $checkConstraints, ]; - } catch (\Exception $e) { + } catch (Exception $e) { Log::error('Failed to get table structure for: '.$tableName, [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), @@ -136,15 +136,15 @@ protected function getTableIndexes(?string $connection, string $tableName): arra foreach ($indexes as $index) { $indexDetails[$index['name']] = [ - 'columns' => $index['columns'], - 'type' => $index['type'] ?? null, - 'is_unique' => $index['unique'] ?? false, - 'is_primary' => $index['primary'] ?? false, + 'columns' => Arr::get($index, 'columns'), + 'type' => Arr::get($index, 'type'), + 'is_unique' => Arr::get($index, 'unique', false), + 'is_primary' => Arr::get($index, 'primary', false), ]; } return $indexDetails; - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -153,7 +153,7 @@ protected function getTableForeignKeys(?string $connection, string $tableName): { try { return Schema::connection($connection)->getForeignKeys($tableName); - } catch (\Exception $e) { + } catch (Exception) { return []; } } diff --git a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php index 6782660..8ec339f 100644 --- a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Mcp\Tools\DatabaseSchema; +use Exception; use Illuminate\Support\Facades\DB; class MySQLSchemaDriver extends DatabaseSchemaDriver @@ -16,7 +17,7 @@ public function getViews(): array FROM information_schema.VIEWS WHERE TABLE_SCHEMA = DATABASE() '); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -25,7 +26,7 @@ public function getStoredProcedures(): array { try { return DB::connection($this->connection)->select('SHOW PROCEDURE STATUS WHERE Db = DATABASE()'); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -34,7 +35,7 @@ public function getFunctions(): array { try { return DB::connection($this->connection)->select('SHOW FUNCTION STATUS WHERE Db = DATABASE()'); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -47,7 +48,7 @@ public function getTriggers(?string $table = null): array } return DB::connection($this->connection)->select('SHOW TRIGGERS'); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -61,7 +62,7 @@ public function getCheckConstraints(string $table): array WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = ? ', [$table]); - } catch (\Exception $e) { + } catch (Exception) { return []; } } diff --git a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php index 7e196de..3214741 100644 --- a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Mcp\Tools\DatabaseSchema; +use Exception; use Illuminate\Support\Facades\DB; class PostgreSQLSchemaDriver extends DatabaseSchemaDriver @@ -12,11 +13,11 @@ public function getViews(): array { try { return DB::connection($this->connection)->select(" - SELECT schemaname, viewname, definition - FROM pg_views + SELECT schemaname, viewname, definition + FROM pg_views WHERE schemaname NOT IN ('pg_catalog', 'information_schema') "); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -31,7 +32,7 @@ public function getStoredProcedures(): array WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND prokind = 'p' "); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -46,7 +47,7 @@ public function getFunctions(): array WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND prokind = 'f' "); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -66,7 +67,7 @@ public function getTriggers(?string $table = null): array } return DB::connection($this->connection)->select($sql); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -76,11 +77,11 @@ public function getCheckConstraints(string $table): array try { return DB::connection($this->connection)->select(" SELECT conname, pg_get_constraintdef(oid) as definition - FROM pg_constraint - WHERE contype = 'c' + FROM pg_constraint + WHERE contype = 'c' AND conrelid = ?::regclass ", [$table]); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -93,7 +94,7 @@ public function getSequences(): array FROM information_schema.sequences WHERE sequence_schema = current_schema() '); - } catch (\Exception $e) { + } catch (Exception) { return []; } } diff --git a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php index f77cb1c..8b5ebde 100644 --- a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Mcp\Tools\DatabaseSchema; +use Exception; use Illuminate\Support\Facades\DB; class SQLiteSchemaDriver extends DatabaseSchemaDriver @@ -12,11 +13,11 @@ public function getViews(): array { try { return DB::connection($this->connection)->select(" - SELECT name, sql - FROM sqlite_master + SELECT name, sql + FROM sqlite_master WHERE type = 'view' "); - } catch (\Exception $e) { + } catch (Exception) { return []; } } @@ -42,7 +43,7 @@ public function getTriggers(?string $table = null): array } return DB::connection($this->connection)->select($sql); - } catch (\Exception $e) { + } catch (Exception) { return []; } } diff --git a/src/Mcp/Tools/GetAbsoluteUrl.php b/src/Mcp/Tools/GetAbsoluteUrl.php index 37b626c..744894d 100644 --- a/src/Mcp/Tools/GetAbsoluteUrl.php +++ b/src/Mcp/Tools/GetAbsoluteUrl.php @@ -4,12 +4,13 @@ namespace Laravel\Boost\Mcp\Tools; +use Illuminate\Support\Arr; use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class GetAbsoluteUrl extends Tool { public function description(): string @@ -35,8 +36,8 @@ public function schema(ToolInputSchema $schema): ToolInputSchema */ public function handle(array $arguments): ToolResult { - $path = $arguments['path'] ?? null; - $routeName = $arguments['route'] ?? null; + $path = Arr::get($arguments, 'path'); + $routeName = Arr::get($arguments, 'route'); if ($path) { return ToolResult::text(url($path)); diff --git a/src/Mcp/Tools/GetConfig.php b/src/Mcp/Tools/GetConfig.php index 04eee15..22bb371 100644 --- a/src/Mcp/Tools/GetConfig.php +++ b/src/Mcp/Tools/GetConfig.php @@ -34,7 +34,7 @@ public function handle(array $arguments): ToolResult $key = $arguments['key']; if (! Config::has($key)) { - return ToolResult::error("Config key '{$key}' not found."); + return ToolResult::error("Config key '$key' not found."); } return ToolResult::json([ diff --git a/src/Mcp/Tools/LastError.php b/src/Mcp/Tools/LastError.php index 3751f08..8999a9d 100644 --- a/src/Mcp/Tools/LastError.php +++ b/src/Mcp/Tools/LastError.php @@ -13,7 +13,7 @@ use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class LastError extends Tool { use ReadsLogs; @@ -69,11 +69,11 @@ public function handle(array $arguments): ToolResult return ToolResult::text($entry); } - // Locate the correct log file using shared helper. + // Locate the correct log file using the shared helper. $logFile = $this->resolveLogFilePath(); if (! file_exists($logFile)) { - return ToolResult::error("Log file not found at {$logFile}"); + return ToolResult::error("Log file not found at $logFile"); } $entry = $this->readLastErrorEntry($logFile); diff --git a/src/Mcp/Tools/ListArtisanCommands.php b/src/Mcp/Tools/ListArtisanCommands.php index 47a72a6..647210b 100644 --- a/src/Mcp/Tools/ListArtisanCommands.php +++ b/src/Mcp/Tools/ListArtisanCommands.php @@ -11,7 +11,7 @@ use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class ListArtisanCommands extends Tool { public function description(): string @@ -21,7 +21,6 @@ public function description(): string public function schema(ToolInputSchema $schema): ToolInputSchema { - // No inputs required. return $schema; } @@ -42,7 +41,7 @@ public function handle(array $arguments): ToolResult } // Sort alphabetically by name for determinism. - usort($commandList, fn ($a, $b) => strcmp($a['name'], $b['name'])); + usort($commandList, fn ($firstCommand, $secondCommand) => strcmp($firstCommand['name'], $secondCommand['name'])); return ToolResult::json($commandList); } diff --git a/src/Mcp/Tools/ListAvailableConfigKeys.php b/src/Mcp/Tools/ListAvailableConfigKeys.php index d8a532d..8e8bc3f 100644 --- a/src/Mcp/Tools/ListAvailableConfigKeys.php +++ b/src/Mcp/Tools/ListAvailableConfigKeys.php @@ -20,7 +20,6 @@ public function description(): string public function schema(ToolInputSchema $schema): ToolInputSchema { - // No inputs required for this tool. return $schema; } @@ -47,16 +46,16 @@ private function flattenToDotNotation(array $array, string $prefix = ''): array $results = []; foreach ($array as $key => $value) { - $newPrefix = $prefix === '' ? $key : $prefix.$key; + $currentKey = $prefix.$key; if (is_array($value)) { - $results = array_merge($results, $this->flattenToDotNotation($value, $newPrefix.'.')); + $results = array_merge($results, $this->flattenToDotNotation($value, $currentKey.'.')); } else { // Skip numeric keys at the top level (they're likely array values, not config keys) if ($prefix === '' && is_numeric($key)) { continue; } - $results[] = $newPrefix; + $results[] = $currentKey; } } diff --git a/src/Mcp/Tools/ListAvailableEnvVars.php b/src/Mcp/Tools/ListAvailableEnvVars.php index e4384a8..d5c8ffe 100644 --- a/src/Mcp/Tools/ListAvailableEnvVars.php +++ b/src/Mcp/Tools/ListAvailableEnvVars.php @@ -4,12 +4,13 @@ namespace Laravel\Boost\Mcp\Tools; +use Illuminate\Support\Arr; use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; -#[IsReadOnly()] +#[IsReadOnly] class ListAvailableEnvVars extends Tool { public function description(): string @@ -31,25 +32,27 @@ public function schema(ToolInputSchema $schema): ToolInputSchema */ public function handle(array $arguments): ToolResult { - $filename = $arguments['filename'] ?? null; - $filePath = ! empty($filename) ? base_path($filename) : base_path('.env'); - if (str_contains($filePath, '.env') === false) { + $filename = Arr::get($arguments, 'filename', '.env'); + + $filePath = base_path($filename); + + if (! str_contains($filePath, '.env')) { return ToolResult::error('This tool can only read .env files'); } if (! file_exists($filePath)) { - return ToolResult::error("File not found at '{$filePath}'"); + return ToolResult::error("File not found at '$filePath'"); } $envLines = file_get_contents($filePath); - if ($envLines === false) { + if (! $envLines) { return ToolResult::error('Failed to read .env file.'); } $count = preg_match_all('/^(?!\s*#)\s*([^=\s]+)=/m', $envLines, $matches); - if ($count === false) { + if (! $count) { return ToolResult::error('Failed to parse .env file'); } diff --git a/src/Mcp/Tools/Tinker.php b/src/Mcp/Tools/Tinker.php index d9b34ea..d4dc384 100644 --- a/src/Mcp/Tools/Tinker.php +++ b/src/Mcp/Tools/Tinker.php @@ -4,6 +4,8 @@ namespace Laravel\Boost\Mcp\Tools; +use Exception; +use Illuminate\Support\Arr; use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\ToolInputSchema; use Laravel\Mcp\Server\Tools\ToolResult; @@ -33,57 +35,45 @@ public function schema(ToolInputSchema $schema): ToolInputSchema /** * @param array $arguments + * + * @throws Exception */ public function handle(array $arguments): ToolResult { - $code = str_replace([''], '', (string) $arguments['code']); - $timeout = 30; - if (! empty($arguments['timeout']) && is_int($arguments['timeout'])) { - $timeout = $arguments['timeout']; - } - $timeout = min(180, $timeout); + $code = str_replace([''], '', (string) Arr::get($arguments, 'code')); - // Set execution timeout + $timeout = min(180, (int) (Arr::get($arguments, 'timeout', 30))); set_time_limit($timeout); - - // Set memory limit for safety ini_set('memory_limit', '128M'); // Use PCNTL alarm for additional timeout control if available (Unix only) if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGALRM, function () { - throw new \Exception('Code execution timed out'); + throw new Exception('Code execution timed out'); }); pcntl_alarm($timeout); } - // Start output buffering to capture any output ob_start(); try { - // Execute the code and capture the return value $result = eval($code); - // Cancel alarm if set if (function_exists('pcntl_alarm')) { pcntl_alarm(0); } - // Get any output that was printed $output = ob_get_contents(); - - // Clean the output buffer ob_end_clean(); - // Prepare the response $response = [ 'result' => $result, 'output' => $output, 'type' => gettype($result), ]; - // If result is an object, include class name + // If a result is an object, include the class name if (is_object($result)) { $response['class'] = get_class($result); } @@ -91,12 +81,10 @@ public function handle(array $arguments): ToolResult return ToolResult::json($response); } catch (Throwable $e) { - // Cancel alarm if set if (function_exists('pcntl_alarm')) { pcntl_alarm(0); } - // Clean the output buffer on error ob_end_clean(); return ToolResult::json([