-
-
Notifications
You must be signed in to change notification settings - Fork 0
Secure MCP tool execution via Artisan #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
48078c0
b890233
da734b3
02f9647
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
| } | ||
|
Comment on lines
+505
to
+516
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code uses several magic numbers for buffer chunk sizes and maximum output sizes (e.g., |
||
|
|
||
| // 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; | ||
|
Comment on lines
+521
to
+524
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the process terminates, the remaining output is read from the pipes using // Read remaining output
$output .= stream_get_contents($pipes[1]);
$errorOutput .= stream_get_contents($pipes[2]);
if (strlen($output) > 10 * 1024 * 1024) { // 10MB absolute max
throw new \RuntimeException('Output size limit exceeded');
}
if (strlen($errorOutput) > 1024 * 1024) { // 1MB max for stderr
throw new \RuntimeException('Error output size limit exceeded');
}
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'])) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code currently attempts to read from both stdout and stderr pipes on every loop iteration where
stream_selectdoesn't fail. This is inefficient because it doesn't check which stream is actually ready for reading. You should use the$readarray, which is modified bystream_selectto contain only the ready streams, to conditionally read from the correct pipe.