Skip to content

Client API

The Client class drives the client side of an MCP session over a TransportInterface. Build one with the fluent ClientBuilder, connect it to a transport, then issue typed requests. Each request is self-contained, so typed calls can start as soon as the transport is connected.

The SDK ships two client transports. StdioClientTransport launches the server as a subprocess. StreamableHttpClientTransport POSTs to a remote MCP endpoint. Everything on this page works the same on both. The one exception is mirrored tool parameters, which are HTTP-only.

use Nexus\Mcp\Client\ClientBuilder;
use Nexus\Mcp\Client\Transport\StdioClientTransport;

$client = (new ClientBuilder())
    ->setClientInfo(name: 'my-client', version: '1.0.0')
    ->build()
;

$transport = new StdioClientTransport(command: [\PHP_BINARY, 'server.php']);
$client->connect($transport);

foreach ($client->listTools()->tools as $tool) {
    echo $tool->name, "\n";
}

$transport->close();

connect() does not block. It attaches the listener chain and starts the transport.

Shut the session down with Client::disconnect(), or close the transport directly. disconnect() closes the transport and detaches it, so a later connect() can run. Either way, the SDK cancels the pending requests with a TransportAlreadyClosedException. disconnect() does nothing when the client is not connected.

Request metadata

Every request the client sends carries the client's identity in its _meta block. The SDK stamps three namespaced keys onto every outbound request: io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, and io.modelcontextprotocol/clientCapabilities. The server reads the client's identity and capabilities from each request's _meta.

Only protocolVersion and clientCapabilities are required in the envelope. A server must treat clientInfo as optional, and must not make behavioural or security decisions on it.

Protocol version rejection

A server that will not serve the stamped version answers -32022 and names the versions it accepts. The client retries once. It restamps the request with the first of those versions that this SDK also speaks.

When the rejection names no usable version, or the retry is rejected in turn, the error reaches the caller as a RemoteCallFailedException. Its error is the UnsupportedProtocolVersionError, which carries supported and requested.

Guide

Lifecycle

Build

build() validates the configuration and returns a Client. The client info must be set.

Connect

connect($transport) attaches the listener chain, starts the transport, and returns immediately.

Typed requests

The client correlates inbound responses to awaiting callers by RequestId. You can call discover() first to learn the server's identity and capabilities. That call is optional.

Shutdown

Call disconnect(), or call close() on the transport directly. The SDK cancels the pending requests with TransportAlreadyClosedException. In-flight notification handlers drain before the close listeners fire.

Losing the peer

Losing the peer is not the same as shutting down. Behind a SupervisedTransport, the client can send a pending request again to the replacement instead of failing it. Retrying a lost request covers this. It is off by default, and it is limited to methods that read state.

See also

  • Getting started: install and a minimal server.
  • Server API: the symmetric builder reference.
  • Transports: StdioClientTransport (subprocess launcher), StreamableHttpClientTransport (one POST per message), and the in-memory paired transport.
  • Architecture: the dispatch kernel and layering.