Skip to content

Commit 197e963

Browse files
Refactor ToolExecutor to use subprocess execution and enhance timeout handling; update Tinker tool to reflect new timeout defaults and remove inline execution tests.
1 parent ad5ab58 commit 197e963

File tree

4 files changed

+46
-141
lines changed

4 files changed

+46
-141
lines changed

src/Mcp/ToolExecutor.php

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,10 @@ public function execute(string $toolClass, array $arguments = []): ToolResult
2323
return ToolResult::error("Tool not registered or not allowed: {$toolClass}");
2424
}
2525

26-
if ($this->shouldUseProcessIsolation()) {
27-
return $this->executeInProcess($toolClass, $arguments);
28-
}
29-
30-
return $this->executeInline($toolClass, $arguments);
26+
return $this->executeInSubprocess($toolClass, $arguments);
3127
}
3228

33-
protected function executeInProcess(string $toolClass, array $arguments): ToolResult
29+
protected function executeInSubprocess(string $toolClass, array $arguments): ToolResult
3430
{
3531
$command = $this->buildCommand($toolClass, $arguments);
3632

@@ -45,7 +41,7 @@ protected function executeInProcess(string $toolClass, array $arguments): ToolRe
4541
$process = new Process(
4642
command: $command,
4743
env: $cleanEnv,
48-
timeout: $this->getTimeout()
44+
timeout: $this->getTimeout($arguments)
4945
);
5046

5147
try {
@@ -62,7 +58,7 @@ protected function executeInProcess(string $toolClass, array $arguments): ToolRe
6258
} catch (ProcessTimedOutException $e) {
6359
$process->stop();
6460

65-
return ToolResult::error("Tool execution timed out after {$this->getTimeout()} seconds");
61+
return ToolResult::error("Tool execution timed out after {$this->getTimeout($arguments)} seconds");
6662

6763
} catch (ProcessFailedException $e) {
6864
$errorOutput = $process->getErrorOutput().$process->getOutput();
@@ -71,30 +67,11 @@ protected function executeInProcess(string $toolClass, array $arguments): ToolRe
7167
}
7268
}
7369

74-
protected function executeInline(string $toolClass, array $arguments): ToolResult
75-
{
76-
try {
77-
/** @var \Laravel\Mcp\Server\Tool $tool */
78-
$tool = app($toolClass);
79-
80-
return $tool->handle($arguments);
81-
} catch (\Throwable $e) {
82-
return ToolResult::error("Inline tool execution failed: {$e->getMessage()}");
83-
}
84-
}
85-
86-
protected function shouldUseProcessIsolation(): bool
70+
protected function getTimeout(array $arguments): int
8771
{
88-
if (app()->environment('testing')) {
89-
return false;
90-
}
72+
$timeout = (int) ($arguments['timeout'] ?? 180);
9173

92-
return config('boost.process_isolation.enabled', true);
93-
}
94-
95-
protected function getTimeout(): int
96-
{
97-
return config('boost.process_isolation.timeout', 180);
74+
return max(1, min(600, $timeout));
9875
}
9976

10077
/**

src/Mcp/Tools/Tinker.php

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function schema(ToolInputSchema $schema): ToolInputSchema
3030
->description('PHP code to execute (without opening <?php tags)')
3131
->required()
3232
->integer('timeout')
33-
->description('Maximum execution time in seconds (default: 30)');
33+
->description('Maximum execution time in seconds (default: 180)');
3434
}
3535

3636
/**
@@ -42,26 +42,8 @@ public function handle(array $arguments): ToolResult
4242
{
4343
$code = str_replace(['<?php', '?>'], '', (string) Arr::get($arguments, 'code'));
4444

45-
$timeout = min(180, (int) (Arr::get($arguments, 'timeout', 30)));
46-
47-
// When process isolation is enabled, the ToolExecutor handles timeouts
48-
if (! config('boost.process_isolation.enabled', false)) {
49-
// On Windows: set_time_limit causes uncatchable fatal errors that crash the MCP server
50-
// On Unix: set_time_limit works alongside PCNTL for redundant timeout protection
51-
set_time_limit($timeout);
52-
}
53-
5445
ini_set('memory_limit', '128M');
5546

56-
// Use PCNTL alarm for timeout control if available (Unix only)
57-
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
58-
pcntl_async_signals(true);
59-
pcntl_signal(SIGALRM, function () {
60-
throw new Exception('Code execution timed out');
61-
});
62-
pcntl_alarm($timeout);
63-
}
64-
6547
ob_start();
6648

6749
try {
@@ -94,11 +76,6 @@ public function handle(array $arguments): ToolResult
9476
} finally {
9577

9678
ob_end_clean();
97-
98-
// Clean up PCNTL alarm
99-
if (function_exists('pcntl_alarm')) {
100-
pcntl_alarm(0);
101-
}
10279
}
10380
}
10481
}

tests/Feature/Mcp/ToolExecutorTest.php

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,7 @@
66
use Laravel\Boost\Mcp\Tools\Tinker;
77
use Laravel\Mcp\Server\Tools\ToolResult;
88

9-
test('can execute tool inline', function () {
10-
// Disable process isolation for this test
11-
config(['boost.process_isolation.enabled' => false]);
12-
13-
$executor = app(ToolExecutor::class);
14-
$result = $executor->execute(ApplicationInfo::class, []);
15-
16-
expect($result)->toBeInstanceOf(ToolResult::class);
17-
});
18-
19-
test('can execute tool with process isolation', function () {
20-
// Enable process isolation for this test
21-
config(['boost.process_isolation.enabled' => true]);
22-
9+
test('can execute tool in subprocess', function () {
2310
// Create a mock that overrides buildCommand to work with testbench
2411
$executor = Mockery::mock(ToolExecutor::class)->makePartial()
2512
->shouldAllowMockingProtectedMethods();
@@ -54,8 +41,6 @@
5441
});
5542

5643
test('subprocess proves fresh process isolation', function () {
57-
config(['boost.process_isolation.enabled' => true]);
58-
5944
$executor = Mockery::mock(ToolExecutor::class)->makePartial()
6045
->shouldAllowMockingProtectedMethods();
6146
$executor->shouldReceive('buildCommand')
@@ -76,8 +61,6 @@
7661
});
7762

7863
test('subprocess sees modified autoloaded code changes', function () {
79-
config(['boost.process_isolation.enabled' => true]);
80-
8164
$executor = Mockery::mock(ToolExecutor::class)->makePartial()
8265
->shouldAllowMockingProtectedMethods();
8366
$executor->shouldReceive('buildCommand')
@@ -149,3 +132,40 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array
149132

150133
return [PHP_BINARY, '-r', $testScript];
151134
}
135+
136+
test('respects custom timeout parameter', function () {
137+
$executor = Mockery::mock(ToolExecutor::class)->makePartial()
138+
->shouldAllowMockingProtectedMethods();
139+
140+
$executor->shouldReceive('buildCommand')
141+
->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments));
142+
143+
// Test with custom timeout - should succeed with fast code
144+
$result = $executor->execute(Tinker::class, [
145+
'code' => 'return "timeout test";',
146+
'timeout' => 30
147+
]);
148+
149+
expect($result->isError)->toBeFalse();
150+
});
151+
152+
test('clamps timeout values correctly', function () {
153+
$executor = new ToolExecutor();
154+
155+
// Test timeout clamping using reflection to access protected method
156+
$reflection = new ReflectionClass($executor);
157+
$method = $reflection->getMethod('getTimeout');
158+
$method->setAccessible(true);
159+
160+
// Test default
161+
expect($method->invoke($executor, []))->toBe(180);
162+
163+
// Test custom value
164+
expect($method->invoke($executor, ['timeout' => 60]))->toBe(60);
165+
166+
// Test minimum clamp
167+
expect($method->invoke($executor, ['timeout' => 0]))->toBe(1);
168+
169+
// Test maximum clamp
170+
expect($method->invoke($executor, ['timeout' => 1000]))->toBe(600);
171+
});

tests/Feature/Mcp/Tools/TinkerTest.php

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -140,72 +140,3 @@
140140

141141
expect($tool->shouldRegister())->toBeTrue();
142142
});
143-
144-
test('uses custom timeout parameter', function () {
145-
$tool = new Tinker;
146-
$result = $tool->handle(['code' => 'return 2 + 2;', 'timeout' => 10]);
147-
148-
expect($result)->isToolResult()
149-
->toolJsonContentToMatchArray([
150-
'result' => 4,
151-
'type' => 'integer',
152-
]);
153-
});
154-
155-
test('uses default timeout when not specified', function () {
156-
$tool = new Tinker;
157-
$result = $tool->handle(['code' => 'return 2 + 2;']);
158-
159-
expect($result)->isToolResult()
160-
->toolJsonContentToMatchArray([
161-
'result' => 4,
162-
'type' => 'integer',
163-
]);
164-
});
165-
166-
test('times out when code takes too long', function () {
167-
// Skip if PCNTL functions are not available
168-
if (!function_exists('pcntl_async_signals') || !function_exists('pcntl_signal')) {
169-
$this->markTestSkipped('PCNTL functions not available for timeout testing');
170-
}
171-
172-
// Disable process isolation for this test
173-
config(['boost.process_isolation.enabled' => false]);
174-
175-
$tool = new Tinker;
176-
177-
// Code that will take more than 1 second to execute
178-
$slowCode = '
179-
$start = microtime(true);
180-
while (microtime(true) - $start < 1.2) {
181-
usleep(50000); // Don\'t waste entire CPU
182-
}
183-
return "should not reach here";
184-
';
185-
186-
$result = $tool->handle(['code' => $slowCode, 'timeout' => 1]);
187-
188-
expect($result)->isToolResult()
189-
->toolJsonContent(function ($data) {
190-
expect($data)->toHaveKey('error')
191-
->and($data['error'])->toMatch('/(Maximum execution time|Code execution timed out|Fatal error)/');
192-
});
193-
});
194-
195-
test('relies on process isolation for timeout protection on Windows', function () {
196-
// This test documents that Windows relies on process isolation for safe timeout handling
197-
if (PHP_OS_FAMILY !== 'Windows') {
198-
$this->markTestSkipped('Windows-specific timeout behavior test');
199-
}
200-
201-
// Process isolation enabled (recommended for Windows)
202-
config(['boost.process_isolation.enabled' => true]);
203-
204-
$tool = new Tinker;
205-
$result = $tool->handle(['code' => 'return "Windows: process isolation provides safe timeout protection";']);
206-
207-
expect($result)->isToolResult()
208-
->toolJsonContent(function ($data) {
209-
expect($data['result'])->toBe('Windows: process isolation provides safe timeout protection');
210-
});
211-
});

0 commit comments

Comments
 (0)