diff --git a/composer.json b/composer.json index 3b4ae4e..2bda2b6 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,28 @@ "license": "EUPL-1.2", "require": { "php": "^8.2", - "host-uk/core": "@dev", + "host-uk/core": "dev-dev", + "host-uk/core-mcp": "dev-dev", "symfony/yaml": "^7.0" }, + "require-dev": { + "pestphp/pest": "^3.0", + "laravel/pint": "^1.18", + "orchestra/testbench": "^10.0", + "phpstan/phpstan": "^2.1", + "vimeo/psalm": "^6.14", + "phpunit/phpunit": "^11.5" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/host-uk/core-php" + }, + { + "type": "vcs", + "url": "https://github.com/host-uk/core-mcp" + } + ], "autoload": { "psr-4": { "Core\\Api\\": "src/Api/", @@ -19,6 +38,11 @@ "providers": [] } }, - "minimum-stability": "stable", + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "minimum-stability": "dev", "prefer-stable": true } diff --git a/src/Api/Controllers/McpApiController.php b/src/Api/Controllers/McpApiController.php index 828e85b..73f112e 100644 --- a/src/Api/Controllers/McpApiController.php +++ b/src/Api/Controllers/McpApiController.php @@ -14,6 +14,8 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; /** @@ -444,6 +446,9 @@ protected function executeToolViaArtisan(string $server, string $tool, array $ar throw new \RuntimeException("Unknown server: {$server}"); } + // Add timeout from configuration + $timeout = config('api.mcp.execution_timeout', 30); + // Build MCP request $mcpRequest = [ 'jsonrpc' => '2.0', @@ -471,15 +476,68 @@ protected function executeToolViaArtisan(string $server, string $tool, array $ar throw new \RuntimeException('Failed to start MCP server process'); } + // Write request to stdin fwrite($pipes[0], json_encode($mcpRequest)."\n"); fclose($pipes[0]); - $output = stream_get_contents($pipes[1]); + // Set non-blocking and use stream_select for timeout and size limits + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + $output = ''; + $errorOutput = ''; + $startTime = time(); + + while (true) { + $read = [$pipes[1], $pipes[2]]; + $write = $except = null; + + if (stream_select($read, $write, $except, 1) === false) { + break; + } + + if (time() - $startTime > $timeout) { + proc_terminate($process); + throw new \RuntimeException('MCP server execution timed out'); + } + + // Read with size limits + $output .= fread($pipes[1], 1024 * 1024); + $errorOutput .= fread($pipes[2], 1024 * 64); + + if (strlen($output) > 10 * 1024 * 1024) { // 10MB absolute max + proc_terminate($process); + throw new \RuntimeException('Output size limit exceeded'); + } + + if (strlen($errorOutput) > 1024 * 1024) { // 1MB max for stderr + proc_terminate($process); + throw new \RuntimeException('Error output size limit exceeded'); + } + + // Check if process is still running + $status = proc_get_status($process); + if (! $status['running']) { + // Read remaining output + $output .= stream_get_contents($pipes[1]); + $errorOutput .= stream_get_contents($pipes[2]); + break; + } + } + fclose($pipes[1]); fclose($pipes[2]); - proc_close($process); + // Log stderr for debugging (sanitized) + if (! empty($errorOutput)) { + Log::warning('MCP server stderr output', [ + 'server' => $server, + 'tool' => $tool, + 'error' => Str::limit($errorOutput, 1000), + ]); + } + $response = json_decode($output, true); if (isset($response['error'])) { diff --git a/src/Api/config.php b/src/Api/config.php index 701ee76..064ef05 100644 --- a/src/Api/config.php +++ b/src/Api/config.php @@ -234,4 +234,18 @@ 'max_per_page' => 100, ], + /* + |-------------------------------------------------------------------------- + | MCP Settings + |-------------------------------------------------------------------------- + | + | Configuration for Model Context Protocol (MCP) servers and execution. + | + */ + + 'mcp' => [ + // Execution timeout in seconds + 'execution_timeout' => env('MCP_EXECUTION_TIMEOUT', 30), + ], + ];