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 sent by the client 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

  • Client configuration: client info, capabilities, the logger, and the request-ID and progress-token factories.
  • Connecting and discovery: attaching a transport and reading server/discover.
  • Typed requests: the per-method calls, mirrored tool parameters over HTTP, and the sendRequest() escape hatch.
  • When the server asks for input first: answering an InputRequiredResult.
  • Progress and timeouts: streaming progress and request deadlines.
  • Notification handlers: reacting to server notifications.
  • Extensions: enabling SEP-2133 extensions, their capability advertisement, and the outbound gate.
  • Tasks: the SEP-2663 tasks extension. It calls tools as tasks and polls them to completion.
  • Apps: the SEP-1865 MCP Apps extension. It advertises renderable mime types and reads _meta.ui metadata.
  • Skills: the SEP-2640 skills extension. It lists skills and verifies each file it reads.
  • Subscriptions: opening subscriptions/listen streams.

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.