Client
June 2, 2026 ยท View on GitHub
The MCP Client SDK provides a synchronous, framework-agnostic API for communicating with MCP servers from PHP applications. It handles connection management, request/response correlation, server-initiated requests (sampling), and real-time notifications.
Table of Contents
- Overview
- Client Builder
- Transports
- Connecting to Servers
- Server Information
- Working with Tools
- Working with Resources
- Working with Prompts
- Server-Initiated Communication
- Error Handling
- Complete Example
Overview
The client follows a builder pattern for configuration and provides a synchronous API for all operations:
use Mcp\Client;
use Mcp\Client\Transport\StdioTransport;
// Build and configure the client
$client = Client::builder()
->setClientInfo('My Client', '1.0.0')
->setInitTimeout(30)
->setRequestTimeout(120)
->build();
// Create a transport
$transport = new StdioTransport(
command: 'php',
args: ['/path/to/server.php'],
);
// Connect and use the server
$client->connect($transport);
$tools = $client->listTools();
$client->disconnect();
Client Builder
The Client\Builder provides fluent configuration of client instances.
Basic Configuration
use Mcp\Client;
$client = Client::builder()
->setClientInfo('My Application', '1.0.0', 'Description of my client')
->setInitTimeout(30) // Seconds to wait for initialization
->setRequestTimeout(120) // Seconds to wait for request responses
->setMaxRetries(3) // Retry attempts for failed connections
->build();
Client Information
Set the client's identity reported to servers during initialization:
$client = Client::builder()
->setClientInfo(
name: 'AI Assistant Client',
version: '2.1.0',
description: 'Client for automated AI workflows'
)
->build();
Protocol Version
Specify the MCP protocol version (defaults to latest):
use Mcp\Schema\Enum\ProtocolVersion;
$client = Client::builder()
->setProtocolVersion(ProtocolVersion::V2025_11_25)
->build();
Capabilities
Declare client capabilities to enable server features:
use Mcp\Schema\ClientCapabilities;
$client = Client::builder()
->setCapabilities(new ClientCapabilities(
sampling: true, // Enable LLM sampling requests from server
roots: true, // Enable filesystem root listing
))
->build();
Notification Handlers
Register handlers for server-initiated notifications:
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
use Mcp\Schema\Notification\LoggingMessageNotification;
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) {
echo "[{$notification->level->value}] {$notification->data}\n";
}
);
$client = Client::builder()
->addNotificationHandler($loggingHandler)
->build();
Request Handlers
Register handlers for server-initiated requests (e.g., sampling):
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
use Mcp\Schema\Request\CreateSamplingMessageRequest;
use Mcp\Schema\Result\CreateSamplingMessageResult;
$samplingCallback = new class implements SamplingCallbackInterface {
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
{
// Perform LLM sampling and return result
}
};
$client = Client::builder()
->addRequestHandler(new SamplingRequestHandler($samplingCallback))
->build();
Logger
Configure PSR-3 logging for debugging:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('mcp-client');
$logger->pushHandler(new StreamHandler('client.log', Logger::DEBUG));
$client = Client::builder()
->setLogger($logger)
->build();
Transports
Transports handle the communication layer between client and server.
STDIO Transport
Spawns a server process and communicates via standard input/output:
use Mcp\Client\Transport\StdioTransport;
$transport = new StdioTransport(
command: 'php',
args: ['/path/to/server.php'],
cwd: '/working/directory', // Optional working directory
env: ['KEY' => 'value'], // Optional environment variables
);
Parameters:
command(string): The command to executeargs(array): Command argumentscwd(string|null): Working directory for the processenv(array|null): Environment variableslogger(LoggerInterface|null): Optional PSR-3 logger
HTTP Transport
Communicates with remote MCP servers over HTTP:
use Mcp\Client\Transport\HttpTransport;
$transport = new HttpTransport(
endpoint: 'http://localhost:8000',
headers: ['Authorization' => 'Bearer token'],
);
Parameters:
endpoint(string): The MCP server URLheaders(array): Additional HTTP headershttpClient(ClientInterface|null): PSR-18 HTTP client (auto-discovered)requestFactory(RequestFactoryInterface|null): PSR-17 request factory (auto-discovered)streamFactory(StreamFactoryInterface|null): PSR-17 stream factory (auto-discovered)logger(LoggerInterface|null): Optional PSR-3 logger
PSR-18 Auto-Discovery:
The transport automatically discovers PSR-18 HTTP clients from:
php-http/guzzle7-adapterphp-http/curl-clientsymfony/http-client- And other PSR-18 compatible implementations
# Install any PSR-18 client - discovery works automatically
composer require php-http/guzzle7-adapter
Connecting to Servers
Establishing Connection
$client->connect($transport);
The connect() method performs the MCP initialization handshake:
- Opens the transport connection
- Sends InitializeRequest with client capabilities
- Waits for InitializeResult from server
- Sends InitializedNotification
Important
Always wrap connection in try/catch to handle ConnectionException for failed connections.
Checking Connection State
if ($client->isConnected()) {
// Client is connected and initialized
}
Disconnecting
$client->disconnect();
Always disconnect when finished to clean up resources:
try {
$client->connect($transport);
// ... use the client ...
} finally {
$client->disconnect();
}
Server Information
After successful connection, retrieve server metadata:
// Get server implementation info
$serverInfo = $client->getServerInfo();
echo "Server: {$serverInfo->name} v{$serverInfo->version}\n";
// Get server instructions
$instructions = $client->getInstructions();
if ($instructions) {
echo "Instructions: {$instructions}\n";
}
Working with Tools
Listing Tools
$toolsResult = $client->listTools();
foreach ($toolsResult->tools as $tool) {
echo "- {$tool->name}: {$tool->description}\n";
}
// Handle pagination
if ($toolsResult->nextCursor) {
$moreTools = $client->listTools($toolsResult->nextCursor);
}
Calling Tools
$result = $client->callTool(
name: 'calculate',
arguments: ['a' => 5, 'b' => 3, 'operation' => 'add'],
);
// Access results
foreach ($result->content as $content) {
if ($content instanceof TextContent) {
echo $content->text;
}
}
Progress Notifications
Hook into tool execution progress (if server supports it):
$result = $client->callTool(
name: 'long_running_task',
arguments: ['data' => 'large_dataset'],
onProgress: static function (float $progress, ?float $total, ?string $message) {
$percent = $total > 0 ? round(($progress / $total) * 100) : 0;
echo "Progress: {$percent}% - {$message}\n";
}
);
Note
Progress notifications are only received if the server sends them. The callback will not be invoked if the server doesn't support or send progress updates.
Working with Resources
Listing Resources
$resourcesResult = $client->listResources();
foreach ($resourcesResult->resources as $resource) {
echo "- {$resource->uri}: {$resource->name}\n";
}
Listing Resource Templates
$templatesResult = $client->listResourceTemplates();
foreach ($templatesResult->resourceTemplates as $template) {
echo "- {$template->uriTemplate}: {$template->name}\n";
}
Reading Resources
$resourceResult = $client->readResource('config://app/settings');
foreach ($resourceResult->contents as $content) {
if ($content instanceof TextResourceContents) {
echo "Text: {$content->text}\n";
} elseif ($content instanceof BlobResourceContents) {
echo "Binary data (base64): {$content->blob}\n";
}
}
Resources also support progress notifications:
$result = $client->readResource(
uri: 'file://large-file.bin',
onProgress: static function (float $progress, ?float $total, ?string $message) {
echo "Reading: {$progress}/{$total} bytes\n";
}
);
Working with Prompts
Listing Prompts
$promptsResult = $client->listPrompts();
foreach ($promptsResult->prompts as $prompt) {
echo "- {$prompt->name}: {$prompt->description}\n";
}
Getting Prompts
$promptResult = $client->getPrompt(
name: 'code_review',
arguments: ['language' => 'php', 'code' => '...'],
);
foreach ($promptResult->messages as $message) {
echo "{$message->role->value}: {$message->content->text}\n";
}
Prompts also support progress notifications:
$result = $client->getPrompt(
name: 'generate_report',
arguments: ['topic' => 'quarterly_analysis'],
onProgress: static function (float $progress, ?float $total, ?string $message) {
echo "Generating: {$message}\n";
}
);
Requesting Completions
Request auto-completion suggestions for prompt or resource arguments:
use Mcp\Schema\PromptReference;
$completionResult = $client->complete(
ref: new PromptReference('code_review'),
argument: ['name' => 'language', 'value' => 'ph'],
);
foreach ($completionResult->values as $value) {
echo "Suggestion: {$value}\n";
}
Server-Initiated Communication
The client can receive requests and notifications from the server when configured with appropriate handlers.
Logging Notifications
Receive structured log messages from the server:
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
use Mcp\Schema\Notification\LoggingMessageNotification;
use Mcp\Schema\Enum\LoggingLevel;
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) {
// Route to your application's logging system
$level = $notification->level;
$message = $notification->data;
match ($level) {
LoggingLevel::Debug => logger()->debug($message),
LoggingLevel::Info => logger()->info($message),
LoggingLevel::Warning => logger()->warning($message),
LoggingLevel::Error => logger()->error($message),
default => logger()->info($message),
};
}
);
$client = Client::builder()
->addNotificationHandler($loggingHandler)
->build();
// Set minimum log level (optional)
$client->setLoggingLevel(LoggingLevel::Info);
Sampling (LLM Requests)
Handle server requests for LLM completions:
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
use Mcp\Exception\SamplingException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Request\CreateSamplingMessageRequest;
use Mcp\Schema\Result\CreateSamplingMessageResult;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\Role;
class LlmSamplingCallback implements SamplingCallbackInterface
{
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
{
try {
// Call your LLM provider
$response = $this->llmClient->complete(
messages: $request->messages,
maxTokens: $request->maxTokens,
temperature: $request->temperature ?? 0.7,
);
return new CreateSamplingMessageResult(
role: Role::Assistant,
content: new TextContent($response->text),
model: $response->model,
stopReason: $response->stopReason,
);
} catch (\Throwable $e) {
// Throw SamplingException to surface error to server
throw new SamplingException(
"LLM sampling failed: {$e->getMessage()}",
(int) $e->getCode(),
$e
);
}
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(sampling: true))
->addRequestHandler(new SamplingRequestHandler(new LlmSamplingCallback))
->build();
Important
Error Handling in Sampling Callbacks:
When implementing sampling callbacks, error handling is critical:
- Throw
SamplingExceptionto forward specific error messages to the server - Any other exception will be logged but return a generic error to the server
This distinction allows you to control what error information the server receives:
// Good: Server receives "Rate limit exceeded" message
throw new SamplingException('Rate limit exceeded. Retry after 60 seconds.');
// Bad: Server receives generic "Error while sampling LLM" message
throw new \RuntimeException('Rate limit exceeded');
Error Handling
The client throws exceptions for various error conditions:
ConnectionException
Thrown when connection or initialization fails:
use Mcp\Exception\ConnectionException;
try {
$client->connect($transport);
} catch (ConnectionException $e) {
echo "Failed to connect: {$e->getMessage()}\n";
}
RequestException
Thrown when a request returns an error response:
use Mcp\Exception\RequestException;
try {
$result = $client->callTool('unknown_tool', []);
} catch (RequestException $e) {
echo "Request failed: {$e->getMessage()}\n";
echo "Error code: {$e->getCode()}\n";
}
Complete Example
Here's a comprehensive example demonstrating client usage:
<?php
use Mcp\Client;
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Client\Transport\StdioTransport;
use Mcp\Exception\SamplingException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\Enum\Role;
use Mcp\Schema\Notification\LoggingMessageNotification;
use Mcp\Schema\Request\CreateSamplingMessageRequest;
use Mcp\Schema\Result\CreateSamplingMessageResult;
// Configure logging notification handler
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) {
echo "[LOG {$notification->level->value}] {$notification->data}\n";
}
);
// Configure sampling callback
$samplingCallback = new class implements SamplingCallbackInterface {
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
{
echo "[SAMPLING] Processing request (max {$request->maxTokens} tokens)\n";
try {
// Integration with your LLM provider
$response = "This is a mock LLM response for: " .
json_encode($request->messages);
return new CreateSamplingMessageResult(
role: Role::Assistant,
content: new TextContent($response),
model: 'mock-llm',
stopReason: 'end_turn',
);
} catch (\Throwable $e) {
throw new SamplingException(
"Sampling failed: {$e->getMessage()}",
0,
$e
);
}
}
};
// Build client
$client = Client::builder()
->setClientInfo('Example Client', '1.0.0')
->setInitTimeout(30)
->setRequestTimeout(120)
->setCapabilities(new ClientCapabilities(sampling: true))
->addNotificationHandler($loggingHandler)
->addRequestHandler(new SamplingRequestHandler($samplingCallback))
->build();
// Create transport
$transport = new StdioTransport(
command: 'php',
args: [__DIR__ . '/server.php'],
);
// Connect and use server
try {
echo "Connecting to server...\n";
$client->connect($transport);
// Get server info
$serverInfo = $client->getServerInfo();
echo "Connected to: {$serverInfo->name} v{$serverInfo->version}\n\n";
// List capabilities
echo "Available tools:\n";
$tools = $client->listTools();
foreach ($tools->tools as $tool) {
echo " - {$tool->name}\n";
}
echo "\nAvailable resources:\n";
$resources = $client->listResources();
foreach ($resources->resources as $resource) {
echo " - {$resource->uri}\n";
}
// Set logging level
$client->setLoggingLevel(LoggingLevel::Debug);
// Call tool with progress
echo "\nCalling tool with progress...\n";
$result = $client->callTool(
name: 'process_data',
arguments: ['dataset' => 'large_file.csv'],
onProgress: static function (float $progress, ?float $total, ?string $message) {
$percent = $total > 0 ? round(($progress / $total) * 100) : 0;
echo " Progress: {$percent}% - {$message}\n";
}
);
echo "\nResult:\n";
foreach ($result->content as $content) {
if ($content instanceof TextContent) {
echo $content->text . "\n";
}
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
echo $e->getTraceAsString() . "\n";
} finally {
$client->disconnect();
echo "\nDisconnected.\n";
}