[Client] Implement setMaxRetries connection retries (#413)

* [Client] Implement setMaxRetries connection retries

* [Client] Reject a negative retry count in Configuration

* [Client] Use a positive init timeout in the retry test

* [Client] Drop redundant docblock note on setMaxRetries

* [Client] Clear the initialized flag on a failed connection

* [Client] Trim comments to the non-obvious ones

* [Client] Cover retries and timeout cleanup with integration tests

The retry fixture counts the processes it was started as, which is what
separates a real retry from a second call into the same server. The timeout
fixture outlives the client's patience and then goes idle, so the next
request runs on a connection the client already gave up on once.
This commit is contained in:
Christopher Hertel 2026-08-15 02:43:50 +02:00 • committed by GitHub
commit d8cc21a345
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -58,6 +58,8 @@ use Psr\Log\NullLogger;
*/
class Client
{
private const RETRY_BASE_DELAY_MS = 100;
private ?TransportInterface $transport = null;
public function __construct(
@ -78,16 +80,44 @@ class Client
/**
* Connect to an MCP server using the provided transport.
*
* @throws ConnectionException If connection or initialization fails
* A failed attempt is closed and retried, see {@see Builder::setMaxRetries()}.
*
* @throws ConnectionException If connection or initialization fails on every attempt
*/
public function connect(TransportInterface $transport): void
{
$this->transport = $transport;
$this->protocol->connect($transport, $this->config);
$transport->connect();
$maxAttempts = $this->config->maxRetries + 1;
$this->logger->info('Client connected and initialized');
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
try {
$transport->connect();
$this->logger->info('Client connected and initialized', ['attempt' => $attempt]);
return;
} catch (ConnectionException $e) {
// initialize() flags the session before sending the initialized
// notification, so a failure in between leaves the flag set.
$this->protocol->getState()->setInitialized(false);
$transport->close();
if ($attempt === $maxAttempts) {
throw $e;
}
$this->logger->warning('Connection attempt failed, retrying', [
'attempt' => $attempt,
'max_attempts' => $maxAttempts,
'exception' => $e,
]);
usleep($attempt * self::RETRY_BASE_DELAY_MS * 1000);
}
}
}
/**