A client is configured once through its builder, then connected to a transport. When you connect, the client and the server agree on a protocol version. After that, the server's capabilities are known and you can use its tools, resources and prompts.
The Client\Builder provides fluent configuration of client instances.
use Mcp\Client\Client;
$client = Client::builder()
->setClientInfo('My Application', '1.0.0', 'Description of my client')
->setInitTimeout(30) // seconds to wait for each message while connecting (default: 30)
->setRequestTimeout(120) // seconds one call may take, including your callbacks (default: 120)
->setMaxRetries(3) // retries for a failed connection (default: 3)
->build();A server that doesn't answer uses up the initialization timeout twice per attempt: once for
server/discover and once for initialize. With the default values, connect() can wait about
4 minutes before it fails.
The request timeout covers the whole call, including the time your elicitation or sampling callback
takes. To give a user more time to answer, pass timeoutSeconds to callTool(). See
Cancellation and deadlines.
setMaxRetries() controls how often connect() retries a failed connection. It
counts retries rather than attempts, so the default of 3 means one initial
attempt plus up to three retries — four in total — before the ConnectionException
of the last attempt is rethrown:
$client = Client::builder()
->setMaxRetries(0) // Fail on the first failed attempt
->build();Between two attempts the transport is closed, so a retry never reuses a
half-established connection: a StdioTransport spawns a fresh server process and
an HttpTransport discards the session ID of the failed attempt. Each retry is
preceded by a short, linearly growing delay (100ms, 200ms, 300ms, …).
Only the connection handshake is retried. Individual requests such as
callTool() are always sent once — retrying them is unsafe as tool calls are not
necessarily idempotent.
Set the client's identity that is reported to the server:
$client = Client::builder()
->setClientInfo(
name: 'AI Assistant Client',
version: '2.1.0',
description: 'Client for automated AI workflows',
// optional name to display in user interfaces
title: 'AI Assistant',
)
->build();A client speaks both protocol eras by default. It prefers 2026-07-28, probes for it with server/discover when
connecting, and falls back to the initialize handshake on 2025-11-25 when the server turns out not to speak it.
Use $client->getProtocolVersion() after connecting to read what the connection settled on.
use Mcp\Schema\Enum\ProtocolVersion;
// Fall back to an older handshake revision instead of 2025-11-25…
$client = Client::builder()
->setFallbackProtocolVersion(ProtocolVersion::V2025_06_18)
->build();
// …or not at all: connect() then throws a ConnectionException
// when the server doesn't speak 2026-07-28
$client = Client::builder()
->setFallbackProtocolVersion(null)
->build();Passing a handshake revision to setProtocolVersion() skips the probe and opens with initialize, the way a client
from before the modern era would:
$client = Client::builder()
->setProtocolVersion(ProtocolVersion::V2025_11_25)
->build();The handshake is an offer, not a demand. A server that does not support the requested revision counter-offers one it
does, as described in the specification's
version negotiation
section. The client accepts any handshake revision it knows about and continues on that revision. Any other counter-offer
fails the handshake with a ConnectionException.
See Protocol versions for how the probe is read, and the handshake era for the server side of the exchange.
Declare client capabilities to enable server features:
use Mcp\Schema\ClientCapabilities;
$client = Client::builder()
->setCapabilities(new ClientCapabilities(
elicitation: true, // Let the server ask the user for input
))
->build();ClientCapabilities also takes sub-capabilities. Pass them as named arguments:
new ClientCapabilities(
sampling: true,
samplingContext: true, // the server may ask to include context in sampling
samplingTools: true, // the server may pass tools in sampling requests
elicitationForm: true, // form mode, implied by elicitation: true alone
elicitationUrl: true, // url mode, the user continues in a browser
);Form mode is implied only when you declare no mode. To support both modes, pass both flags.
To declare a protocol extension (e.g. MCP Apps), call enableExtension() on the builder.
See Protocol extensions.
Register handlers for server-initiated notifications:
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
use Mcp\Schema\Notification\LoggingMessageNotification;
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) {
$message = \is_string($notification->data) ? $notification->data : json_encode($notification->data);
echo "[{$notification->level->value}] {$message}\n";
}
);
$client = Client::builder()
->addNotificationHandler($loggingHandler)
->build();!!! note
LoggingNotificationHandler is deprecated, together with MCP logging. On revision 2026-07-28,
the server sends log messages only after you call setLoggingLevel(). See
Logging notifications.
Register handlers for server-initiated requests (e.g., elicitation). The same handlers answer a
multi round-trip input_required result on a modern revision, where the server
returns its ask instead of sending a request:
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
$elicitationCallback = new class implements ElicitationCallbackInterface {
public function __invoke(ElicitRequest $request): ElicitResult
{
// Ask the user for the requested input and return their answer.
// Without a user interface, decline the request:
return new ElicitResult(ElicitAction::Decline);
}
};
$client = Client::builder()
// the server only sends elicitation requests if the client declares the capability
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler(new ElicitationRequestHandler($elicitationCallback))
->build();Configure PSR-3 logging for debugging. This example uses Monolog, which you install with
composer require monolog/monolog:
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$logger = new Logger('mcp-client');
$logger->pushHandler(new StreamHandler('client.log', Level::Debug));
$client = Client::builder()
->setLogger($logger)
->build();$client->connect($transport);By default, the connect() method opens the transport and sends a server/discover request. If the server
speaks revision 2026-07-28, the connection is ready. If it doesn't, the client falls back to the
initialize handshake and offers 2025-11-25, or the revision set with
setFallbackProtocolVersion(). A server process that exits fails the connection instead.
With setFallbackProtocolVersion(null), connect() fails instead. See
How the client settles on an era.
A client pinned to a handshake revision with setProtocolVersion() skips server/discover and starts with the
initialize handshake. See Protocol Version.
!!! warning
Always wrap connection in try/catch to handle ConnectionException for failed connections.
if ($client->isConnected()) {
// Client is connected and initialized
}$client->disconnect();Always disconnect when finished to clean up resources:
try {
$client->connect($transport);
// ... use the client ...
} finally {
$client->disconnect();
}Calling connect() again on the same client closes its current transport before opening the new one.
After successful connection, retrieve server metadata:
// Get server implementation info
// (null on 2026-07-28 when the server/discover result has no serverInfo)
$serverInfo = $client->getServerInfo();
echo "Server: {$serverInfo?->name} v{$serverInfo?->version}\n";
// Get the capabilities the server declared
if ($client->getServerCapabilities()?->tools) {
$tools = $client->listTools();
}
// Get server instructions
$instructions = $client->getInstructions();
if ($instructions) {
echo "Instructions: {$instructions}\n";
}