diff --git a/README.md b/README.md index 3232afd2..80534a56 100644 --- a/README.md +++ b/README.md @@ -10,202 +10,143 @@ development practices and standards from the Symfony project, including [Coding Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html), please see the [roadmap](./ROADMAP.md) for planned next steps and features. +## Table of Contents + +- [Installation](#installation) +- [Overview](#overview) +- [Server SDK](#server-sdk) +- [Client SDK](#client-sdk) +- [Documentation](#documentation) +- [External Resources](#external-resources) +- [PHP Libraries Using the MCP SDK](#php-libraries-using-the-mcp-sdk) +- [Contributing](#contributing) +- [Credits](#credits) +- [License](#license) + ## Installation ```bash composer require mcp/sdk ``` -## Quick Start +## Overview -This example demonstrates the most common usage pattern - a STDIO server using attribute discovery. +The MCP PHP SDK provides both **server** and **client** implementations for the Model Context Protocol, enabling you to: -### 1. Define Your MCP Elements +- **Build MCP Servers**: Expose your PHP application's functionality (tools, resources, prompts) to AI agents +- **Create MCP Clients**: Connect to and interact with MCP servers from your PHP applications -Create a class with MCP capabilities using attributes: +## Server SDK -```php - $a + $b, - 'subtract' => $a - $b, - 'multiply' => $a * $b, - 'divide' => $b != 0 ? $a / $b : 'Error: Division by zero', - default => 'Error: Unknown operation' - }; - } - - #[McpResource( - uri: 'config://calculator/settings', - name: 'calculator_config', - mimeType: 'application/json' - )] + #[McpResource(uri: 'config://calculator/settings')] public function getSettings(): array { - return ['precision' => 2, 'allow_negative' => true]; + return ['precision' => 2]; } } -``` - -### 2. Create the Server Script - -Create your MCP server: - -```php -#!/usr/bin/env php -setServerInfo('Calculator Server', '1.0.0') - ->setDiscovery(__DIR__, ['.']) + ->setDiscovery(__DIR__, ['.']) // Auto-discover attributes ->build(); $transport = new StdioTransport(); - $server->run($transport); ``` -### 3. Configure Your MCP Client - -Add to your client configuration (e.g., Claude Desktop's `mcp.json`): - -```json -{ - "mcpServers": { - "php-calculator": { - "command": "php", - "args": ["/absolute/path/to/your/server.php"] - } - } -} -``` - -### 4. Test Your Server +### Server Capabilities -```bash -# Test with MCP Inspector -npx @modelcontextprotocol/inspector php /path/to/server.php +- **Tools**: Executable functions that AI agents can call +- **Resources**: Data sources that can be read (files, configs, databases) +- **Resource Templates**: Dynamic resources with URI parameters +- **Prompts**: Pre-defined templates for AI interactions +- **Server-Initiated Communication**: Sampling, logging, progress notifications -# Your AI assistant can now call: -# - add: Add two integers -# - calculate: Perform arithmetic operations -# - Read config://calculator/settings resource -``` - -## Key Features - -### Attribute-Based Discovery +### Registration Methods -Define MCP elements using PHP attributes with automatic discovery: +There are multiple ways to register your MCP capabilities—choose the approach that best fits your application's architecture: +**1. Attribute-Based Discovery** — Define capabilities using PHP attributes for automatic discovery: ```php -// Tool with automatic name and description from method #[McpTool] public function generateReport(): string { /* ... */ } -// Tool with custom name -#[McpTool(name: 'custom_name')] -public function myMethod(): string { /* ... */ } - -// Resource with URI and metadata -#[McpResource(uri: 'config://app/settings', mimeType: 'application/json')] +#[McpResource(uri: 'config://app/settings')] public function getConfig(): array { /* ... */ } ``` -### Manual Registration - -Register capabilities programmatically: +**2. Manual Registration** — Register capabilities programmatically without attributes: +```php +$server = Server::builder() + ->addTool([Calculator::class, 'add'], 'add_numbers') + ->addResource([Config::class, 'get'], 'config://app') + ->build(); +``` +**3. Hybrid Approach** — Combine both methods for maximum flexibility: ```php $server = Server::builder() - ->addTool([MyClass::class, 'myMethod'], 'tool_name') - ->addResource([MyClass::class, 'getData'], 'data://config') + ->setDiscovery(__DIR__, ['.']) + ->addTool([ExternalService::class, 'process'], 'external') ->build(); ``` -### Multiple Transport Options +### Transports + +Choose the transport that matches your deployment environment: -**STDIO Transport** (Command-line integration): +**1. STDIO Transport** — For command-line integration and local processes: ```php $transport = new StdioTransport(); $server->run($transport); ``` -**HTTP Transport** (Web-based communication): +**2. HTTP Transport** — For web-based servers and distributed systems: ```php $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); $response = $server->run($transport); -// Handle $response in your web application ``` ### Session Management -By default, the SDK uses in-memory sessions. You can configure different session stores: +Configure session storage to maintain state between requests. Choose the backend that fits your infrastructure: +**In-Memory** (default, suitable for STDIO): ```php -use Mcp\Server\Session\FileSessionStore; -use Mcp\Server\Session\InMemorySessionStore; -use Mcp\Server\Session\Psr16SessionStore; -use Symfony\Component\Cache\Psr16Cache; -use Symfony\Component\Cache\Adapter\RedisAdapter; - -// Use default in-memory sessions with custom TTL $server = Server::builder() ->setSession(ttl: 7200) // 2 hours ->build(); +``` -// Override with file-based storage +**File-Based** (suitable for single-server HTTP deployments): +```php $server = Server::builder() ->setSession(new FileSessionStore(__DIR__ . '/sessions')) ->build(); +``` -// Override with in-memory storage and custom TTL -$server = Server::builder() - ->setSession(new InMemorySessionStore(3600)) - ->build(); - -// Override with PSR-16 cache-based storage -// Requires psr/simple-cache and symfony/cache (or any other PSR-16 implementation) -// composer require psr/simple-cache symfony/cache -$redisAdapter = new RedisAdapter( - RedisAdapter::createConnection('redis://localhost:6379'), - 'mcp_sessions' -); - +**Redis** (suitable for distributed/multi-server deployments): +```php $server = Server::builder() ->setSession(new Psr16SessionStore( cache: new Psr16Cache($redisAdapter), @@ -215,60 +156,141 @@ $server = Server::builder() ->build(); ``` -### Discovery Caching +[→ Server Documentation](docs/server-builder.md) + +## Client SDK -Use any PSR-16 cache implementation to cache discovery results and avoid running discovery on every server start: +Connect to MCP servers from your PHP applications to access their tools, resources, and prompts. + +### Quick Example ```php -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Cache\Psr16Cache; +use Mcp\Client; +use Mcp\Client\Transport\StdioTransport; + +// Build the client +$client = Client::builder() + ->setClientInfo('My Application', '1.0.0') + ->setInitTimeout(30) + ->setRequestTimeout(120) + ->build(); -$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery')); +// Connect to a server +$transport = new StdioTransport( + command: 'php', + args: ['/path/to/server.php'], +); -$server = Server::builder() - ->setDiscovery( - basePath: __DIR__, - scanDirs: ['.', 'src'], // Default: ['.', 'src'] - excludeDirs: ['vendor'], // Default: ['vendor', 'node_modules'] - cache: $cache - ) +$client->connect($transport); + +// Discover and use capabilities +$tools = $client->listTools(); +$result = $client->callTool('add', ['a' => 5, 'b' => 3]); + +$resources = $client->listResources(); +$content = $client->readResource('config://calculator/settings'); + +$client->disconnect(); +``` + +### Client Capabilities + +- **Tool Calling**: List and execute tools from any MCP server +- **Resource Access**: Read static and dynamic resources +- **Prompt Management**: List and retrieve prompt templates +- **Completion Support**: Request argument completion suggestions + +### Advanced Features + +- **Progress Tracking**: Real-time progress during long operations +```php +$result = $client->callTool( + name: 'process_data', + arguments: ['dataset' => 'large_file.csv'], + onProgress: function (float $progress, ?float $total, ?string $message) { + echo "Progress: {$progress}/{$total} - {$message}\n"; + } +); +``` + +- **Sampling Support**: Handle server LLM sampling requests +```php +$samplingHandler = new SamplingRequestHandler($myCallback); +$client = Client::builder() + ->setCapabilities(new ClientCapabilities(sampling: true)) + ->addRequestHandler($samplingHandler) + ->build(); +``` + +- **Logging Notifications**: Receive server log messages +```php +$loggingHandler = new LoggingNotificationHandler($myCallback); +$client = Client::builder() + ->addNotificationHandler($loggingHandler) ->build(); ``` +### Transports + +Connect to MCP servers using the transport that matches your setup: + +**1. STDIO Transport** — Connect to local server processes: +```php +$transport = new StdioTransport( + command: 'php', + args: ['/path/to/server.php'], +); + +$client->connect($transport); +``` + +**2. HTTP Transport** — Connect to remote or web-based servers: +```php +$transport = new HttpTransport('http://localhost:8000'); + +$client->connect($transport); +``` + +[→ Client Documentation](docs/client.md) + ## Documentation -**Core Concepts:** -- [Server Builder](docs/server-builder.md) - Complete ServerBuilder reference and configuration -- [Transports](docs/transports.md) - STDIO and HTTP transport setup and usage -- [MCP Elements](docs/mcp-elements.md) - Creating tools, resources, and prompts -- [Client Communication](docs/client-communication.md) - Communicating back to the client from server-side -- [Events](docs/events.md) - Hooking into server lifecycle with events +### Core Concepts + +- **[Server Builder](docs/server-builder.md)** — Complete ServerBuilder reference and configuration +- **[Client](docs/client.md)** — Client SDK for connecting to and communicating with MCP servers +- **[Transports](docs/transports.md)** — STDIO and HTTP transport setup and usage +- **[MCP Elements](docs/mcp-elements.md)** — Creating tools, resources, prompts, and templates +- **[Server-Client Communication](docs/server-client-communication.md)** — Sampling, logging, progress, and notifications +- **[Events](docs/events.md)** — Hooking into server lifecycle with events + +### Learning & Examples -**Learning:** -- [Examples](docs/examples.md) - Comprehensive example walkthroughs +- **[Examples](docs/examples.md)** — Comprehensive example walkthroughs for servers and clients +- **[ROADMAP.md](ROADMAP.md)** — Planned features and development roadmap -**External Resources:** -- [Model Context Protocol documentation](https://modelcontextprotocol.io) -- [Model Context Protocol specification](https://spec.modelcontextprotocol.io) -- [Officially supported servers](https://github.com/modelcontextprotocol/servers) +## External Resources + +- **[Model Context Protocol Documentation](https://modelcontextprotocol.io)** — Official MCP documentation +- **[Model Context Protocol Specification](https://spec.modelcontextprotocol.io)** — Protocol specification +- **[Officially Supported Servers](https://github.com/modelcontextprotocol/servers)** — Reference server implementations ## PHP Libraries Using the MCP SDK -* [pronskiy/mcp](https://github.com/pronskiy/mcp) - Additional DX layer -* [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) - Symfony integration bundle -* [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) - CakePHP integration plugin +- [pronskiy/mcp](https://github.com/pronskiy/mcp) — Additional developer experience layer +- [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) — Symfony integration bundle +- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP integration plugin ## Contributing -We are passionate about supporting contributors of all levels of experience and would love to see you get involved in -the project. See the [contributing guide](CONTRIBUTING.md) to get started before you [report issues](https://github.com/modelcontextprotocol/php-sdk/issues) and [send pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls). +We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. + +See the [Contributing Guide](CONTRIBUTING.md) to get started before you [report issues](https://github.com/modelcontextprotocol/php-sdk/issues) and [send pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls). ## Credits -The starting point for this SDK was the [PHP-MCP](https://github.com/php-mcp/server) project, initiated by -[Kyrian Obikwelu](https://github.com/CodeWithKyrian), and the [Symfony AI initiative](https://github.com/symfony/ai). We are grateful for the work -done by both projects and their contributors, which created a solid foundation for this SDK. +The starting point for this SDK was the [PHP-MCP](https://github.com/php-mcp/server) project, initiated by [Kyrian Obikwelu](https://github.com/CodeWithKyrian), and the [Symfony AI initiative](https://github.com/symfony/ai). We are grateful for the work done by both projects and their contributors, which created a solid foundation for this SDK. ## License -This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under the MIT License — see the [LICENSE](LICENSE) file for details. diff --git a/ROADMAP.md b/ROADMAP.md index cab6bf0f..03e47515 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,8 +8,8 @@ This roadmap is a living document that outlines the planned features and improve - [ ] Implement full support for elicitations - [ ] Implement OAuth2 authentication for server - **Client** -- [ ] Implement client-side support -- [ ] Implement client examples and documentation +- [x] Implement client-side support +- [x] Implement client examples and documentation - **Schema** - [ ] Implement schema generation based on TS or JSON Schema diff --git a/docs/client.md b/docs/client.md new file mode 100644 index 00000000..304fad77 --- /dev/null +++ b/docs/client.md @@ -0,0 +1,691 @@ +# Client + +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](#overview) +- [Client Builder](#client-builder) +- [Transports](#transports) +- [Connecting to Servers](#connecting-to-servers) +- [Server Information](#server-information) +- [Working with Tools](#working-with-tools) +- [Working with Resources](#working-with-resources) +- [Working with Prompts](#working-with-prompts) +- [Server-Initiated Communication](#server-initiated-communication) +- [Error Handling](#error-handling) +- [Complete Example](#complete-example) + +## Overview + +The client follows a builder pattern for configuration and provides a synchronous API for all operations: + +```php +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 + +```php +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: + +```php +$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): + +```php +use Mcp\Schema\Enum\ProtocolVersion; + +$client = Client::builder() + ->setProtocolVersion(ProtocolVersion::V2025_06_18) + ->build(); +``` + +### Capabilities + +Declare client capabilities to enable server features: + +```php +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: + +```php +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): + +```php +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: + +```php +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: + +```php +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 execute +- `args` (array): Command arguments +- `cwd` (string|null): Working directory for the process +- `env` (array|null): Environment variables +- `logger` (LoggerInterface|null): Optional PSR-3 logger + +### HTTP Transport + +Communicates with remote MCP servers over HTTP: + +```php +use Mcp\Client\Transport\HttpTransport; + +$transport = new HttpTransport( + endpoint: 'http://localhost:8000', + headers: ['Authorization' => 'Bearer token'], +); +``` + +**Parameters:** +- `endpoint` (string): The MCP server URL +- `headers` (array): Additional HTTP headers +- `httpClient` (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-adapter` +- `php-http/curl-client` +- `symfony/http-client` +- And other PSR-18 compatible implementations + +```bash +# Install any PSR-18 client - discovery works automatically +composer require php-http/guzzle7-adapter +``` + + +## Connecting to Servers + +### Establishing Connection + +```php +$client->connect($transport); +``` + +The `connect()` method performs the MCP initialization handshake: +1. Opens the transport connection +2. Sends InitializeRequest with client capabilities +3. Waits for InitializeResult from server +4. Sends InitializedNotification + +> [!IMPORTANT] +> Always wrap connection in try/catch to handle `ConnectionException` for failed connections. + +### Checking Connection State + +```php +if ($client->isConnected()) { + // Client is connected and initialized +} +``` + +### Disconnecting + +```php +$client->disconnect(); +``` + +Always disconnect when finished to clean up resources: + +```php +try { + $client->connect($transport); + // ... use the client ... +} finally { + $client->disconnect(); +} +``` + +## Server Information + +After successful connection, retrieve server metadata: + +```php +// 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 + +```php +$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 + +```php +$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): + +```php +$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 + +```php +$resourcesResult = $client->listResources(); + +foreach ($resourcesResult->resources as $resource) { + echo "- {$resource->uri}: {$resource->name}\n"; +} +``` + +### Listing Resource Templates + +```php +$templatesResult = $client->listResourceTemplates(); + +foreach ($templatesResult->resourceTemplates as $template) { + echo "- {$template->uriTemplate}: {$template->name}\n"; +} +``` + +### Reading Resources + +```php +$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: + +```php +$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 + +```php +$promptsResult = $client->listPrompts(); + +foreach ($promptsResult->prompts as $prompt) { + echo "- {$prompt->name}: {$prompt->description}\n"; +} +``` + +### Getting Prompts + +```php +$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: + +```php +$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: + +```php +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: + +```php +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: + +```php +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 `SamplingException`** to 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: +> +> ```php +> // 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: + +```php +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: + +```php +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 +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"; +} +``` diff --git a/docs/examples.md b/docs/examples.md index e356f489..4475f131 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -7,7 +7,8 @@ specific features and can be run independently to understand how the SDK works. - [Getting Started](#getting-started) - [Running Examples](#running-examples) -- [Examples](#examples) +- [Server Examples](#server-examples) +- [Client Examples](#client-examples) ## Getting Started @@ -58,7 +59,7 @@ curl -X POST http://localhost:8000 \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0.0"},"capabilities":{}}}' ``` -## Examples +## Server Examples ### Discovery Calculator @@ -353,3 +354,149 @@ npx @modelcontextprotocol/inspector php examples/server/elicitation/server.php 1. **book_restaurant** - Multi-field reservation form with number, date, and enum fields 2. **confirm_action** - Simple boolean confirmation dialog 3. **collect_feedback** - Rating and comments form with optional fields + +## Client Examples + +### STDIO Discovery Calculator (Client) + +**File**: `examples/client/stdio_discovery_calculator.php` + +**What it demonstrates:** +- Basic MCP client usage with STDIO transport +- Connecting to a local MCP server process +- Listing and calling tools +- Reading resources + +**Key Features:** +```php +$client = Client::builder() + ->setClientInfo('STDIO Example Client', '1.0.0') + ->setInitTimeout(30) + ->setRequestTimeout(60) + ->build(); + +$transport = new StdioTransport( + command: 'php', + args: [__DIR__.'/../server/discovery-calculator/server.php'], +); + +$client->connect($transport); +$tools = $client->listTools(); +$result = $client->callTool('calculate', ['a' => 5, 'b' => 3, 'operation' => 'add']); +$resourceContent = $client->readResource('config://calculator/settings'); +``` + +**Usage:** +```bash +# Run the client (automatically starts the server) +php examples/client/stdio_discovery_calculator.php +``` + +### HTTP Discovery Calculator (Client) + +**File**: `examples/client/http_discovery_calculator.php` + +**What it demonstrates:** +- MCP client with HTTP transport +- Connecting to remote MCP servers +- Listing tools, resources, and prompts + +**Key Features:** +```php +$transport = new HttpTransport('http://localhost:8000'); +$client->connect($transport); + +$tools = $client->listTools(); +$resources = $client->listResources(); +$prompts = $client->listPrompts(); +``` + +**Usage:** +```bash +# Start the server first +php -S localhost:8000 examples/server/http-discovery-calculator/server.php + +# Then run the client +php examples/client/http_discovery_calculator.php +``` + +### STDIO Client Communication + +**File**: `examples/client/stdio_client_communication.php` + +**What it demonstrates:** +- Server-to-client communication (logging, progress, sampling) +- Handling logging notifications from server +- Implementing sampling callbacks for LLM requests +- Progress tracking during tool execution + +**Key Features:** +```php +use Mcp\Client\Handler\Notification\LoggingNotificationHandler; +use Mcp\Client\Handler\Request\SamplingRequestHandler; +use Mcp\Client\Handler\Request\SamplingCallbackInterface; +use Mcp\Schema\ClientCapabilities; + +$loggingHandler = new LoggingNotificationHandler( + static function (LoggingMessageNotification $n) { + echo "[LOG {$n->level->value}] {$n->data}\n"; + } +); + +$samplingHandler = new SamplingRequestHandler(new class implements SamplingCallbackInterface { + public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult + { + // Perform LLM sampling and return result + } +}); + +$client = Client::builder() + ->setCapabilities(new ClientCapabilities(sampling: true)) + ->addNotificationHandler($loggingHandler) + ->addRequestHandler($samplingHandler) + ->build(); + +// Call tool with progress tracking +$result = $client->callTool( + name: 'run_dataset_quality_checks', + arguments: ['dataset' => 'customer_orders_2024'], + onProgress: static function (float $progress, ?float $total, ?string $message) { + $percent = $total > 0 ? round(($progress / $total) * 100) : '?'; + echo "[PROGRESS {$percent}%] {$message}\n"; + } +); +``` + +**Usage:** +```bash +# Run the client (automatically starts the communication server) +php examples/client/stdio_client_communication.php +``` + +### HTTP Client Communication + +**File**: `examples/client/http_client_communication.php` + +**What it demonstrates:** +- Server-to-client communication over HTTP +- Receiving logging and progress notifications via SSE streaming +- Implementing sampling for HTTP-based servers +- Progress tracking with long-running operations + +**Key Features:** +- Same client-side code as STDIO version +- Uses HttpTransport instead of StdioTransport +- Demonstrates SSE-based real-time notifications +- Shows HTTP session management + +**Usage:** +```bash +# Start the server +php -S 127.0.0.1:8000 examples/server/client-communication/server.php + +# Run the client +php examples/client/http_client_communication.php +``` + +> [!NOTE] +> For sampling with HTTP transport, the server must support concurrent request processing (e.g., using Symfony CLI, PHP-FPM, or a production web server). PHP's built-in development server cannot handle the concurrent requests required for sampling. diff --git a/docs/index.md b/docs/index.md index 5bfe45ea..86a30185 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,7 @@ - [MCP Elements](mcp-elements.md) — Core capabilities (Tools, Resources, Resource Templates, and Prompts) with registration methods. - [Server Builder](server-builder.md) — Fluent builder class for creating and configuring MCP server instances. +- [Client](client.md) — Client SDK for connecting to and communicating with MCP servers. - [Transports](transports.md) — STDIO and HTTP transport implementations with guidance on choosing between them. - [Server-Client Communication](server-client-communication.md) — Methods for servers to communicate back to clients: sampling, logging, progress, and notifications. - [Examples](examples.md) — Example projects demonstrating attribute-based discovery, dependency injection, HTTP transport, and more.