From ef8b390f0d2bc8fe3477e85738aeb17200c4bb68 Mon Sep 17 00:00:00 2001 From: CodeWithKyrian <48791154+CodeWithKyrian@users.noreply.github.com> Date: Tue, 6 May 2025 18:02:11 +0000 Subject: [PATCH 1/3] Update CHANGELOG --- CHANGELOG.md | 136 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad8b3e..c47d2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to `php-mcp/client` will be documented in this file. +## v1.0.1 - 2025-05-06 + +This patch release introduces an API improvement to the `ClientBuilder` for a more streamlined configuration experience. + +### ✨ Enhancements + +* **Simplified Client Identity Configuration:** The `ClientBuilder` now features a new method `withClientInfo(string $name, string $version)`. This single method replaces the previous separate `withName(string $name)` and `withVersion(string $version)` methods for setting your client application's identity. This makes the builder API slightly more concise. + +### ⚠️ Deprecations + +* The methods `ClientBuilder::withName(string $name)` and `ClientBuilder::withVersion(string $version)` are now **deprecated**. +* Please update your code to use the new `ClientBuilder::withClientInfo(string $name, string $version)` method. +* The deprecated methods will be removed in a future `v2.0.0` release. They will continue to function as before in the `v1.x.x` series to maintain backward compatibility. + +### 📝 Documentation + +* The `README.md` and examples have been updated to reflect the new `withClientInfo()` method and the removal of the `ClientInfo` object from direct user configuration. + ## v1.0.0 - 2025-05-06 ### v1.0.0 - Initial Release @@ -15,81 +33,93 @@ It offers a robust, flexible, and developer-friendly solution designed specifica #### ✨ Key Features * **Client-per-Server Architecture:** Aligns with the MCP specification's core model where each `Client` instance manages a dedicated, stateful connection to a single configured MCP server. + * **Fluent Configuration Builder:** Set up client instances easily using the `Client::make()->with...()->build()` pattern, configuring client identity, capabilities, and the specific server connection details (`ServerConfig`). + * **Dual API for Flexibility:** - * **Synchronous Facade:** Interact with MCP servers using straightforward, blocking methods (e.g., `$client->initialize()`, `$client->listTools()`, `$client->callTool(...)`) for simple integration into traditional PHP scripts and frameworks. The underlying asynchronous complexity is handled internally. - * **Asynchronous API:** Access Promise-based methods (e.g., `$client->initializeAsync()`, `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for advanced use cases, concurrent operations using `React\Promise\all`, or integration into asynchronous PHP applications (ReactPHP, Amp, Swoole). - + + * **Synchronous Facade:** Interact with MCP servers using straightforward, blocking methods (e.g., `$client->initialize()`, `$client->listTools()`, `$client->callTool(...)`) for simple integration into traditional PHP scripts and frameworks. The underlying asynchronous complexity is handled internally. + * **Asynchronous API:** Access Promise-based methods (e.g., `$client->initializeAsync()`, `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for advanced use cases, concurrent operations using `React\Promise\all`, or integration into asynchronous PHP applications (ReactPHP, Amp, Swoole). + * **Multiple Transport Support:** - * **`stdio`:** Seamlessly connect to and manage local MCP server processes via standard input/output, ideal for command-line tools or embedded servers. Uses `react/child-process` internally. - * **`http`:** Connect to remote or local MCP servers over HTTP, handling POST requests for client messages and Server-Sent Events (SSE) for server messages and notifications. Uses `react/http` internally. - + + * **`stdio`:** Seamlessly connect to and manage local MCP server processes via standard input/output, ideal for command-line tools or embedded servers. Uses `react/child-process` internally. + * **`http`:** Connect to remote or local MCP servers over HTTP, handling POST requests for client messages and Server-Sent Events (SSE) for server messages and notifications. Uses `react/http` internally. + * **Explicit Connection Lifecycle:** Clear methods (`initialize`/`initializeAsync`, `disconnect`/`disconnectAsync`) to manage the stateful connection and MCP handshake process. + * **Full MCP Core Feature Support:** Implements core MCP operations: - * Capability Negotiation (`initialize`) - * Listing Tools, Resources, Prompts, Resource Templates - * Calling Tools (`tools/call`) - * Reading Resources (`resources/read`) - * Getting Prompts (`prompts/get`) - * Resource Subscriptions (`resources/subscribe`, `resources/unsubscribe`) *(Requires server capability)* - * Server Log Level Control (`logging/setLevel`) *(Requires server capability)* - * Connectivity Check (`ping`) - + + * Capability Negotiation (`initialize`) + * Listing Tools, Resources, Prompts, Resource Templates + * Calling Tools (`tools/call`) + * Reading Resources (`resources/read`) + * Getting Prompts (`prompts/get`) + * Resource Subscriptions (`resources/subscribe`, `resources/unsubscribe`) *(Requires server capability)* + * Server Log Level Control (`logging/setLevel`) *(Requires server capability)* + * Connectivity Check (`ping`) + * **PSR Compliance:** - * Integrates with `PSR-3 (LoggerInterface)` for flexible logging. - * Supports optional `PSR-16 (SimpleCacheInterface)` for caching server definitions (tools, resources, etc.) via `DefinitionCache`. - * Supports optional `PSR-14 (EventDispatcherInterface)` for handling server-sent notifications (e.g., `ResourceChanged`, `ToolsListChanged`) asynchronously. - + + * Integrates with `PSR-3 (LoggerInterface)` for flexible logging. + * Supports optional `PSR-16 (SimpleCacheInterface)` for caching server definitions (tools, resources, etc.) via `DefinitionCache`. + * Supports optional `PSR-14 (EventDispatcherInterface)` for handling server-sent notifications (e.g., `ResourceChanged`, `ToolsListChanged`) asynchronously. + * **Robust Error Handling:** Provides a hierarchy of specific exceptions (`ConfigurationException`, `ConnectionException`, `HandshakeException`, `RequestException`, `TimeoutException`, `TransportException`, `UnsupportedCapabilityException`, etc.) for predictable error management. + * **Asynchronous Core:** Built on ReactPHP's event loop and promises for efficient, non-blocking I/O handling crucial for `stdio` and `http+sse` transports. + #### 🚀 Getting Started 1. **Install:** - + ```bash composer require php-mcp/client - + + ``` 2. **Configure:** Define your server connection using `ServerConfig` and build a client instance. - + ```php use PhpMcp\Client\Client; - use PhpMcp\Client\Enum\TransportType; - use PhpMcp\Client\Model\ClientInfo; - use PhpMcp\Client\ServerConfig; - - $serverConfig = new ServerConfig( - name: 'my_stdio_server', - transport: TransportType::Stdio, - command: ['php', '/path/to/your/mcp_server.php'], - timeout: 15 - ); - - $clientInfo = new ClientInfo('MyPHPApp', '1.0'); - - $client = Client::make() - ->withClientName($clientInfo->name) // Pass name/version directly - ->withClientVersion($clientInfo->version) - ->withServerConfig($serverConfig) - // ->withLogger(new MyLogger()) // Optional - ->build(); - + use PhpMcp\Client\Enum\TransportType; + use PhpMcp\Client\Model\ClientInfo; + use PhpMcp\Client\ServerConfig; + + $serverConfig = new ServerConfig( + name: 'my_stdio_server', + transport: TransportType::Stdio, + command: ['php', '/path/to/your/mcp_server.php'], + timeout: 15 + ); + + $clientInfo = new ClientInfo('MyPHPApp', '1.0'); + + $client = Client::make() + ->withClientName($clientInfo->name) // Pass name/version directly + ->withClientVersion($clientInfo->version) + ->withServerConfig($serverConfig) + // ->withLogger(new MyLogger()) // Optional + ->build(); + + ``` 3. **Initialize & Interact (Sync Example):** - + ```php try { - $client->initialize(); // Connect & Handshake (blocks) - $tools = $client->listTools(); // Make request (blocks) - print_r($tools); - // ... other interactions ... - } catch (\Throwable $e) { - echo "Error: " . $e->getMessage(); - } finally { - $client->disconnect(); // Disconnect (blocks) - } - + $client->initialize(); // Connect & Handshake (blocks) + $tools = $client->listTools(); // Make request (blocks) + print_r($tools); + // ... other interactions ... + } catch (\Throwable $e) { + echo "Error: " . $e->getMessage(); + } finally { + $client->disconnect(); // Disconnect (blocks) + } + + ``` #### Documentation & Examples From 87bc19491133e8b7f556e4875f341ce81a366fdc Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Wed, 7 May 2025 01:18:49 +0100 Subject: [PATCH 2/3] chore: Update README.md Fix sse url and comment for http server config --- README.md | 865 +++++++++++++++++++++++++++--------------------------- 1 file changed, 432 insertions(+), 433 deletions(-) diff --git a/README.md b/README.md index e6d5a54..e60184c 100644 --- a/README.md +++ b/README.md @@ -1,436 +1,435 @@ -# PHP MCP Client - -[![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) -[![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) -[![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/client/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/client/actions/workflows/tests.yml) -[![License](https://img.shields.io/packagist/l/php-mcp/client.svg?style=flat-square)](LICENSE) - -**PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).** - -It provides a developer-friendly interface to connect to individual MCP servers using different transports (`stdio`, `http+sse`), manage the connection lifecycle, discover server capabilities (Tools, Resources, Prompts), and execute requests like calling tools or reading resources. - -While utilizing asynchronous I/O internally via ReactPHP for robustness and handling features like server-sent events, the library offers **both** a straightforward **synchronous (blocking) API** for common use cases and an **asynchronous (Promise-based) API** for advanced control and concurrency. - -This library aligns with the MCP specification's model where one client instance manages a stateful connection to one server. - -## Introduction to MCP - -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open standard designed to standardize how AI assistants and applications connect to external data sources, APIs, and tools (like codebases, databases, web browsers). It acts as a communication layer, enabling AI models (like Claude, or models integrated via frameworks like OpenAI's) to securely access and interact with context provided by different servers. - -This client library allows your PHP application (acting as the "Host" in MCP terminology) to *consume* capabilities offered by one or more MCP servers. - -## Features - -* **Client-per-Server Model:** Each `Client` instance manages a stateful connection to a single configured MCP server, aligning with the MCP specification. -* **Fluent Configuration:** Easy setup for each client instance using a `Client::make()->with...()` builder pattern. -* **Dual API:** - * **Synchronous Facade:** Interact with the server using straightforward, blocking methods (e.g., `$client->listTools()`, `$client->callTool(...)`) for simple integration. - * **Asynchronous API:** Access underlying Promise-based methods (e.g., `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for concurrency and integration with async PHP applications. -* **Multiple Transports:** Built-in support for: - * `stdio`: Communicating with server processes via Standard Input/Output. - * `http`: Communicating with servers via HTTP POST and Server-Sent Events (SSE). -* **Explicit Connection Lifecycle:** Requires `->initialize()` or `->initializeAsync()` to connect and perform the handshake before making requests. Provides `disconnect()` / `disconnectAsync()`. -* **Tool/Resource/Prompt Interaction:** Provides comprehensive methods (sync & async) to list available elements and execute requests like `tools/call`, `resources/read`, `prompts/get`. -* **PSR Compliance:** Integrates with standard PHP interfaces: - * `PSR-3` (LoggerInterface): Integrate your application's logger. - * `PSR-16` (SimpleCacheInterface): Optional caching for server definitions. - * `PSR-14` (EventDispatcherInterface): Optional handling of server-sent notifications via events (requires async handling). -* **Robust Error Handling:** Specific exceptions for different failure modes. -* **Asynchronous Core:** Utilizes ReactPHP internally for non-blocking I/O. - -## Requirements - -* PHP >= 8.1 -* Composer -* *(For Stdio Transport)*: Ability to execute the server command. -* *(For Http Transport)*: Network access to the MCP server URL. - -## Installation - -Install the package via Composer: - -```bash -composer require php-mcp/client -``` - -The necessary ReactPHP dependencies (`event-loop`, `promise`, `stream`, `child-process`, `http`) should be installed automatically. - -## Quick Start: Simple Synchronous Usage (Stdio) - -This example connects to a local filesystem server running via `npx`. - -```php -withClientInfo('MyFileSystemApp', '1.0') - ->withCapabilities($clientCapabilities) - // ->withLogger(new MyPsrLogger()) // Optional - ->withServerConfig($fsServerConfig) - ->build(); - -try { - // Initialize Connection (BLOCKING) - $fsClient->initialize(); - - // Interact (Synchronously) - $tools = $fsClient->listTools(); // Blocking call - foreach ($tools as $tool) { - echo "- Tool: {$tool->name}\n"; - } - - // ... Call other methods like $fsClient->callTool(...) ... - -} catch (McpClientException $e) { - echo "[MCP ERROR] " . get_class($e) . ": " . $e->getMessage() . "\n"; - // Check $e->getPrevious() for underlying transport/process errors -} catch (\Throwable $e) { - echo "[UNEXPECTED ERROR] " . $e->getMessage() . "\n"; -} finally { - // Disconnect (BLOCKING) - if (isset($fsClient)) { - $fsClient->disconnect(); - } -} -``` - -## Configuration - -Configuration involves setting up: - -1. **Client Identity:** Your application's name and version, passed directly to the builder. -2. **Client Capabilities:** Features your client supports using `ClientCapabilities`. -3. **Server Connection:** Details for the *single server* this client instance will connect to, using `ServerConfig`. -4. **(Optional) Dependencies:** Logger, Cache, Event Dispatcher, Event Loop. - -### `ClientCapabilities` - -Declares features your client supports. Use the static factory method. - -```php -use PhpMcp\Client\Model\Capabilities as ClientCapabilities; - -// Client supports sampling requests from the server -$clientCapabilities = ClientCapabilities::forClient(supportsSampling: true); - -// Client does NOT support sampling -$clientCapabilities = ClientCapabilities::forClient(supportsSampling: false); - -// TODO: Add support for declaring 'roots' capability if needed -``` - -### `ServerConfig` - -Defines how to connect to a *single* MCP server. - -```php -use PhpMcp\Client\Enum\TransportType; -use PhpMcp\Client\ServerConfig; - -// Example: Stdio Server -$stdioConfig = new ServerConfig( - name: 'local_file_server', // Required: Unique ID for this config - transport: TransportType::Stdio, // Required: Transport type - timeout: 15.0, // Optional: Request timeout (seconds) - command: 'npx', // Required for Stdio: Executable - args: [ // Optional for Stdio: Arguments array - '-y', - '@modelcontextprotocol/server-filesystem', - '/path/to/project' - ], - workingDir: '/path/to/project', // Optional for Stdio: Working directory - env: ['DEBUG' => 'mcp*'] // Optional for Stdio: Environment variables -); - -// Example: HTTP Server -$httpConfig = new ServerConfig( - name: 'remote_web_agent', // Required: Unique ID - transport: TransportType::Http, // Required: Transport type - timeout: 45.0, // Optional: Request timeout - url: 'http://localhost:8080/', // Required for Http: Base URL (NO /sse) - headers: [ // Optional for Http: Auth/Custom headers - 'Authorization' => 'Bearer xyz789' - ], - // sessionId: 'sess_abcdef' // Optional for Http: External session ID -); -``` - -### Loading Config from Array/JSON - -You can easily parse configurations stored in arrays (e.g., from JSON files or framework config). - -```php -use PhpMcp\Client\ServerConfig; -use PhpMcp\Client\Exception\ConfigurationException; - -$jsonConfig = '{ - "mcpServers": { - "stdio_files": { - "command": "php", - "args": ["/app/mcp/file_server.php"], - "timeout": 10 - }, - "http_api": { - "url": "https://api.example.com/mcp", - "transport": "http", - "headers": {"X-API-Key": "secret"} - } - } -}'; - -$decodedConfig = json_decode($jsonConfig, true)['mcpServers'] ?? []; - -$serverConfigs = []; -foreach ($decodedConfig as $name => $data) { - try { - $serverConfigs[$name] = ServerConfig::fromArray($name, $data); - } catch (ConfigurationException $e) { - echo "Error parsing config for '{$name}': {$e->getMessage()}\n"; - } -} - -// Now $serverConfigs['stdio_files'] and $serverConfigs['http_api'] -// contain ServerConfig objects. -``` - -### `ClientBuilder` - -Use the builder to assemble the `Client` instance: - -```php -use PhpMcp\Client\Client; -// ... other use statements for Config, Logger etc... - -$client = Client::make() - ->withClientInfo($clientName, $clientVersion) // Required - ->withCapabilities($clientCapabilities) // Optional (defaults provided) - ->withServerConfig($stdioConfig) // Required: Config for THE server - ->withLogger($myLogger) // Optional - ->withCache($myCache, 3600) // Optional (cache + TTL) - ->withEventDispatcher($myDispatcher) // Optional - ->withIdGenerator($myIdGenerator) // Optional - ->withLoop($myEventLoop) // Optional (defaults to Loop::get()) - ->build(); -``` - -## Usage - -Once you have a configured `Client` instance for a specific server: - -**1. Initialize the Connection:** - -You *must* call `initialize()` or `initializeAsync()` before making requests. - -```php -// Synchronous (Blocking) -try { - $client->initialize(); // Connects, performs handshake, waits until ready - echo "Connection Ready!"; -} catch (Throwable $e) { - echo "Initialization failed: " . $e->getMessage(); - // Handle error... client is likely in Error state -} - -// Asynchronous (Promise-based) -$client->initializeAsync()->then( - function(Client $readyClient) { /* Ready */ }, - function(Throwable $error) { /* Handle init failure */ } -); -// Requires running the event loop ($client->getLoop()->run()) -``` - -**2. Making Requests:** - -Use the client methods. They operate on the single connection established by `initialize()`. - -* **Synchronous API (Recommended for simple scripts/frameworks):** - * Methods like `listTools()`, `callTool()`, `readResource()` block execution until a response is received or a timeout occurs. - * They return the parsed result object (e.g., `array`, `CallToolResult`) or throw an exception (`TimeoutException`, `RequestException`, `ConnectionException`, etc.). - - ```php - try { - if ($client->isReady()) { // Check status - $tools = $client->listTools(); - $result = $client->callTool('myTool', ['param' => 'value']); + # PHP MCP Client + + [![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) + [![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) + [![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/client/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/client/actions/workflows/tests.yml) + [![License](https://img.shields.io/packagist/l/php-mcp/client.svg?style=flat-square)](LICENSE) + + **PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).** + + It provides a developer-friendly interface to connect to individual MCP servers using different transports (`stdio`, `http+sse`), manage the connection lifecycle, discover server capabilities (Tools, Resources, Prompts), and execute requests like calling tools or reading resources. + + While utilizing asynchronous I/O internally via ReactPHP for robustness and handling features like server-sent events, the library offers **both** a straightforward **synchronous (blocking) API** for common use cases and an **asynchronous (Promise-based) API** for advanced control and concurrency. + + This library aligns with the MCP specification's model where one client instance manages a stateful connection to one server. + + ## Introduction to MCP + + The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open standard designed to standardize how AI assistants and applications connect to external data sources, APIs, and tools (like codebases, databases, web browsers). It acts as a communication layer, enabling AI models (like Claude, or models integrated via frameworks like OpenAI's) to securely access and interact with context provided by different servers. + + This client library allows your PHP application (acting as the "Host" in MCP terminology) to *consume* capabilities offered by one or more MCP servers. + + ## Features + + * **Client-per-Server Model:** Each `Client` instance manages a stateful connection to a single configured MCP server, aligning with the MCP specification. + * **Fluent Configuration:** Easy setup for each client instance using a `Client::make()->with...()` builder pattern. + * **Dual API:** + * **Synchronous Facade:** Interact with the server using straightforward, blocking methods (e.g., `$client->listTools()`, `$client->callTool(...)`) for simple integration. + * **Asynchronous API:** Access underlying Promise-based methods (e.g., `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for concurrency and integration with async PHP applications. + * **Multiple Transports:** Built-in support for: + * `stdio`: Communicating with server processes via Standard Input/Output. + * `http`: Communicating with servers via HTTP POST and Server-Sent Events (SSE). + * **Explicit Connection Lifecycle:** Requires `->initialize()` or `->initializeAsync()` to connect and perform the handshake before making requests. Provides `disconnect()` / `disconnectAsync()`. + * **Tool/Resource/Prompt Interaction:** Provides comprehensive methods (sync & async) to list available elements and execute requests like `tools/call`, `resources/read`, `prompts/get`. + * **PSR Compliance:** Integrates with standard PHP interfaces: + * `PSR-3` (LoggerInterface): Integrate your application's logger. + * `PSR-16` (SimpleCacheInterface): Optional caching for server definitions. + * `PSR-14` (EventDispatcherInterface): Optional handling of server-sent notifications via events (requires async handling). + * **Robust Error Handling:** Specific exceptions for different failure modes. + * **Asynchronous Core:** Utilizes ReactPHP internally for non-blocking I/O. + + ## Requirements + + * PHP >= 8.1 + * Composer + * *(For Stdio Transport)*: Ability to execute the server command. + * *(For Http Transport)*: Network access to the MCP server URL. + + ## Installation + + Install the package via Composer: + + ```bash + composer require php-mcp/client + ``` + + The necessary ReactPHP dependencies (`event-loop`, `promise`, `stream`, `child-process`, `http`) should be installed automatically. + + ## Quick Start: Simple Synchronous Usage (Stdio) + + This example connects to a local filesystem server running via `npx`. + + ```php + withClientInfo('MyFileSystemApp', '1.0') + ->withCapabilities($clientCapabilities) + // ->withLogger(new MyPsrLogger()) // Optional + ->withServerConfig($fsServerConfig) + ->build(); + + try { + // Initialize Connection (BLOCKING) + $fsClient->initialize(); + + // Interact (Synchronously) + $tools = $fsClient->listTools(); // Blocking call + foreach ($tools as $tool) { + echo "- Tool: {$tool->name}\n"; + } + + // ... Call other methods like $fsClient->callTool(...) ... + + } catch (McpClientException $e) { + echo "[MCP ERROR] " . get_class($e) . ": " . $e->getMessage() . "\n"; + // Check $e->getPrevious() for underlying transport/process errors + } catch (\Throwable $e) { + echo "[UNEXPECTED ERROR] " . $e->getMessage() . "\n"; + } finally { + // Disconnect (BLOCKING) + if (isset($fsClient)) { + $fsClient->disconnect(); + } } - } catch (Throwable $e) { /* Handle errors */ } - ``` - -* **Asynchronous API (For async applications or concurrent requests):** - * Methods like `listToolsAsync()`, `callToolAsync()`, `readResourceAsync()` return a `React\Promise\PromiseInterface`. - * You need to use promise methods (`then`, `catch`, `finally`) or `React\Async\await` (in a Fiber context) to handle the results. - * Requires the event loop to be running. - - ```php - use function React\Promise\all; - - if ($client->isReady()) { - $p1 = $client->listToolsAsync(); - $p2 = $client->readResourceAsync('config://settings'); - - all([$p1, $p2])->then( - function(array $results) { - [$tools, $readResult] = $results; - // Process async results... - }, - function(Throwable $error) { - // Handle async error... + ``` + + ## Configuration + + Configuration involves setting up: + + 1. **Client Identity:** Your application's name and version, passed directly to the builder. + 2. **Client Capabilities:** Features your client supports using `ClientCapabilities`. + 3. **Server Connection:** Details for the *single server* this client instance will connect to, using `ServerConfig`. + 4. **(Optional) Dependencies:** Logger, Cache, Event Dispatcher, Event Loop. + + ### `ClientCapabilities` + + Declares features your client supports. Use the static factory method. + + ```php + use PhpMcp\Client\Model\Capabilities as ClientCapabilities; + + // Client supports sampling requests from the server + $clientCapabilities = ClientCapabilities::forClient(supportsSampling: true); + + // Client does NOT support sampling + $clientCapabilities = ClientCapabilities::forClient(supportsSampling: false); + + // TODO: Add support for declaring 'roots' capability if needed + ``` + + ### `ServerConfig` + + Defines how to connect to a *single* MCP server. + + ```php + use PhpMcp\Client\Enum\TransportType; + use PhpMcp\Client\ServerConfig; + + // Example: Stdio Server + $stdioConfig = new ServerConfig( + name: 'local_file_server', // Required: Unique ID for this config + transport: TransportType::Stdio, // Required: Transport type + timeout: 15.0, // Optional: Request timeout (seconds) + command: 'npx', // Required for Stdio: Executable + args: [ // Optional for Stdio: Arguments array + '-y', + '@modelcontextprotocol/server-filesystem', + '/path/to/project' + ], + workingDir: '/path/to/project', // Optional for Stdio: Working directory + env: ['DEBUG' => 'mcp*'] // Optional for Stdio: Environment variables + ); + + // Example: HTTP Server + $httpConfig = new ServerConfig( + name: 'remote_web_agent', // Required: Unique ID + transport: TransportType::Http, // Required: Transport type + timeout: 45.0, // Optional: Request timeout + url: 'http://localhost:8080/sse',// Required for Http: SSE URL + headers: [ // Optional for Http: Auth/Custom headers + 'Authorization' => 'Bearer xyz789' + ], + ); + ``` + + ### Loading Config from Array/JSON + + You can easily parse configurations stored in arrays (e.g., from JSON files or framework config). + + ```php + use PhpMcp\Client\ServerConfig; + use PhpMcp\Client\Exception\ConfigurationException; + + $jsonConfig = '{ + "mcpServers": { + "stdio_files": { + "command": "php", + "args": ["/app/mcp/file_server.php"], + "timeout": 10 + }, + "http_api": { + "url": "https://api.example.com/mcp/sse", + "transport": "http", + "headers": {"X-API-Key": "secret"} + } + } + }'; + + $decodedConfig = json_decode($jsonConfig, true)['mcpServers'] ?? []; + + $serverConfigs = []; + foreach ($decodedConfig as $name => $data) { + try { + $serverConfigs[$name] = ServerConfig::fromArray($name, $data); + } catch (ConfigurationException $e) { + echo "Error parsing config for '{$name}': {$e->getMessage()}\n"; } + } + + // Now $serverConfigs['stdio_files'] and $serverConfigs['http_api'] + // contain ServerConfig objects. + ``` + + ### `ClientBuilder` + + Use the builder to assemble the `Client` instance: + + ```php + use PhpMcp\Client\Client; + // ... other use statements for Config, Logger etc... + + $client = Client::make() + ->withClientInfo($clientName, $clientVersion) // Required + ->withCapabilities($clientCapabilities) // Optional (defaults provided) + ->withServerConfig($stdioConfig) // Required: Config for THE server + ->withLogger($myLogger) // Optional + ->withCache($myCache, 3600) // Optional (cache + TTL) + ->withEventDispatcher($myDispatcher) // Optional + ->withIdGenerator($myIdGenerator) // Optional + ->withLoop($myEventLoop) // Optional (defaults to Loop::get()) + ->build(); + ``` + + ## Usage + + Once you have a configured `Client` instance for a specific server: + + **1. Initialize the Connection:** + + You *must* call `initialize()` or `initializeAsync()` before making requests. + + ```php + // Synchronous (Blocking) + try { + $client->initialize(); // Connects, performs handshake, waits until ready + echo "Connection Ready!"; + } catch (Throwable $e) { + echo "Initialization failed: " . $e->getMessage(); + // Handle error... client is likely in Error state + } + + // Asynchronous (Promise-based) + $client->initializeAsync()->then( + function(Client $readyClient) { /* Ready */ }, + function(Throwable $error) { /* Handle init failure */ } ); - // $client->getLoop()->run(); // Need to run the loop - } - ``` - -**3. Disconnecting:** - -Always disconnect when you are finished interacting with a server to release resources (especially for `stdio` transports). - -```php -// Synchronous -$client->disconnect(); // Blocks until closed or timeout - -// Asynchronous -$client->disconnectAsync()->then(function() { echo "Disconnected async"; }); -// $loop->run(); -``` - -## Available Client Methods - -The `Client` class provides methods for interacting with the connected MCP server. Most methods have both a synchronous (blocking) and an asynchronous (Promise-returning) variant. - -**Connection & Lifecycle:** - -* **(Sync)** `initialize(): self` - Connects to the server and performs the MCP handshake. Blocks until ready or throws an exception. Returns the client instance. -* **(Async)** `initializeAsync(): PromiseInterface` - Initiates connection and handshake asynchronously. Returns a promise resolving with the client instance when ready, or rejecting on failure. -* **(Sync)** `disconnect(): void` - Closes the connection gracefully. Blocks until disconnection is complete or times out. -* **(Async)** `disconnectAsync(): PromiseInterface` - Initiates graceful disconnection asynchronously. Returns a promise resolving when disconnection is complete. -* `getStatus(): ConnectionStatus` - Returns the current connection status enum (`Disconnected`, `Connecting`, `Handshaking`, `Ready`, `Closing`, `Closed`, `Error`). -* `isReady(): bool` - Helper method, returns `true` if status is `Ready`. -* `getServerName(): ?string` - Returns the name of the server (available after successful initialization). -* `getServerVersion(): ?string` - Returns the version of the server (available after successful initialization). -* `getNegotiatedCapabilities(): ?Capabilities` - Returns the capabilities negotiated with the server (available after successful initialization). -* `getNegotiatedProtocolVersion(): ?string` - Returns the protocol version agreed upon with the server (available after successful initialization). - -**MCP Operations (Sync):** - -*(These methods require the client to be initialized first and will block)* - -* `ping(): void` -* `listTools(bool $useCache = true): array` -* `listResources(bool $useCache = true): array` -* `listPrompts(bool $useCache = true): array` -* `listResourceTemplates(bool $useCache = true): array` -* `callTool(string $toolName, array $arguments = []): CallToolResult` -* `readResource(string $uri): ReadResourceResult` -* `getPrompt(string $promptName, array $arguments = []): GetPromptResult` -* `subscribeResource(string $uri): void` -* `unsubscribeResource(string $uri): void` -* `setLogLevel(string $level): void` - -**MCP Operations (Async):** - -*(These methods require the client to be initialized first and return `React\Promise\PromiseInterface`)* - -* `pingAsync(): PromiseInterface` -* `listToolsAsync(): PromiseInterface>` -* `listResourcesAsync(): PromiseInterface>` -* `listPromptsAsync(): PromiseInterface>` -* `listResourceTemplatesAsync(): PromiseInterface>` -* `callToolAsync(string $toolName, array $arguments = []): PromiseInterface` -* `readResourceAsync(string $uri): PromiseInterface` -* `getPromptAsync(string $promptName, array $arguments = []): PromiseInterface` -* `subscribeResourceAsync(string $uri): PromiseInterface` -* `unsubscribeResourceAsync(string $uri): PromiseInterface` -* `setLogLevelAsync(string $level): PromiseInterface` - -**Advanced:** - -* `getLoop(): LoopInterface` - Access the underlying ReactPHP event loop instance. - -## Handling Server Notifications (Asynchronous Only) - -MCP servers can send notifications (e.g., `resources/didChange`). To receive these: - -1. Configure the client with a PSR-14 `EventDispatcherInterface` using `->withEventDispatcher(...)`. -2. Add listeners to your dispatcher for events like `PhpMcp\Client\Event\ResourceChanged`. -3. Use the **asynchronous API** (`initializeAsync`, potentially other `*Async` methods). -4. **Run the event loop continuously** (`$client->getLoop()->run()`). Notifications arrive via the underlying transport (usually SSE) only while the loop is active. - -See `examples/04-handling-notifications.php` for a conceptual guide. - -## Error Handling - -The client uses specific exceptions inheriting from `PhpMcp\Client\Exception\McpClientException`. Catching these allows for targeted error handling: - -* **`ConfigurationException`**: Thrown during `ClientBuilder::build()` or `ServerConfig::fromArray()` if the provided configuration is invalid or missing required fields (e.g., missing `command` for stdio, invalid `url` for http). -* **`ConnectionException`**: Thrown by `initialize()` or `initializeAsync()` if the underlying transport connection fails (e.g., stdio process cannot start, TCP connection refused for HTTP, invalid initial response). Also thrown by request methods if called when the client is not in a `Ready` state or if the connection drops unexpectedly during an operation. Check `$e->getPrevious()` for lower-level transport or system errors. -* **`HandshakeException` (Subclass of `ConnectionException`)**: Thrown specifically by `initialize()` or `initializeAsync()` if the MCP handshake phase fails after the transport connection is established (e.g., server returns an error to the `initialize` request, version mismatch, invalid capabilities received). May contain the server's `JsonRpc\Error` via `getRequestException()->getRpcError()`. -* **`TransportException`**: Indicates a low-level error during communication *after* connection (e.g., failure to write to stdio stdin, SSE stream error, unexpected data format received from transport). Often wrapped by `ConnectionException`. -* **`TimeoutException`**: Thrown by synchronous methods (`initialize`, `listTools`, `callTool`, etc.) or rejects asynchronous promises if the server does not respond within the configured `timeout` for the `ServerConfig`. Access timeout value via `$e->getTimeout()`. -* **`RequestException`**: Thrown by synchronous methods or rejects asynchronous promises when the MCP server successfully processed the request but returned a JSON-RPC error payload (e.g., method not found on server, invalid parameters for a tool, tool execution failed on server). Access the `JsonRpc\Error` object via `$e->getRpcError()` to get the code, message, and optional data from the server. -* **`UnsupportedCapabilityException`**: Thrown by methods like `subscribeResource()` or `setLogLevel()` if the connected server did not declare support for the required capability during the initial handshake. -* **`DefinitionException`**: Thrown if there's an error fetching, caching, or parsing server definitions (Tools, Resources, Prompts), often related to cache issues or invalid data structures. -* **`ProtocolException`**: Indicates a violation of the JSON-RPC 2.0 or MCP structure in messages received from the server (e.g., missing required fields, invalid types). - -Always wrap client interactions in `try...catch` blocks to handle these potential failures gracefully. - -## Examples - -See the [`examples/`](./examples/) directory for working code: - -* `01-simple-stdio-sync.php`: Demonstrates basic synchronous interaction with a `stdio` server. -* `02-simple-http-sync.php`: Demonstrates basic synchronous interaction with an `http+sse` server. -* `03-multiple-servers-sync.php`: Shows how to instantiate and use multiple `Client` objects for different servers within the same script (sequentially). -* `04-multiple-servers-async.php`: Demonstrates asynchronous interaction with multiple servers using Promises for concurrency. Requires running the event loop. -* `05-openai-php-integration-sync`: Full example integrating with `openai-php` for tool usage using the synchronous client API, including its own `composer.json` setup. - -## Testing - -```bash -composer install --dev - -composer test -``` - -## Contributing - -Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) or open an issue/pull request. - -## License - -The MIT License (MIT). See [LICENSE](LICENSE). \ No newline at end of file + // Requires running the event loop ($client->getLoop()->run()) + ``` + + **2. Making Requests:** + + Use the client methods. They operate on the single connection established by `initialize()`. + + * **Synchronous API (Recommended for simple scripts/frameworks):** + * Methods like `listTools()`, `callTool()`, `readResource()` block execution until a response is received or a timeout occurs. + * They return the parsed result object (e.g., `array`, `CallToolResult`) or throw an exception (`TimeoutException`, `RequestException`, `ConnectionException`, etc.). + + ```php + try { + if ($client->isReady()) { // Check status + $tools = $client->listTools(); + $result = $client->callTool('myTool', ['param' => 'value']); + } + } catch (Throwable $e) { /* Handle errors */ } + ``` + + * **Asynchronous API (For async applications or concurrent requests):** + * Methods like `listToolsAsync()`, `callToolAsync()`, `readResourceAsync()` return a `React\Promise\PromiseInterface`. + * You need to use promise methods (`then`, `catch`, `finally`) or `React\Async\await` (in a Fiber context) to handle the results. + * Requires the event loop to be running. + + ```php + use function React\Promise\all; + + if ($client->isReady()) { + $p1 = $client->listToolsAsync(); + $p2 = $client->readResourceAsync('config://settings'); + + all([$p1, $p2])->then( + function(array $results) { + [$tools, $readResult] = $results; + // Process async results... + }, + function(Throwable $error) { + // Handle async error... + } + ); + // $client->getLoop()->run(); // Need to run the loop + } + ``` + + **3. Disconnecting:** + + Always disconnect when you are finished interacting with a server to release resources (especially for `stdio` transports). + + ```php + // Synchronous + $client->disconnect(); // Blocks until closed or timeout + + // Asynchronous + $client->disconnectAsync()->then(function() { echo "Disconnected async"; }); + // $loop->run(); + ``` + + ## Available Client Methods + + The `Client` class provides methods for interacting with the connected MCP server. Most methods have both a synchronous (blocking) and an asynchronous (Promise-returning) variant. + + **Connection & Lifecycle:** + + * **(Sync)** `initialize(): self` + Connects to the server and performs the MCP handshake. Blocks until ready or throws an exception. Returns the client instance. + * **(Async)** `initializeAsync(): PromiseInterface` + Initiates connection and handshake asynchronously. Returns a promise resolving with the client instance when ready, or rejecting on failure. + * **(Sync)** `disconnect(): void` + Closes the connection gracefully. Blocks until disconnection is complete or times out. + * **(Async)** `disconnectAsync(): PromiseInterface` + Initiates graceful disconnection asynchronously. Returns a promise resolving when disconnection is complete. + * `getStatus(): ConnectionStatus` + Returns the current connection status enum (`Disconnected`, `Connecting`, `Handshaking`, `Ready`, `Closing`, `Closed`, `Error`). + * `isReady(): bool` + Helper method, returns `true` if status is `Ready`. + * `getServerName(): ?string` + Returns the name of the server (available after successful initialization). + * `getServerVersion(): ?string` + Returns the version of the server (available after successful initialization). + * `getNegotiatedCapabilities(): ?Capabilities` + Returns the capabilities negotiated with the server (available after successful initialization). + * `getNegotiatedProtocolVersion(): ?string` + Returns the protocol version agreed upon with the server (available after successful initialization). + + **MCP Operations (Sync):** + + *(These methods require the client to be initialized first and will block)* + + * `ping(): void` + * `listTools(bool $useCache = true): array` + * `listResources(bool $useCache = true): array` + * `listPrompts(bool $useCache = true): array` + * `listResourceTemplates(bool $useCache = true): array` + * `callTool(string $toolName, array $arguments = []): CallToolResult` + * `readResource(string $uri): ReadResourceResult` + * `getPrompt(string $promptName, array $arguments = []): GetPromptResult` + * `subscribeResource(string $uri): void` + * `unsubscribeResource(string $uri): void` + * `setLogLevel(string $level): void` + + **MCP Operations (Async):** + + *(These methods require the client to be initialized first and return `React\Promise\PromiseInterface`)* + + * `pingAsync(): PromiseInterface` + * `listToolsAsync(): PromiseInterface>` + * `listResourcesAsync(): PromiseInterface>` + * `listPromptsAsync(): PromiseInterface>` + * `listResourceTemplatesAsync(): PromiseInterface>` + * `callToolAsync(string $toolName, array $arguments = []): PromiseInterface` + * `readResourceAsync(string $uri): PromiseInterface` + * `getPromptAsync(string $promptName, array $arguments = []): PromiseInterface` + * `subscribeResourceAsync(string $uri): PromiseInterface` + * `unsubscribeResourceAsync(string $uri): PromiseInterface` + * `setLogLevelAsync(string $level): PromiseInterface` + + **Advanced:** + + * `getLoop(): LoopInterface` + Access the underlying ReactPHP event loop instance. + + ## Handling Server Notifications (Asynchronous Only) + + MCP servers can send notifications (e.g., `resources/didChange`). To receive these: + + 1. Configure the client with a PSR-14 `EventDispatcherInterface` using `->withEventDispatcher(...)`. + 2. Add listeners to your dispatcher for events like `PhpMcp\Client\Event\ResourceChanged`. + 3. Use the **asynchronous API** (`initializeAsync`, potentially other `*Async` methods). + 4. **Run the event loop continuously** (`$client->getLoop()->run()`). Notifications arrive via the underlying transport (usually SSE) only while the loop is active. + + See `examples/04-handling-notifications.php` for a conceptual guide. + + ## Error Handling + + The client uses specific exceptions inheriting from `PhpMcp\Client\Exception\McpClientException`. Catching these allows for targeted error handling: + + * **`ConfigurationException`**: Thrown during `ClientBuilder::build()` or `ServerConfig::fromArray()` if the provided configuration is invalid or missing required fields (e.g., missing `command` for stdio, invalid `url` for http). + * **`ConnectionException`**: Thrown by `initialize()` or `initializeAsync()` if the underlying transport connection fails (e.g., stdio process cannot start, TCP connection refused for HTTP, invalid initial response). Also thrown by request methods if called when the client is not in a `Ready` state or if the connection drops unexpectedly during an operation. Check `$e->getPrevious()` for lower-level transport or system errors. + * **`HandshakeException` (Subclass of `ConnectionException`)**: Thrown specifically by `initialize()` or `initializeAsync()` if the MCP handshake phase fails after the transport connection is established (e.g., server returns an error to the `initialize` request, version mismatch, invalid capabilities received). May contain the server's `JsonRpc\Error` via `getRequestException()->getRpcError()`. + * **`TransportException`**: Indicates a low-level error during communication *after* connection (e.g., failure to write to stdio stdin, SSE stream error, unexpected data format received from transport). Often wrapped by `ConnectionException`. + * **`TimeoutException`**: Thrown by synchronous methods (`initialize`, `listTools`, `callTool`, etc.) or rejects asynchronous promises if the server does not respond within the configured `timeout` for the `ServerConfig`. Access timeout value via `$e->getTimeout()`. + * **`RequestException`**: Thrown by synchronous methods or rejects asynchronous promises when the MCP server successfully processed the request but returned a JSON-RPC error payload (e.g., method not found on server, invalid parameters for a tool, tool execution failed on server). Access the `JsonRpc\Error` object via `$e->getRpcError()` to get the code, message, and optional data from the server. + * **`UnsupportedCapabilityException`**: Thrown by methods like `subscribeResource()` or `setLogLevel()` if the connected server did not declare support for the required capability during the initial handshake. + * **`DefinitionException`**: Thrown if there's an error fetching, caching, or parsing server definitions (Tools, Resources, Prompts), often related to cache issues or invalid data structures. + * **`ProtocolException`**: Indicates a violation of the JSON-RPC 2.0 or MCP structure in messages received from the server (e.g., missing required fields, invalid types). + + Always wrap client interactions in `try...catch` blocks to handle these potential failures gracefully. + + ## Examples + + See the [`examples/`](./examples/) directory for working code: + + * `01-simple-stdio-sync.php`: Demonstrates basic synchronous interaction with a `stdio` server. + * `02-simple-http-sync.php`: Demonstrates basic synchronous interaction with an `http+sse` server. + * `03-multiple-servers-sync.php`: Shows how to instantiate and use multiple `Client` objects for different servers within the same script (sequentially). + * `04-multiple-servers-async.php`: Demonstrates asynchronous interaction with multiple servers using Promises for concurrency. Requires running the event loop. + * `05-openai-php-integration-sync`: Full example integrating with `openai-php` for tool usage using the synchronous client API, including its own `composer.json` setup. + + ## Testing + + ```bash + composer install --dev + + composer test + ``` + + ## Contributing + + Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) or open an issue/pull request. + + ## License + + The MIT License (MIT). See [LICENSE](LICENSE). \ No newline at end of file From 0be2d4c23173fc379ec6b226b8ff6b5023ede03e Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Wed, 7 May 2025 01:19:56 +0100 Subject: [PATCH 3/3] feat: Update README.md skip ci --- README.md | 864 +++++++++++++++++++++++++++--------------------------- 1 file changed, 432 insertions(+), 432 deletions(-) diff --git a/README.md b/README.md index e60184c..28dadfd 100644 --- a/README.md +++ b/README.md @@ -1,435 +1,435 @@ - # PHP MCP Client - - [![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) - [![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) - [![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/client/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/client/actions/workflows/tests.yml) - [![License](https://img.shields.io/packagist/l/php-mcp/client.svg?style=flat-square)](LICENSE) - - **PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).** - - It provides a developer-friendly interface to connect to individual MCP servers using different transports (`stdio`, `http+sse`), manage the connection lifecycle, discover server capabilities (Tools, Resources, Prompts), and execute requests like calling tools or reading resources. - - While utilizing asynchronous I/O internally via ReactPHP for robustness and handling features like server-sent events, the library offers **both** a straightforward **synchronous (blocking) API** for common use cases and an **asynchronous (Promise-based) API** for advanced control and concurrency. - - This library aligns with the MCP specification's model where one client instance manages a stateful connection to one server. - - ## Introduction to MCP - - The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open standard designed to standardize how AI assistants and applications connect to external data sources, APIs, and tools (like codebases, databases, web browsers). It acts as a communication layer, enabling AI models (like Claude, or models integrated via frameworks like OpenAI's) to securely access and interact with context provided by different servers. - - This client library allows your PHP application (acting as the "Host" in MCP terminology) to *consume* capabilities offered by one or more MCP servers. - - ## Features - - * **Client-per-Server Model:** Each `Client` instance manages a stateful connection to a single configured MCP server, aligning with the MCP specification. - * **Fluent Configuration:** Easy setup for each client instance using a `Client::make()->with...()` builder pattern. - * **Dual API:** - * **Synchronous Facade:** Interact with the server using straightforward, blocking methods (e.g., `$client->listTools()`, `$client->callTool(...)`) for simple integration. - * **Asynchronous API:** Access underlying Promise-based methods (e.g., `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for concurrency and integration with async PHP applications. - * **Multiple Transports:** Built-in support for: - * `stdio`: Communicating with server processes via Standard Input/Output. - * `http`: Communicating with servers via HTTP POST and Server-Sent Events (SSE). - * **Explicit Connection Lifecycle:** Requires `->initialize()` or `->initializeAsync()` to connect and perform the handshake before making requests. Provides `disconnect()` / `disconnectAsync()`. - * **Tool/Resource/Prompt Interaction:** Provides comprehensive methods (sync & async) to list available elements and execute requests like `tools/call`, `resources/read`, `prompts/get`. - * **PSR Compliance:** Integrates with standard PHP interfaces: - * `PSR-3` (LoggerInterface): Integrate your application's logger. - * `PSR-16` (SimpleCacheInterface): Optional caching for server definitions. - * `PSR-14` (EventDispatcherInterface): Optional handling of server-sent notifications via events (requires async handling). - * **Robust Error Handling:** Specific exceptions for different failure modes. - * **Asynchronous Core:** Utilizes ReactPHP internally for non-blocking I/O. - - ## Requirements - - * PHP >= 8.1 - * Composer - * *(For Stdio Transport)*: Ability to execute the server command. - * *(For Http Transport)*: Network access to the MCP server URL. - - ## Installation - - Install the package via Composer: - - ```bash - composer require php-mcp/client - ``` - - The necessary ReactPHP dependencies (`event-loop`, `promise`, `stream`, `child-process`, `http`) should be installed automatically. - - ## Quick Start: Simple Synchronous Usage (Stdio) - - This example connects to a local filesystem server running via `npx`. - - ```php - withClientInfo('MyFileSystemApp', '1.0') - ->withCapabilities($clientCapabilities) - // ->withLogger(new MyPsrLogger()) // Optional - ->withServerConfig($fsServerConfig) - ->build(); - - try { - // Initialize Connection (BLOCKING) - $fsClient->initialize(); - - // Interact (Synchronously) - $tools = $fsClient->listTools(); // Blocking call - foreach ($tools as $tool) { - echo "- Tool: {$tool->name}\n"; - } - - // ... Call other methods like $fsClient->callTool(...) ... - - } catch (McpClientException $e) { - echo "[MCP ERROR] " . get_class($e) . ": " . $e->getMessage() . "\n"; - // Check $e->getPrevious() for underlying transport/process errors - } catch (\Throwable $e) { - echo "[UNEXPECTED ERROR] " . $e->getMessage() . "\n"; - } finally { - // Disconnect (BLOCKING) - if (isset($fsClient)) { - $fsClient->disconnect(); - } - } - ``` - - ## Configuration - - Configuration involves setting up: - - 1. **Client Identity:** Your application's name and version, passed directly to the builder. - 2. **Client Capabilities:** Features your client supports using `ClientCapabilities`. - 3. **Server Connection:** Details for the *single server* this client instance will connect to, using `ServerConfig`. - 4. **(Optional) Dependencies:** Logger, Cache, Event Dispatcher, Event Loop. - - ### `ClientCapabilities` - - Declares features your client supports. Use the static factory method. - - ```php - use PhpMcp\Client\Model\Capabilities as ClientCapabilities; - - // Client supports sampling requests from the server - $clientCapabilities = ClientCapabilities::forClient(supportsSampling: true); - - // Client does NOT support sampling - $clientCapabilities = ClientCapabilities::forClient(supportsSampling: false); - - // TODO: Add support for declaring 'roots' capability if needed - ``` - - ### `ServerConfig` - - Defines how to connect to a *single* MCP server. - - ```php - use PhpMcp\Client\Enum\TransportType; - use PhpMcp\Client\ServerConfig; - - // Example: Stdio Server - $stdioConfig = new ServerConfig( - name: 'local_file_server', // Required: Unique ID for this config - transport: TransportType::Stdio, // Required: Transport type - timeout: 15.0, // Optional: Request timeout (seconds) - command: 'npx', // Required for Stdio: Executable - args: [ // Optional for Stdio: Arguments array - '-y', - '@modelcontextprotocol/server-filesystem', - '/path/to/project' - ], - workingDir: '/path/to/project', // Optional for Stdio: Working directory - env: ['DEBUG' => 'mcp*'] // Optional for Stdio: Environment variables - ); - - // Example: HTTP Server - $httpConfig = new ServerConfig( - name: 'remote_web_agent', // Required: Unique ID - transport: TransportType::Http, // Required: Transport type - timeout: 45.0, // Optional: Request timeout - url: 'http://localhost:8080/sse',// Required for Http: SSE URL - headers: [ // Optional for Http: Auth/Custom headers - 'Authorization' => 'Bearer xyz789' - ], - ); - ``` - - ### Loading Config from Array/JSON - - You can easily parse configurations stored in arrays (e.g., from JSON files or framework config). - - ```php - use PhpMcp\Client\ServerConfig; - use PhpMcp\Client\Exception\ConfigurationException; - - $jsonConfig = '{ - "mcpServers": { - "stdio_files": { - "command": "php", - "args": ["/app/mcp/file_server.php"], - "timeout": 10 - }, - "http_api": { - "url": "https://api.example.com/mcp/sse", - "transport": "http", - "headers": {"X-API-Key": "secret"} - } - } - }'; - - $decodedConfig = json_decode($jsonConfig, true)['mcpServers'] ?? []; - - $serverConfigs = []; - foreach ($decodedConfig as $name => $data) { - try { - $serverConfigs[$name] = ServerConfig::fromArray($name, $data); - } catch (ConfigurationException $e) { - echo "Error parsing config for '{$name}': {$e->getMessage()}\n"; - } +# PHP MCP Client + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) +[![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/client.svg?style=flat-square)](https://packagist.org/packages/php-mcp/client) +[![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/client/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/client/actions/workflows/tests.yml) +[![License](https://img.shields.io/packagist/l/php-mcp/client.svg?style=flat-square)](LICENSE) + +**PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).** + +It provides a developer-friendly interface to connect to individual MCP servers using different transports (`stdio`, `http+sse`), manage the connection lifecycle, discover server capabilities (Tools, Resources, Prompts), and execute requests like calling tools or reading resources. + +While utilizing asynchronous I/O internally via ReactPHP for robustness and handling features like server-sent events, the library offers **both** a straightforward **synchronous (blocking) API** for common use cases and an **asynchronous (Promise-based) API** for advanced control and concurrency. + +This library aligns with the MCP specification's model where one client instance manages a stateful connection to one server. + +## Introduction to MCP + +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open standard designed to standardize how AI assistants and applications connect to external data sources, APIs, and tools (like codebases, databases, web browsers). It acts as a communication layer, enabling AI models (like Claude, or models integrated via frameworks like OpenAI's) to securely access and interact with context provided by different servers. + +This client library allows your PHP application (acting as the "Host" in MCP terminology) to *consume* capabilities offered by one or more MCP servers. + +## Features + +* **Client-per-Server Model:** Each `Client` instance manages a stateful connection to a single configured MCP server, aligning with the MCP specification. +* **Fluent Configuration:** Easy setup for each client instance using a `Client::make()->with...()` builder pattern. +* **Dual API:** + * **Synchronous Facade:** Interact with the server using straightforward, blocking methods (e.g., `$client->listTools()`, `$client->callTool(...)`) for simple integration. + * **Asynchronous API:** Access underlying Promise-based methods (e.g., `$client->listToolsAsync()`, `$client->callToolAsync(...)`) for concurrency and integration with async PHP applications. +* **Multiple Transports:** Built-in support for: + * `stdio`: Communicating with server processes via Standard Input/Output. + * `http`: Communicating with servers via HTTP POST and Server-Sent Events (SSE). +* **Explicit Connection Lifecycle:** Requires `->initialize()` or `->initializeAsync()` to connect and perform the handshake before making requests. Provides `disconnect()` / `disconnectAsync()`. +* **Tool/Resource/Prompt Interaction:** Provides comprehensive methods (sync & async) to list available elements and execute requests like `tools/call`, `resources/read`, `prompts/get`. +* **PSR Compliance:** Integrates with standard PHP interfaces: + * `PSR-3` (LoggerInterface): Integrate your application's logger. + * `PSR-16` (SimpleCacheInterface): Optional caching for server definitions. + * `PSR-14` (EventDispatcherInterface): Optional handling of server-sent notifications via events (requires async handling). +* **Robust Error Handling:** Specific exceptions for different failure modes. +* **Asynchronous Core:** Utilizes ReactPHP internally for non-blocking I/O. + +## Requirements + +* PHP >= 8.1 +* Composer +* *(For Stdio Transport)*: Ability to execute the server command. +* *(For Http Transport)*: Network access to the MCP server URL. + +## Installation + +Install the package via Composer: + +```bash +composer require php-mcp/client +``` + +The necessary ReactPHP dependencies (`event-loop`, `promise`, `stream`, `child-process`, `http`) should be installed automatically. + +## Quick Start: Simple Synchronous Usage (Stdio) + +This example connects to a local filesystem server running via `npx`. + +```php +withClientInfo('MyFileSystemApp', '1.0') + ->withCapabilities($clientCapabilities) + // ->withLogger(new MyPsrLogger()) // Optional + ->withServerConfig($fsServerConfig) + ->build(); + +try { + // Initialize Connection (BLOCKING) + $fsClient->initialize(); + + // Interact (Synchronously) + $tools = $fsClient->listTools(); // Blocking call + foreach ($tools as $tool) { + echo "- Tool: {$tool->name}\n"; + } + + // ... Call other methods like $fsClient->callTool(...) ... + +} catch (McpClientException $e) { + echo "[MCP ERROR] " . get_class($e) . ": " . $e->getMessage() . "\n"; + // Check $e->getPrevious() for underlying transport/process errors +} catch (\Throwable $e) { + echo "[UNEXPECTED ERROR] " . $e->getMessage() . "\n"; +} finally { + // Disconnect (BLOCKING) + if (isset($fsClient)) { + $fsClient->disconnect(); + } +} +``` + +## Configuration + +Configuration involves setting up: + +1. **Client Identity:** Your application's name and version, passed directly to the builder. +2. **Client Capabilities:** Features your client supports using `ClientCapabilities`. +3. **Server Connection:** Details for the *single server* this client instance will connect to, using `ServerConfig`. +4. **(Optional) Dependencies:** Logger, Cache, Event Dispatcher, Event Loop. + +### `ClientCapabilities` + +Declares features your client supports. Use the static factory method. + +```php +use PhpMcp\Client\Model\Capabilities as ClientCapabilities; + +// Client supports sampling requests from the server +$clientCapabilities = ClientCapabilities::forClient(supportsSampling: true); + +// Client does NOT support sampling +$clientCapabilities = ClientCapabilities::forClient(supportsSampling: false); + +// TODO: Add support for declaring 'roots' capability if needed +``` + +### `ServerConfig` + +Defines how to connect to a *single* MCP server. + +```php +use PhpMcp\Client\Enum\TransportType; +use PhpMcp\Client\ServerConfig; + +// Example: Stdio Server +$stdioConfig = new ServerConfig( + name: 'local_file_server', // Required: Unique ID for this config + transport: TransportType::Stdio, // Required: Transport type + timeout: 15.0, // Optional: Request timeout (seconds) + command: 'npx', // Required for Stdio: Executable + args: [ // Optional for Stdio: Arguments array + '-y', + '@modelcontextprotocol/server-filesystem', + '/path/to/project' + ], + workingDir: '/path/to/project', // Optional for Stdio: Working directory + env: ['DEBUG' => 'mcp*'] // Optional for Stdio: Environment variables +); + +// Example: HTTP Server +$httpConfig = new ServerConfig( + name: 'remote_web_agent', // Required: Unique ID + transport: TransportType::Http, // Required: Transport type + timeout: 45.0, // Optional: Request timeout + url: 'http://localhost:8080/sse',// Required for Http: SSE URL + headers: [ // Optional for Http: Auth/Custom headers + 'Authorization' => 'Bearer xyz789' + ], +); +``` + +### Loading Config from Array/JSON + +You can easily parse configurations stored in arrays (e.g., from JSON files or framework config). + +```php +use PhpMcp\Client\ServerConfig; +use PhpMcp\Client\Exception\ConfigurationException; + +$jsonConfig = '{ + "mcpServers": { + "stdio_files": { + "command": "php", + "args": ["/app/mcp/file_server.php"], + "timeout": 10 + }, + "http_api": { + "url": "https://api.example.com/mcp/sse", + "transport": "http", + "headers": {"X-API-Key": "secret"} } - - // Now $serverConfigs['stdio_files'] and $serverConfigs['http_api'] - // contain ServerConfig objects. - ``` - - ### `ClientBuilder` - - Use the builder to assemble the `Client` instance: - - ```php - use PhpMcp\Client\Client; - // ... other use statements for Config, Logger etc... - - $client = Client::make() - ->withClientInfo($clientName, $clientVersion) // Required - ->withCapabilities($clientCapabilities) // Optional (defaults provided) - ->withServerConfig($stdioConfig) // Required: Config for THE server - ->withLogger($myLogger) // Optional - ->withCache($myCache, 3600) // Optional (cache + TTL) - ->withEventDispatcher($myDispatcher) // Optional - ->withIdGenerator($myIdGenerator) // Optional - ->withLoop($myEventLoop) // Optional (defaults to Loop::get()) - ->build(); - ``` - - ## Usage - - Once you have a configured `Client` instance for a specific server: - - **1. Initialize the Connection:** - - You *must* call `initialize()` or `initializeAsync()` before making requests. - - ```php - // Synchronous (Blocking) - try { - $client->initialize(); // Connects, performs handshake, waits until ready - echo "Connection Ready!"; - } catch (Throwable $e) { - echo "Initialization failed: " . $e->getMessage(); - // Handle error... client is likely in Error state + } +}'; + +$decodedConfig = json_decode($jsonConfig, true)['mcpServers'] ?? []; + +$serverConfigs = []; +foreach ($decodedConfig as $name => $data) { + try { + $serverConfigs[$name] = ServerConfig::fromArray($name, $data); + } catch (ConfigurationException $e) { + echo "Error parsing config for '{$name}': {$e->getMessage()}\n"; + } +} + +// Now $serverConfigs['stdio_files'] and $serverConfigs['http_api'] +// contain ServerConfig objects. +``` + +### `ClientBuilder` + +Use the builder to assemble the `Client` instance: + +```php +use PhpMcp\Client\Client; +// ... other use statements for Config, Logger etc... + +$client = Client::make() + ->withClientInfo($clientName, $clientVersion) // Required + ->withCapabilities($clientCapabilities) // Optional (defaults provided) + ->withServerConfig($stdioConfig) // Required: Config for THE server + ->withLogger($myLogger) // Optional + ->withCache($myCache, 3600) // Optional (cache + TTL) + ->withEventDispatcher($myDispatcher) // Optional + ->withIdGenerator($myIdGenerator) // Optional + ->withLoop($myEventLoop) // Optional (defaults to Loop::get()) + ->build(); +``` + +## Usage + +Once you have a configured `Client` instance for a specific server: + +**1. Initialize the Connection:** + +You *must* call `initialize()` or `initializeAsync()` before making requests. + +```php +// Synchronous (Blocking) +try { + $client->initialize(); // Connects, performs handshake, waits until ready + echo "Connection Ready!"; +} catch (Throwable $e) { + echo "Initialization failed: " . $e->getMessage(); + // Handle error... client is likely in Error state +} + +// Asynchronous (Promise-based) +$client->initializeAsync()->then( + function(Client $readyClient) { /* Ready */ }, + function(Throwable $error) { /* Handle init failure */ } +); +// Requires running the event loop ($client->getLoop()->run()) +``` + +**2. Making Requests:** + +Use the client methods. They operate on the single connection established by `initialize()`. + +* **Synchronous API (Recommended for simple scripts/frameworks):** + * Methods like `listTools()`, `callTool()`, `readResource()` block execution until a response is received or a timeout occurs. + * They return the parsed result object (e.g., `array`, `CallToolResult`) or throw an exception (`TimeoutException`, `RequestException`, `ConnectionException`, etc.). + + ```php + try { + if ($client->isReady()) { // Check status + $tools = $client->listTools(); + $result = $client->callTool('myTool', ['param' => 'value']); } - - // Asynchronous (Promise-based) - $client->initializeAsync()->then( - function(Client $readyClient) { /* Ready */ }, - function(Throwable $error) { /* Handle init failure */ } - ); - // Requires running the event loop ($client->getLoop()->run()) - ``` - - **2. Making Requests:** - - Use the client methods. They operate on the single connection established by `initialize()`. - - * **Synchronous API (Recommended for simple scripts/frameworks):** - * Methods like `listTools()`, `callTool()`, `readResource()` block execution until a response is received or a timeout occurs. - * They return the parsed result object (e.g., `array`, `CallToolResult`) or throw an exception (`TimeoutException`, `RequestException`, `ConnectionException`, etc.). - - ```php - try { - if ($client->isReady()) { // Check status - $tools = $client->listTools(); - $result = $client->callTool('myTool', ['param' => 'value']); - } - } catch (Throwable $e) { /* Handle errors */ } - ``` - - * **Asynchronous API (For async applications or concurrent requests):** - * Methods like `listToolsAsync()`, `callToolAsync()`, `readResourceAsync()` return a `React\Promise\PromiseInterface`. - * You need to use promise methods (`then`, `catch`, `finally`) or `React\Async\await` (in a Fiber context) to handle the results. - * Requires the event loop to be running. - - ```php - use function React\Promise\all; - - if ($client->isReady()) { - $p1 = $client->listToolsAsync(); - $p2 = $client->readResourceAsync('config://settings'); - - all([$p1, $p2])->then( - function(array $results) { - [$tools, $readResult] = $results; - // Process async results... - }, - function(Throwable $error) { - // Handle async error... - } - ); - // $client->getLoop()->run(); // Need to run the loop + } catch (Throwable $e) { /* Handle errors */ } + ``` + +* **Asynchronous API (For async applications or concurrent requests):** + * Methods like `listToolsAsync()`, `callToolAsync()`, `readResourceAsync()` return a `React\Promise\PromiseInterface`. + * You need to use promise methods (`then`, `catch`, `finally`) or `React\Async\await` (in a Fiber context) to handle the results. + * Requires the event loop to be running. + + ```php + use function React\Promise\all; + + if ($client->isReady()) { + $p1 = $client->listToolsAsync(); + $p2 = $client->readResourceAsync('config://settings'); + + all([$p1, $p2])->then( + function(array $results) { + [$tools, $readResult] = $results; + // Process async results... + }, + function(Throwable $error) { + // Handle async error... } - ``` - - **3. Disconnecting:** - - Always disconnect when you are finished interacting with a server to release resources (especially for `stdio` transports). - - ```php - // Synchronous - $client->disconnect(); // Blocks until closed or timeout - - // Asynchronous - $client->disconnectAsync()->then(function() { echo "Disconnected async"; }); - // $loop->run(); - ``` - - ## Available Client Methods - - The `Client` class provides methods for interacting with the connected MCP server. Most methods have both a synchronous (blocking) and an asynchronous (Promise-returning) variant. - - **Connection & Lifecycle:** - - * **(Sync)** `initialize(): self` - Connects to the server and performs the MCP handshake. Blocks until ready or throws an exception. Returns the client instance. - * **(Async)** `initializeAsync(): PromiseInterface` - Initiates connection and handshake asynchronously. Returns a promise resolving with the client instance when ready, or rejecting on failure. - * **(Sync)** `disconnect(): void` - Closes the connection gracefully. Blocks until disconnection is complete or times out. - * **(Async)** `disconnectAsync(): PromiseInterface` - Initiates graceful disconnection asynchronously. Returns a promise resolving when disconnection is complete. - * `getStatus(): ConnectionStatus` - Returns the current connection status enum (`Disconnected`, `Connecting`, `Handshaking`, `Ready`, `Closing`, `Closed`, `Error`). - * `isReady(): bool` - Helper method, returns `true` if status is `Ready`. - * `getServerName(): ?string` - Returns the name of the server (available after successful initialization). - * `getServerVersion(): ?string` - Returns the version of the server (available after successful initialization). - * `getNegotiatedCapabilities(): ?Capabilities` - Returns the capabilities negotiated with the server (available after successful initialization). - * `getNegotiatedProtocolVersion(): ?string` - Returns the protocol version agreed upon with the server (available after successful initialization). - - **MCP Operations (Sync):** - - *(These methods require the client to be initialized first and will block)* - - * `ping(): void` - * `listTools(bool $useCache = true): array` - * `listResources(bool $useCache = true): array` - * `listPrompts(bool $useCache = true): array` - * `listResourceTemplates(bool $useCache = true): array` - * `callTool(string $toolName, array $arguments = []): CallToolResult` - * `readResource(string $uri): ReadResourceResult` - * `getPrompt(string $promptName, array $arguments = []): GetPromptResult` - * `subscribeResource(string $uri): void` - * `unsubscribeResource(string $uri): void` - * `setLogLevel(string $level): void` - - **MCP Operations (Async):** - - *(These methods require the client to be initialized first and return `React\Promise\PromiseInterface`)* - - * `pingAsync(): PromiseInterface` - * `listToolsAsync(): PromiseInterface>` - * `listResourcesAsync(): PromiseInterface>` - * `listPromptsAsync(): PromiseInterface>` - * `listResourceTemplatesAsync(): PromiseInterface>` - * `callToolAsync(string $toolName, array $arguments = []): PromiseInterface` - * `readResourceAsync(string $uri): PromiseInterface` - * `getPromptAsync(string $promptName, array $arguments = []): PromiseInterface` - * `subscribeResourceAsync(string $uri): PromiseInterface` - * `unsubscribeResourceAsync(string $uri): PromiseInterface` - * `setLogLevelAsync(string $level): PromiseInterface` - - **Advanced:** - - * `getLoop(): LoopInterface` - Access the underlying ReactPHP event loop instance. - - ## Handling Server Notifications (Asynchronous Only) - - MCP servers can send notifications (e.g., `resources/didChange`). To receive these: - - 1. Configure the client with a PSR-14 `EventDispatcherInterface` using `->withEventDispatcher(...)`. - 2. Add listeners to your dispatcher for events like `PhpMcp\Client\Event\ResourceChanged`. - 3. Use the **asynchronous API** (`initializeAsync`, potentially other `*Async` methods). - 4. **Run the event loop continuously** (`$client->getLoop()->run()`). Notifications arrive via the underlying transport (usually SSE) only while the loop is active. - - See `examples/04-handling-notifications.php` for a conceptual guide. - - ## Error Handling - - The client uses specific exceptions inheriting from `PhpMcp\Client\Exception\McpClientException`. Catching these allows for targeted error handling: - - * **`ConfigurationException`**: Thrown during `ClientBuilder::build()` or `ServerConfig::fromArray()` if the provided configuration is invalid or missing required fields (e.g., missing `command` for stdio, invalid `url` for http). - * **`ConnectionException`**: Thrown by `initialize()` or `initializeAsync()` if the underlying transport connection fails (e.g., stdio process cannot start, TCP connection refused for HTTP, invalid initial response). Also thrown by request methods if called when the client is not in a `Ready` state or if the connection drops unexpectedly during an operation. Check `$e->getPrevious()` for lower-level transport or system errors. - * **`HandshakeException` (Subclass of `ConnectionException`)**: Thrown specifically by `initialize()` or `initializeAsync()` if the MCP handshake phase fails after the transport connection is established (e.g., server returns an error to the `initialize` request, version mismatch, invalid capabilities received). May contain the server's `JsonRpc\Error` via `getRequestException()->getRpcError()`. - * **`TransportException`**: Indicates a low-level error during communication *after* connection (e.g., failure to write to stdio stdin, SSE stream error, unexpected data format received from transport). Often wrapped by `ConnectionException`. - * **`TimeoutException`**: Thrown by synchronous methods (`initialize`, `listTools`, `callTool`, etc.) or rejects asynchronous promises if the server does not respond within the configured `timeout` for the `ServerConfig`. Access timeout value via `$e->getTimeout()`. - * **`RequestException`**: Thrown by synchronous methods or rejects asynchronous promises when the MCP server successfully processed the request but returned a JSON-RPC error payload (e.g., method not found on server, invalid parameters for a tool, tool execution failed on server). Access the `JsonRpc\Error` object via `$e->getRpcError()` to get the code, message, and optional data from the server. - * **`UnsupportedCapabilityException`**: Thrown by methods like `subscribeResource()` or `setLogLevel()` if the connected server did not declare support for the required capability during the initial handshake. - * **`DefinitionException`**: Thrown if there's an error fetching, caching, or parsing server definitions (Tools, Resources, Prompts), often related to cache issues or invalid data structures. - * **`ProtocolException`**: Indicates a violation of the JSON-RPC 2.0 or MCP structure in messages received from the server (e.g., missing required fields, invalid types). - - Always wrap client interactions in `try...catch` blocks to handle these potential failures gracefully. - - ## Examples - - See the [`examples/`](./examples/) directory for working code: - - * `01-simple-stdio-sync.php`: Demonstrates basic synchronous interaction with a `stdio` server. - * `02-simple-http-sync.php`: Demonstrates basic synchronous interaction with an `http+sse` server. - * `03-multiple-servers-sync.php`: Shows how to instantiate and use multiple `Client` objects for different servers within the same script (sequentially). - * `04-multiple-servers-async.php`: Demonstrates asynchronous interaction with multiple servers using Promises for concurrency. Requires running the event loop. - * `05-openai-php-integration-sync`: Full example integrating with `openai-php` for tool usage using the synchronous client API, including its own `composer.json` setup. - - ## Testing - - ```bash - composer install --dev - - composer test - ``` - - ## Contributing - - Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) or open an issue/pull request. - - ## License - - The MIT License (MIT). See [LICENSE](LICENSE). \ No newline at end of file + ); + // $client->getLoop()->run(); // Need to run the loop + } + ``` + +**3. Disconnecting:** + +Always disconnect when you are finished interacting with a server to release resources (especially for `stdio` transports). + +```php +// Synchronous +$client->disconnect(); // Blocks until closed or timeout + +// Asynchronous +$client->disconnectAsync()->then(function() { echo "Disconnected async"; }); +// $loop->run(); +``` + +## Available Client Methods + +The `Client` class provides methods for interacting with the connected MCP server. Most methods have both a synchronous (blocking) and an asynchronous (Promise-returning) variant. + +**Connection & Lifecycle:** + +* **(Sync)** `initialize(): self` + Connects to the server and performs the MCP handshake. Blocks until ready or throws an exception. Returns the client instance. +* **(Async)** `initializeAsync(): PromiseInterface` + Initiates connection and handshake asynchronously. Returns a promise resolving with the client instance when ready, or rejecting on failure. +* **(Sync)** `disconnect(): void` + Closes the connection gracefully. Blocks until disconnection is complete or times out. +* **(Async)** `disconnectAsync(): PromiseInterface` + Initiates graceful disconnection asynchronously. Returns a promise resolving when disconnection is complete. +* `getStatus(): ConnectionStatus` + Returns the current connection status enum (`Disconnected`, `Connecting`, `Handshaking`, `Ready`, `Closing`, `Closed`, `Error`). +* `isReady(): bool` + Helper method, returns `true` if status is `Ready`. +* `getServerName(): ?string` + Returns the name of the server (available after successful initialization). +* `getServerVersion(): ?string` + Returns the version of the server (available after successful initialization). +* `getNegotiatedCapabilities(): ?Capabilities` + Returns the capabilities negotiated with the server (available after successful initialization). +* `getNegotiatedProtocolVersion(): ?string` + Returns the protocol version agreed upon with the server (available after successful initialization). + +**MCP Operations (Sync):** + +*(These methods require the client to be initialized first and will block)* + +* `ping(): void` +* `listTools(bool $useCache = true): array` +* `listResources(bool $useCache = true): array` +* `listPrompts(bool $useCache = true): array` +* `listResourceTemplates(bool $useCache = true): array` +* `callTool(string $toolName, array $arguments = []): CallToolResult` +* `readResource(string $uri): ReadResourceResult` +* `getPrompt(string $promptName, array $arguments = []): GetPromptResult` +* `subscribeResource(string $uri): void` +* `unsubscribeResource(string $uri): void` +* `setLogLevel(string $level): void` + +**MCP Operations (Async):** + +*(These methods require the client to be initialized first and return `React\Promise\PromiseInterface`)* + +* `pingAsync(): PromiseInterface` +* `listToolsAsync(): PromiseInterface>` +* `listResourcesAsync(): PromiseInterface>` +* `listPromptsAsync(): PromiseInterface>` +* `listResourceTemplatesAsync(): PromiseInterface>` +* `callToolAsync(string $toolName, array $arguments = []): PromiseInterface` +* `readResourceAsync(string $uri): PromiseInterface` +* `getPromptAsync(string $promptName, array $arguments = []): PromiseInterface` +* `subscribeResourceAsync(string $uri): PromiseInterface` +* `unsubscribeResourceAsync(string $uri): PromiseInterface` +* `setLogLevelAsync(string $level): PromiseInterface` + +**Advanced:** + +* `getLoop(): LoopInterface` + Access the underlying ReactPHP event loop instance. + +## Handling Server Notifications (Asynchronous Only) + +MCP servers can send notifications (e.g., `resources/didChange`). To receive these: + +1. Configure the client with a PSR-14 `EventDispatcherInterface` using `->withEventDispatcher(...)`. +2. Add listeners to your dispatcher for events like `PhpMcp\Client\Event\ResourceChanged`. +3. Use the **asynchronous API** (`initializeAsync`, potentially other `*Async` methods). +4. **Run the event loop continuously** (`$client->getLoop()->run()`). Notifications arrive via the underlying transport (usually SSE) only while the loop is active. + +See `examples/04-handling-notifications.php` for a conceptual guide. + +## Error Handling + +The client uses specific exceptions inheriting from `PhpMcp\Client\Exception\McpClientException`. Catching these allows for targeted error handling: + +* **`ConfigurationException`**: Thrown during `ClientBuilder::build()` or `ServerConfig::fromArray()` if the provided configuration is invalid or missing required fields (e.g., missing `command` for stdio, invalid `url` for http). +* **`ConnectionException`**: Thrown by `initialize()` or `initializeAsync()` if the underlying transport connection fails (e.g., stdio process cannot start, TCP connection refused for HTTP, invalid initial response). Also thrown by request methods if called when the client is not in a `Ready` state or if the connection drops unexpectedly during an operation. Check `$e->getPrevious()` for lower-level transport or system errors. +* **`HandshakeException` (Subclass of `ConnectionException`)**: Thrown specifically by `initialize()` or `initializeAsync()` if the MCP handshake phase fails after the transport connection is established (e.g., server returns an error to the `initialize` request, version mismatch, invalid capabilities received). May contain the server's `JsonRpc\Error` via `getRequestException()->getRpcError()`. +* **`TransportException`**: Indicates a low-level error during communication *after* connection (e.g., failure to write to stdio stdin, SSE stream error, unexpected data format received from transport). Often wrapped by `ConnectionException`. +* **`TimeoutException`**: Thrown by synchronous methods (`initialize`, `listTools`, `callTool`, etc.) or rejects asynchronous promises if the server does not respond within the configured `timeout` for the `ServerConfig`. Access timeout value via `$e->getTimeout()`. +* **`RequestException`**: Thrown by synchronous methods or rejects asynchronous promises when the MCP server successfully processed the request but returned a JSON-RPC error payload (e.g., method not found on server, invalid parameters for a tool, tool execution failed on server). Access the `JsonRpc\Error` object via `$e->getRpcError()` to get the code, message, and optional data from the server. +* **`UnsupportedCapabilityException`**: Thrown by methods like `subscribeResource()` or `setLogLevel()` if the connected server did not declare support for the required capability during the initial handshake. +* **`DefinitionException`**: Thrown if there's an error fetching, caching, or parsing server definitions (Tools, Resources, Prompts), often related to cache issues or invalid data structures. +* **`ProtocolException`**: Indicates a violation of the JSON-RPC 2.0 or MCP structure in messages received from the server (e.g., missing required fields, invalid types). + +Always wrap client interactions in `try...catch` blocks to handle these potential failures gracefully. + +## Examples + +See the [`examples/`](./examples/) directory for working code: + +* `01-simple-stdio-sync.php`: Demonstrates basic synchronous interaction with a `stdio` server. +* `02-simple-http-sync.php`: Demonstrates basic synchronous interaction with an `http+sse` server. +* `03-multiple-servers-sync.php`: Shows how to instantiate and use multiple `Client` objects for different servers within the same script (sequentially). +* `04-multiple-servers-async.php`: Demonstrates asynchronous interaction with multiple servers using Promises for concurrency. Requires running the event loop. +* `05-openai-php-integration-sync`: Full example integrating with `openai-php` for tool usage using the synchronous client API, including its own `composer.json` setup. + +## Testing + +```bash +composer install --dev + +composer test +``` + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) or open an issue/pull request. + +## License + +The MIT License (MIT). See [LICENSE](LICENSE). \ No newline at end of file