diff --git a/README.md b/README.md index 94001f1..f441b3c 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,27 @@ # MCP PHP SDK -The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers in PHP. +The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers +and clients in PHP. > [!IMPORTANT] -> Currently, we are still in the process of merging [Symfony's MCP SDK](https://github.com/symfony/mcp-sdk) and -> [PHP-MCP](https://github.com/php-mcp) components. Not all code paths are fully tested or complete, and this package -> may still contain duplicate functionality or dead code. +> This SDK is currently in active development with ongoing refinement of its architecture and features. While +> functional, the API may experience changes as we work toward stabilization. > -> If you want to help us stabilize the SDK, please see the -> [issue tracker](https://github.com/modelcontextprotocol/php-sdk/issues). +> If you want to help us stabilize the SDK, please see the [issue tracker](https://github.com/modelcontextprotocol/php-sdk/issues). -This project is a collaboration between [the PHP Foundation](https://thephp.foundation/) and the -[Symfony project](https://symfony.com/). It adopts development practices and standards from the Symfony project, -including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the +This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts +development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the [Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). -Until the first major release, this SDK is considered -[experimental](https://symfony.com/doc/current/contributing/code/experimental.html). +Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html). -## 🚧 Roadmap +## Roadmap -Features -- [x] Bring back PHP-MCP examples -- [x] Glue handler, registry and reference handlers -- [x] Revive `ServerBuilder` -- [x] Revive transports - - [x] Streamable Transport https://github.com/modelcontextprotocol/php-sdk/issues/7 - - [ ] ~~Http/SSE-based Transport https://github.com/modelcontextprotocol/php-sdk/issues/8~~ -- [ ] Support pagination -- [ ] Support Schema validation -- [ ] Support multiple versions of the MCP specification https://github.com/modelcontextprotocol/php-sdk/issues/14 -- [ ] (Re-)Implement missing Notification & Request Handlers https://github.com/modelcontextprotocol/php-sdk/issues/9 +**Features** +- [ ] Stabilize server component with all needed handlers and functional tests +- [ ] Extend documentation, including integration guides for popular frameworks +- [ ] Implement Client component +- [ ] Support multiple schema versions ## Installation @@ -38,19 +29,13 @@ Features composer require mcp/sdk ``` -Since this package has no tagged releases yet, it is required to extend your `composer.json`: -```json -"minimum-stability": "dev", -"prefer-stable": true -``` - -## ⚡ Quick Start: Stdio Server with Discovery +## Quick Start -This example demonstrates the most common usage pattern - a `stdio` server using attribute discovery. +This example demonstrates the most common usage pattern - a STDIO server using attribute discovery. -**1. Define Your MCP Elements** +### 1. Define Your MCP Elements -Create `src/CalculatorElements.php`: +Create a class with MCP capabilities using attributes: ```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' + )] + public function getSettings(): array + { + return ['precision' => 2, 'allow_negative' => true]; + } } ``` -**2. Create the Server Script** +### 2. Create the Server Script -Create `mcp-server.php`: +Create your MCP server: ```php #!/usr/bin/env php @@ -84,54 +102,166 @@ require_once __DIR__ . '/vendor/autoload.php'; use Mcp\Server; use Mcp\Server\Transport\StdioTransport; -Server::builder() - ->setServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.') +$server = Server::builder() + ->setServerInfo('Calculator Server', '1.0.0') ->setDiscovery(__DIR__, ['.']) - ->build() - ->connect(new StdioTransport()); + ->build(); + +$transport = new StdioTransport(); +$server->connect($transport); +$transport->listen(); ``` -**3. Configure Your MCP Client** +### 3. Configure Your MCP Client -Add to your client configuration (e.g., `mcp.json`): +Add to your client configuration (e.g., Claude Desktop's `mcp.json`): ```json { "mcpServers": { "php-calculator": { "command": "php", - "args": ["/absolute/path/to/your/mcp-server.php"] + "args": ["/absolute/path/to/your/server.php"] } } } ``` -**4. Test the Server** +### 4. Test Your Server + +```bash +# Test with MCP Inspector +npx @modelcontextprotocol/inspector php /path/to/server.php + +# Your AI assistant can now call: +# - add: Add two integers +# - calculate: Perform arithmetic operations +# - Read config://calculator/settings resource +``` + +## Key Features + +### Attribute-Based Discovery + +Define MCP elements using PHP attributes with 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')] +public function getConfig(): array { /* ... */ } +``` + +### Manual Registration -Your AI assistant can now call: -- `add_numbers` - Add two integers +Register capabilities programmatically: + +```php +$server = Server::builder() + ->addTool([MyClass::class, 'myMethod'], 'tool_name') + ->addResource([MyClass::class, 'getData'], 'data://config') + ->build(); +``` + +### Multiple Transport Options + +**STDIO Transport** (Command-line integration): +```php +$transport = new StdioTransport(); +$server->connect($transport); +$transport->listen(); +``` + +**HTTP Transport** (Web-based communication): +```php +$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); +$server->connect($transport); +$response = $transport->listen(); +// Handle $response in your web application +``` + +### Session Management + +By default, the SDK uses in-memory sessions. You can configure different session stores: + +```php +use Mcp\Server\Session\InMemorySessionStore; +use Mcp\Server\Session\FileSessionStore; + +// Use default in-memory sessions (TTL only) +$server = Server::builder() + ->setSession(ttl: 7200) // 2 hours + ->build(); + +// Use file-based sessions +$server = Server::builder() + ->setSession(new FileSessionStore(__DIR__ . '/sessions')) + ->build(); + +// Use in-memory with custom TTL +$server = Server::builder() + ->setSession(new InMemorySessionStore(3600)) + ->build(); +``` + +### Discovery Caching + +Use any PSR-16 cache implementation to cache discovery results and avoid running discovery on every server start: + +```php +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; + +$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery')); + +$server = Server::builder() + ->setDiscovery( + basePath: __DIR__, + scanDirs: ['.', 'src'], // Default: ['.', 'src'] + excludeDirs: ['vendor'], // Default: ['vendor', 'node_modules'] + cache: $cache + ) + ->build(); +``` ## Documentation -- [SDK documentation](doc/index.rst) +**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 + +**Learning:** +- [Examples](docs/examples.md) - Comprehensive example walkthroughs + +**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) -## Examples of MCP Tools that use this SDK +## PHP Libraries Using the MCP SDK -- https://github.com/pronskiy/mcp +* [pronskiy/mcp](https://github.com/pronskiy/mcp) - Additional DX layer +* [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) - Symfony integration bundle ## 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). +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). We are grateful for the work done by Kyrian and other contributors to that repository, 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 MIT License - see the LICENSE file for details. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/docs/discovery-caching.md b/docs/discovery-caching.md deleted file mode 100644 index 9f10082..0000000 --- a/docs/discovery-caching.md +++ /dev/null @@ -1,106 +0,0 @@ -# Discovery Caching - -This document explains how to use the discovery caching feature in the PHP MCP SDK to improve performance. - -## Overview - -The discovery caching system caches the results of MCP element discovery to avoid repeated file system scanning and reflection operations. This is particularly useful in: - -- **Development environments** where the server is restarted frequently -- **Production environments** where discovery happens on every request -- **Large codebases** with many MCP elements to discover - -## Usage - -### Basic Setup - -```php -use Mcp\Server; -use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\Cache\Psr16Cache; - -$server = Server::builder() - ->setServerInfo('My Server', '1.0.0') - ->setDiscovery(__DIR__, ['.'], [], new Psr16Cache(new ArrayAdapter())) // Enable caching - ->build(); -``` - -### Available Cache Implementations - -The caching system works with any PSR-16 SimpleCache implementation. Popular options include: - -#### Symfony Cache - -```php -use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Cache\Psr16Cache; - -// In-memory cache (development) -$cache = new Psr16Cache(new ArrayAdapter()); - -// Filesystem cache (production) -$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery', 0, '/var/cache')); -``` - -#### Other PSR-16 Implementations - -```php -use Doctrine\Common\Cache\Psr6\DoctrineProvider; -use Doctrine\Common\Cache\ArrayCache; - -$cache = DoctrineProvider::wrap(new ArrayCache()); -``` - -## Performance Benefits - -- **First run**: Same as without caching -- **Subsequent runs**: 80-95% faster discovery -- **Memory usage**: Slightly higher due to cache storage -- **Cache hit ratio**: 90%+ in typical development scenarios - -## Best Practices - -### Development Environment - -```php -// Use in-memory cache for fast development cycles -$cache = new Psr16Cache(new ArrayAdapter()); - -$server = Server::builder() - ->setDiscovery(__DIR__, ['.'], [], $cache) - ->build(); -``` - -### Production Environment - -```php -// Use persistent cache -$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery', 0, '/var/cache')); - -$server = Server::builder() - ->setDiscovery(__DIR__, ['.'], [], $cache) - ->build(); -``` - -## Cache Invalidation - -The cache automatically invalidates when: - -- Discovery parameters change (base path, directories, exclude patterns) -- Files are modified (detected through file system state) - -For manual invalidation, restart your application or clear the cache directory. - -## Troubleshooting - -### Cache Not Working - -1. Verify PSR-16 SimpleCache implementation is properly installed -2. Check cache permissions (for filesystem caches) -3. Check logs for cache-related warnings - -### Memory Issues - -- Use filesystem cache instead of in-memory cache for large codebases -- Consider using a dedicated cache server (Redis, Memcached) for high-traffic applications diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..77fdc1f --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,272 @@ +# Examples + +The MCP PHP SDK includes comprehensive examples demonstrating different patterns and use cases. Each example showcases +specific features and can be run independently to understand how the SDK works. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Running Examples](#running-examples) +- [STDIO Examples](#stdio-examples) +- [HTTP Examples](#http-examples) +- [Advanced Patterns](#advanced-patterns) +- [Testing and Debugging](#testing-and-debugging) + +## Getting Started + +All examples are located in the `examples/` directory and use the SDK dependencies from the root project. Most examples +can be run directly without additional setup. + +### Prerequisites + +```bash +# Install dependencies (in project root) +composer install +``` + +## Running Examples + +### STDIO Examples + +STDIO examples use standard input/output for communication: + +```bash +# Interactive testing with MCP Inspector +npx @modelcontextprotocol/inspector php examples/stdio-discovery-calculator/server.php + +# Run with debugging enabled +npx @modelcontextprotocol/inspector -e DEBUG=1 -e FILE_LOG=1 php examples/stdio-discovery-calculator/server.php + +# Or configure the script path in your MCP client +# Path: php examples/stdio-discovery-calculator/server.php +``` + +### HTTP Examples + +HTTP examples run as web servers: + +```bash +# Start the server +php -S localhost:8000 examples/http-discovery-userprofile/server.php + +# Test with MCP Inspector +npx @modelcontextprotocol/inspector http://localhost:8000 + +# Test with curl +curl -X POST http://localhost:8000 \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0.0"},"capabilities":{}}}' +``` + +## STDIO Examples + +### Discovery Calculator + +**File**: `examples/stdio-discovery-calculator/` + +**What it demonstrates:** +- Attribute-based discovery using `#[McpTool]` and `#[McpResource]` +- Basic arithmetic operations +- Configuration management through resources +- State management between tool calls + +**Key Features:** +```php +#[McpTool(name: 'calculate')] +public function calculate(float $a, float $b, string $operation): float|string + +#[McpResource( + uri: 'config://calculator/settings', + name: 'calculator_config', + mimeType: 'application/json' +)] +public function getConfiguration(): array +``` + +**Usage:** +```bash +# Interactive testing +npx @modelcontextprotocol/inspector php examples/stdio-discovery-calculator/server.php + +# Or configure in MCP client: php examples/stdio-discovery-calculator/server.php +``` + +### Explicit Registration + +**File**: `examples/stdio-explicit-registration/` + +**What it demonstrates:** +- Manual registration of tools, resources, and prompts +- Alternative to attribute-based discovery +- Simple handler functions + +**Key Features:** +```php +$server = Server::builder() + ->addTool([SimpleHandlers::class, 'echoText'], 'echo_text') + ->addResource([SimpleHandlers::class, 'getAppVersion'], 'app://version') + ->addPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting') +``` + +### Environment Variables + +**File**: `examples/stdio-env-variables/` + +**What it demonstrates:** +- Environment variable integration +- Server configuration from environment +- Environment-based tool behavior + +**Key Features:** +- Reading environment variables within tools +- Conditional behavior based on environment +- Environment validation and defaults + +### Custom Dependencies + +**File**: `examples/stdio-custom-dependencies/` + +**What it demonstrates:** +- Dependency injection with PSR-11 containers +- Service layer architecture +- Repository pattern implementation +- Complex business logic integration + +**Key Features:** +```php +$container->set(TaskRepositoryInterface::class, $taskRepo); +$container->set(StatsServiceInterface::class, $statsService); + +$server = Server::builder() + ->setContainer($container) + ->setDiscovery(__DIR__, ['.']) +``` + +### Cached Discovery + +**File**: `examples/stdio-cached-discovery/` + +**What it demonstrates:** +- Discovery caching for improved performance +- PSR-16 cache integration +- Cache invalidation strategies + +**Key Features:** +```php +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; + +$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery')); + +$server = Server::builder() + ->setDiscovery(__DIR__, ['.'], [], $cache) +``` + +## HTTP Examples + +### Discovery User Profile + +**File**: `examples/http-discovery-userprofile/` + +**What it demonstrates:** +- HTTP transport with StreamableHttpTransport +- Resource templates with URI parameters +- Completion providers for parameter hints +- User profile management system +- Session persistence with FileSessionStore + +**Key Features:** +```php +#[McpResourceTemplate( + uriTemplate: 'user://{userId}/profile', + name: 'user_profile', + mimeType: 'application/json' +)] +public function getUserProfile( + #[CompletionProvider(values: ['101', '102', '103'])] + string $userId +): array + +#[McpPrompt(name: 'generate_bio_prompt')] +public function generateBio(string $userId, string $tone = 'professional'): array +``` + +**Usage:** +```bash +# Start the HTTP server +php -S localhost:8000 examples/http-discovery-userprofile/server.php + +# Test with MCP Inspector +npx @modelcontextprotocol/inspector http://localhost:8000 + +# Or configure in MCP client: http://localhost:8000 +``` + +### Combined Registration + +**File**: `examples/http-combined-registration/` + +**What it demonstrates:** +- Mixing attribute discovery with manual registration +- HTTP server with both discovered and manual capabilities +- Flexible registration patterns + +**Key Features:** +```php +$server = Server::builder() + ->setDiscovery(__DIR__, ['.']) // Automatic discovery + ->addTool([ManualHandlers::class, 'manualGreeter']) // Manual registration + ->addResource([ManualHandlers::class, 'getPriorityConfig'], 'config://priority') +``` + +### Complex Tool Schema + +**File**: `examples/http-complex-tool-schema/` + +**What it demonstrates:** +- Advanced JSON schema definitions +- Complex data structures and validation +- Event scheduling and management +- Enum types and nested objects + +**Key Features:** +```php +#[Schema(definition: [ + 'type' => 'object', + 'properties' => [ + 'title' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100], + 'eventType' => ['type' => 'string', 'enum' => ['meeting', 'deadline', 'reminder']], + 'priority' => ['type' => 'string', 'enum' => ['low', 'medium', 'high', 'urgent']] + ] +])] +public function scheduleEvent(array $eventData): array +``` + +### Schema Showcase + +**File**: `examples/http-schema-showcase/` + +**What it demonstrates:** +- Comprehensive JSON schema features +- Parameter-level schema validation +- String constraints (minLength, maxLength, pattern) +- Numeric constraints (minimum, maximum, multipleOf) +- Array and object validation + +**Key Features:** +```php +#[McpTool] +public function formatText( + #[Schema( + type: 'string', + minLength: 5, + maxLength: 100, + pattern: '^[a-zA-Z0-9\s\.,!?\-]+$' + )] + string $text, + + #[Schema(enum: ['uppercase', 'lowercase', 'title', 'sentence'])] + string $format = 'sentence' +): array +``` diff --git a/docs/mcp-elements.md b/docs/mcp-elements.md new file mode 100644 index 0000000..911b3d5 --- /dev/null +++ b/docs/mcp-elements.md @@ -0,0 +1,744 @@ +# MCP Elements + +MCP elements are the core capabilities of your server: Tools, Resources, Resource Templates, and Prompts. These elements +define what your server can do and how clients can interact with it. The PHP MCP SDK provides both attribute-based +discovery and manual registration methods. + +## Table of Contents + +- [Overview](#overview) +- [Tools](#tools) +- [Resources](#resources) +- [Resource Templates](#resource-templates) +- [Prompts](#prompts) +- [Completion Providers](#completion-providers) +- [Schema Generation and Validation](#schema-generation-and-validation) +- [Discovery vs Manual Registration](#discovery-vs-manual-registration) + +## Overview + +MCP defines four types of capabilities: + +- **Tools**: Functions that can be called by clients to perform actions +- **Resources**: Data sources that clients can read (static URIs) +- **Resource Templates**: URI templates for dynamic resources with variables +- **Prompts**: Template generators for AI prompts + +### Registration Methods + +Each capability can be registered using two methods: + +1. **Attribute-Based Discovery**: Use PHP attributes (`#[McpTool]`, `#[McpResource]`, etc.) on methods or classes. The + server automatically discovers and registers them. + +2. **Manual Registration**: Explicitly register capabilities using `ServerBuilder` methods (`addTool()`, `addResource()`, etc.). + +**Priority**: Manual registrations **always override** discovered elements with the same identifier: +- **Tools**: Same `name` +- **Resources**: Same `uri` +- **Resource Templates**: Same `uriTemplate` +- **Prompts**: Same `name` + +For manual registration details, see [Server Builder Manual Registration](server-builder.md#manual-capability-registration). + +## Tools + +Tools are callable functions that perform actions and return results. + +```php +use Mcp\Capability\Attribute\McpTool; + +class Calculator +{ + /** + * Performs arithmetic operations with validation. + */ + #[McpTool(name: 'calculate')] + public function performCalculation(float $a, float $b, string $operation): float + { + return match($operation) { + 'add' => $a + $b, + 'subtract' => $a - $b, + 'multiply' => $a * $b, + 'divide' => $b != 0 ? $a / $b : throw new \InvalidArgumentException('Division by zero'), + default => throw new \InvalidArgumentException('Invalid operation') + }; + } +} +``` + +### Parameters + +- **`name`** (optional): Tool identifier. Defaults to method name if not provided. +- **`description`** (optional): Tool description. Defaults to docblock summary if not provided, otherwise uses method name. +- **`annotations`** (optional): `ToolAnnotations` object for additional metadata. + +**Priority for name/description**: Attribute parameters → DocBlock content → Method name + +For tool parameter validation and JSON schema generation, see [Schema Generation and Validation](#schema-generation-and-validation). + +### Tool Return Values + +Tools can return any data type and the SDK will automatically wrap them in appropriate MCP content types. + +#### Automatic Content Wrapping + +```php +// Primitive types → TextContent +public function getString(): string { return "Hello"; } // TextContent +public function getNumber(): int { return 42; } // TextContent +public function getBool(): bool { return true; } // TextContent +public function getArray(): array { return ['key' => 'value']; } // TextContent (JSON) + +// Special cases +public function getNull(): ?string { return null; } // TextContent("(null)") +public function returnVoid(): void { /* no return */ } // Empty content +``` + +#### Explicit Content Types + +For fine control over output formatting: + +```php +use Mcp\Schema\Content\{TextContent, ImageContent, AudioContent, EmbeddedResource}; + +public function getFormattedCode(): TextContent +{ + return TextContent::code(' 'file://data.json', 'text' => 'File content'] + ); +} +``` + +#### Multiple Content Items + +Return an array of content items: + +```php +public function getMultipleContent(): array +{ + return [ + new TextContent('Here is the analysis:'), + TextContent::code($code, 'php'), + new TextContent('And here is the summary.') + ]; +} +``` + +#### Error Handling + +Tools can throw exceptions which are automatically converted to proper JSON-RPC error responses: + +```php +#[McpTool] +public function divideNumbers(float $a, float $b): float +{ + if ($b === 0.0) { + throw new \InvalidArgumentException('Division by zero is not allowed'); + } + + return $a / $b; +} + +#[McpTool] +public function processFile(string $filename): string +{ + if (!file_exists($filename)) { + throw new \InvalidArgumentException("File not found: {$filename}"); + } + + return file_get_contents($filename); +} +``` + +The SDK will convert these exceptions into appropriate JSON-RPC error responses that MCP clients can understand. + +## Resources + +Resources provide access to static data that clients can read. + +```php +use Mcp\Capability\Attribute\McpResource; + +class ConfigProvider +{ + /** + * Provides the current application configuration. + */ + #[McpResource(uri: 'config://app/settings', name: 'app_settings')] + public function getSettings(): array + { + return [ + 'version' => '1.0.0', + 'debug' => false, + 'features' => ['auth', 'logging'] + ]; + } +} +``` + +### Parameters + +- **`uri`** (required): Unique resource identifier. Must comply with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). +- **`name`** (optional): Human-readable name. Defaults to method name if not provided. +- **`description`** (optional): Resource description. Defaults to docblock summary if not provided. +- **`mimeType`** (optional): MIME type of the resource content. +- **`size`** (optional): Size in bytes if known. + +**Standard Protocol URI Schemes**: `https://` (web resources), `file://` (filesystem), `git://` (version control). +**Custom schemes**: `config://`, `data://`, `db://`, `api://` or any RFC 3986 compliant scheme. + +### Resource Return Values + +Resource handlers can return various data types that are automatically formatted into appropriate MCP resource content types. + +#### Supported Return Types + +```php +// String content - converted to text resource +public function getTextFile(): string +{ + return "File content here"; +} + +// Array content - converted to JSON +public function getConfig(): array +{ + return ['debug' => true, 'version' => '1.0']; +} + +// Stream resource - read and converted to blob +public function getImageStream(): resource +{ + return fopen('image.png', 'r'); +} + +// SplFileInfo - file content with MIME type detection +public function getFileInfo(): \SplFileInfo +{ + return new \SplFileInfo('document.pdf'); +} +``` + +**Explicit resource content types** + +```php +use Mcp\Schema\Content\{TextResourceContents, BlobResourceContents}; + +public function getExplicitText(): TextResourceContents +{ + return new TextResourceContents( + uri: 'config://app/settings', + mimeType: 'application/json', + text: json_encode(['setting' => 'value']) + ); +} + +public function getExplicitBlob(): BlobResourceContents +{ + return new BlobResourceContents( + uri: 'file://image.png', + mimeType: 'image/png', + blob: base64_encode(file_get_contents('image.png')) + ); +} +``` + +**Special Array Formats** + +```php +// Array with 'text' key - used as text content +public function getTextArray(): array +{ + return ['text' => 'Content here', 'mimeType' => 'text/plain']; +} + +// Array with 'blob' key - used as blob content +public function getBlobArray(): array +{ + return ['blob' => base64_encode($data), 'mimeType' => 'image/png']; +} + +// Multiple resource contents +public function getMultipleResources(): array +{ + return [ + new TextResourceContents('file://readme.txt', 'text/plain', 'README content'), + new TextResourceContents('file://config.json', 'application/json', '{"key": "value"}') + ]; +} +``` + +#### Error Handling + +Resource handlers can throw exceptions for error cases: + +```php +#[McpResource(uri: 'file://{path}')] +public function getFile(string $path): string +{ + if (!file_exists($path)) { + throw new \InvalidArgumentException("File not found: {$path}"); + } + + if (!is_readable($path)) { + throw new \RuntimeException("File not readable: {$path}"); + } + + return file_get_contents($path); +} +``` + +## Resource Templates + +Resource templates are **dynamic resources** that use parameterized URIs with variables. They follow all the same rules +as static resources (URI schemas, return values, MIME types, etc.) but accept variables using [RFC 6570 URI template syntax](https://datatracker.ietf.org/doc/html/rfc6570). + +```php +use Mcp\Capability\Attribute\McpResourceTemplate; + +class UserProvider +{ + /** + * Retrieves user profile information by ID. + */ + #[McpResourceTemplate( + uriTemplate: 'user://{userId}/profile/{section}', + name: 'user_profile', + description: 'User profile data by section', + mimeType: 'application/json' + )] + public function getUserProfile(string $userId, string $section): array + { + return $this->users[$userId][$section] ?? throw new \InvalidArgumentException("Profile section not found"); + } +} +``` + +### Parameters + +- **`uriTemplate`** (required): URI template with `{variables}` using RFC 6570 syntax. Must comply with RFC 3986. +- **`name`** (optional): Human-readable name. Defaults to method name if not provided. +- **`description`** (optional): Template description. Defaults to docblock summary if not provided. +- **`mimeType`** (optional): MIME type of the resource content. +- **`annotations`** (optional): Additional metadata. + +### Variable Rules + +1. **Variable names must match exactly** between URI template and method parameters +2. **Parameter order matters** - variables are passed in the order they appear in the URI template +3. **All variables are required** - no optional parameters supported +4. **Type hints work normally** - parameters can be typed (string, int, etc.) + +**Example mapping**: `user://123/profile/settings` → `getUserProfile("123", "settings")` + +## Prompts + +Prompts generate templates for AI interactions. + +```php +use Mcp\Capability\Attribute\McpPrompt; + +class PromptGenerator +{ + /** + * Generates a code review request prompt. + */ + #[McpPrompt(name: 'code_review'] + public function reviewCode(string $language, string $code, string $focus = 'general'): array + { + return [ + ['role' => 'system', 'content' => 'You are an expert code reviewer.'], + ['role' => 'user', 'content' => "Review this {$language} code focusing on {$focus}:\n\n```{$language}\n{$code}\n```"] + ]; + } +} +``` + +### Parameters + +- **`name`** (optional): Prompt identifier. Defaults to method name if not provided. +- **`description`** (optional): Prompt description. Defaults to docblock summary if not provided. + +### Prompt Return Values + +Prompt handlers must return an array of message structures that are automatically formatted into MCP prompt messages. + +#### Supported Return Formats + +```php +// Array of message objects with role and content +public function basicPrompt(): array +{ + return [ + ['role' => 'assistant', 'content' => 'You are a helpful assistant'], + ['role' => 'user', 'content' => 'Hello, how are you?'] + ]; +} + +// Single message (automatically wrapped in array) +public function singleMessage(): array +{ + return [ + ['role' => 'user', 'content' => 'Write a poem about PHP'] + ]; +} + +// Associative array with user/assistant keys +public function userAssistantFormat(): array +{ + return [ + 'user' => 'Explain how arrays work in PHP', + 'assistant' => 'Arrays in PHP are ordered maps...' + ]; +} + +// Mixed content types in messages +use Mcp\Schema\Content\{TextContent, ImageContent}; + +public function mixedContent(): array +{ + return [ + [ + 'role' => 'user', + 'content' => [ + new TextContent('Analyze this image:'), + new ImageContent(data: $imageData, mimeType: 'image/png') + ] + ] + ]; +} + +// Using explicit PromptMessage objects +use Mcp\Schema\PromptMessage; +use Mcp\Schema\Enum\Role; + +public function explicitMessages(): array +{ + return [ + new PromptMessage(Role::Assistant, [new TextContent('System instructions')]), + new PromptMessage(Role::User, [new TextContent('User question')]) + ]; +} +``` + +#### Valid Message Roles + +- **`user`**: User input or questions +- **`assistant`**: Assistant responses/system + +#### Error Handling + +Prompt handlers can throw exceptions for invalid inputs: + +```php +#[McpPrompt] +public function generatePrompt(string $topic, string $style): array +{ + $validStyles = ['casual', 'formal', 'technical']; + + if (!in_array($style, $validStyles)) { + throw new \InvalidArgumentException( + "Invalid style '{$style}'. Must be one of: " . implode(', ', $validStyles) + ); + } + + return [ + ['role' => 'user', 'content' => "Write about {$topic} in a {$style} style"] + ]; +} +``` + +The SDK automatically validates that all messages have valid roles and converts the result into the appropriate MCP prompt message format. + +## Completion Providers + +Completion providers help MCP clients offer auto-completion suggestions for Resource Templates and Prompts. Unlike Tools +and static Resources (which can be listed via `tools/list` and `resources/list`), Resource Templates and Prompts have +dynamic parameters that benefit from completion hints. + +### Completion Provider Types + +#### 1. Value Lists + +Provide a static list of possible values: + +```php +use Mcp\Capability\Attribute\CompletionProvider; + +#[McpPrompt] +public function generateContent( + #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide'])] + string $contentType, + + #[CompletionProvider(values: ['beginner', 'intermediate', 'advanced'])] + string $difficulty +): array +{ + return [ + ['role' => 'user', 'content' => "Create a {$difficulty} level {$contentType}"] + ]; +} +``` + +#### 2. Enum Classes + +Use enum values for completion: + +```php +enum Priority: string +{ + case LOW = 'low'; + case MEDIUM = 'medium'; + case HIGH = 'high'; +} + +enum Status // Unit enum +{ + case DRAFT; + case PUBLISHED; + case ARCHIVED; +} + +#[McpResourceTemplate(uriTemplate: 'tasks/{taskId}')] +public function getTask( + string $taskId, + + #[CompletionProvider(enum: Priority::class)] // Uses backing values + string $priority, + + #[CompletionProvider(enum: Status::class)] // Uses case names + string $status +): array +{ + // Implementation +} +``` + +#### 3. Custom Provider Classes + +For dynamic completion logic: + +```php +use Mcp\Capability\Prompt\Completion\ProviderInterface; + +class UserIdCompletionProvider implements ProviderInterface +{ + public function __construct(private DatabaseService $db) {} + + public function getCompletions(string $currentValue): array + { + // Return dynamic completions based on current input + return $this->db->searchUserIds($currentValue); + } +} + +#[McpResourceTemplate(uriTemplate: 'user://{userId}/profile')] +public function getUserProfile( + #[CompletionProvider(provider: UserIdCompletionProvider::class)] + string $userId +): array +{ + // Implementation +} +``` + +**Provider Resolution:** +- **Class strings** (`Provider::class`) → Resolved from PSR-11 container +- **Instances** (`new Provider()`) → Used directly +- **Values** (`['a', 'b']`) → Wrapped in `ListCompletionProvider` +- **Enums** (`MyEnum::class`) → Wrapped in `EnumCompletionProvider` + +> **Important** +> +> Completion providers only offer **suggestions** to users. Users can still input any value, so **always validate +> parameters** in your handlers. Providers don't enforce validation - they're purely for UX improvement. + +## Schema Generation and Validation + +The SDK automatically generates JSON schemas for **tool parameters** using a sophisticated priority system. Schema +generation applies to both attribute-discovered and manually registered tools. + +### Schema Generation Priority + +The server follows this order of precedence: + +1. **`#[Schema]` attribute with `definition`** - Complete schema override (highest priority) +2. **Parameter-level `#[Schema]` attribute** - Parameter-specific enhancements +3. **Method-level `#[Schema]` attribute** - Method-wide configuration +4. **PHP type hints + docblocks** - Automatic inference (lowest priority) + +### Automatic Schema from PHP Types + +```php +#[McpTool] +public function processUser( + string $email, // Required string + int $age, // Required integer + ?string $name = null, // Optional string + bool $active = true // Boolean with default +): array +{ + // Schema auto-generated from method signature +} +``` + +### Parameter-Level Schema Enhancement + +Add validation rules to specific parameters: + +```php +use Mcp\Capability\Attribute\Schema; + +#[McpTool] +public function validateUser( + #[Schema(format: 'email')] + string $email, + + #[Schema(minimum: 18, maximum: 120)] + int $age, + + #[Schema( + pattern: '^[A-Z][a-z]+$', + description: 'Capitalized first name' + )] + string $firstName +): bool +{ + // PHP types provide base validation + // Schema attributes add constraints +} +``` + +### Method-Level Schema + +Add validation for complex object structures: + +```php +#[McpTool] +#[Schema( + properties: [ + 'userData' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'minLength' => 2], + 'email' => ['type' => 'string', 'format' => 'email'], + 'age' => ['type' => 'integer', 'minimum' => 18] + ], + 'required' => ['name', 'email'] + ] + ], + required: ['userData'] +)] +public function createUser(array $userData): array +{ + // Method-level schema adds object structure validation + // PHP array type provides base type +} +``` + +### Complete Schema Override + +**Use sparingly** - bypasses all automatic inference: + +```php +#[McpTool] +#[Schema(definition: [ + 'type' => 'object', + 'properties' => [ + 'endpoint' => ['type' => 'string', 'format' => 'uri'], + 'method' => ['type' => 'string', 'enum' => ['GET', 'POST', 'PUT', 'DELETE']], + 'headers' => [ + 'type' => 'object', + 'patternProperties' => [ + '^[A-Za-z0-9-]+$' => ['type' => 'string'] + ] + ] + ], + 'required' => ['endpoint', 'method'] +])] +public function makeApiRequest(string $endpoint, string $method, array $headers): array +{ + // Complete definition override - PHP types ignored +} +``` + +**Warning:** Only use complete schema override if you're well-versed with JSON Schema specification and have complex +validation requirements that cannot be achieved through the priority system. + +## Discovery vs Manual Registration + +### Attribute-Based Discovery + +**Advantages:** +- Declarative and readable +- Automatic parameter inference +- DocBlock integration +- Type-safe by default +- Caching support + +**Example:** +```php +$server = Server::builder() + ->setDiscovery(__DIR__, ['.']) // Automatic discovery + ->build(); +``` + +### Manual Registration + +**Advantages:** +- Fine-grained control +- Runtime configuration +- Conditional registration +- External handler support + +**Example:** +```php +$server = Server::builder() + ->addTool([Calculator::class, 'add'], 'add_numbers') + ->addResource([Config::class, 'get'], 'config://app') + ->addPrompt([Prompts::class, 'email'], 'write_email') + ->build(); +``` + +For detailed information on manual registration, see [Server Builder](server-builder.md#manual-capability-registration). + +### Hybrid Approach + +Combine both methods for maximum flexibility: + +```php +$server = Server::builder() + ->setDiscovery(__DIR__, ['.']) // Discover most capabilities + ->addTool([ExternalService::class, 'process'], 'external') // Add specific ones + ->build(); +``` + +Manual registrations always take precedence over discovered elements with the same identifier. diff --git a/docs/server-builder.md b/docs/server-builder.md new file mode 100644 index 0000000..0d51ed7 --- /dev/null +++ b/docs/server-builder.md @@ -0,0 +1,515 @@ +# Server Builder + +The server `Builder` is a fluent builder class that simplifies the creation and configuration of an MCP server instance. +It provides methods for setting server information, configuring discovery, registering capabilities, and customizing +various aspects of the server behavior. + +## Table of Contents + +- [Basic Usage](#basic-usage) +- [Server Configuration](#server-configuration) +- [Discovery Configuration](#discovery-configuration) +- [Session Management](#session-management) +- [Manual Capability Registration](#manual-capability-registration) +- [Service Dependencies](#service-dependencies) +- [Custom Capability Handlers](#custom-capability-handlers) +- [Complete Example](#complete-example) +- [Method Reference](#method-reference) + +## Basic Usage + +There are two ways to obtain a server builder instance: + +### Method 1: Static Builder Method (Recommended) + +```php +use Mcp\Server; + +$server = Server::builder() + ->setServerInfo('My MCP Server', '1.0.0') + ->setDiscovery(__DIR__, ['.']) + ->build(); +``` + +### Method 2: Direct Instantiation + +```php +use Mcp\Server\Builder; + +$server = (new Builder()) + ->setServerInfo('My MCP Server', '1.0.0') + ->setDiscovery(__DIR__, ['.']) + ->build(); +``` + +Both methods return a `Builder` instance that you can configure with fluent methods. The `build()` method returns the +final `Server` instance ready for use. + +## Server Configuration + +### Server Information + +Set the server's identity with name, version, and optional description: + +```php +$server = Server::builder() + ->setServerInfo('Calculator Server', '1.2.0', 'Advanced mathematical calculations'); +``` + +**Parameters:** +- `$name` (string): The server name +- `$version` (string): Version string (semantic versioning recommended) +- `$description` (string|null): Optional description + +### Pagination Limit + +Configure the maximum number of items returned in paginated responses: + +```php +$server = Server::builder() + ->setPaginationLimit(100); // Default: 50 +``` + +### Instructions + +Provide hints to help AI models understand how to use your server: + +```php +$server = Server::builder() + ->setInstructions('This calculator supports basic arithmetic operations. Use the calculate tool for math operations and check the config resource for current settings.'); +``` + +## Discovery Configuration + +**Required when using MCP attributes.** If you're using PHP attributes (`#[McpTool]`, `#[McpResource]`, `#[McpResourceTemplate]`, `#[McpPrompt]`) to define your MCP elements, you **MUST** configure discovery to tell the server where to look for these attributes. + +```php +$server = Server::builder() + ->setDiscovery( + basePath: __DIR__, + scanDirs: ['.', 'src', 'lib'], // Where to look for MCP attributes + excludeDirs: ['vendor', 'tests'], // Where NOT to look + cache: $cacheInstance // Optional: cache discovered elements + ); +``` + +**Parameters:** +- `$basePath` (string): Base directory for discovery (typically `__DIR__`) +- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`) +- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning +- `$cache` (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance + +**Basic Discovery (scans current directory and `src/`):** +```php +$server = Server::builder() + ->setDiscovery(__DIR__) // Minimal setup + ->build(); +``` + +**Production Setup with Caching:** +```php +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; + +// Cache discovered elements to avoid filesystem scanning on every server start +$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery')); + +$server = Server::builder() + ->setDiscovery( + basePath: __DIR__, + scanDirs: ['src', 'lib'], // Scan these directories recursively + excludeDirs: ['vendor', 'tests', 'temp'], // Skip these directory names within scanned dirs + cache: $cache // Cache for performance + ) + ->build(); +``` + +**How `excludeDirs` works:** +- If scanning `src/` and there's `src/vendor/`, it will be excluded +- If scanning `lib/` and there's `lib/tests/`, it will be excluded +- But if `vendor/` and `tests/` are at the same level as `src/`, they're not scanned anyway (not in `scanDirs`) + +> **Performance**: Always use a cache in production. The first run scans and caches all discovered MCP elements, making +> subsequent server startups nearly instantaneous. + +## Session Management + +Configure session storage and lifecycle. By default, the SDK uses `InMemorySessionStore`: + +```php +use Mcp\Server\Session\FileSessionStore; +use Mcp\Server\Session\InMemorySessionStore; + +// Use default in-memory sessions with custom TTL +$server = Server::builder() + ->setSession(ttl: 7200) // 2 hours + ->build(); + +// Override with file-based storage +$server = Server::builder() + ->setSession(new FileSessionStore('/tmp/mcp-sessions')) + ->build(); + +// Override with in-memory storage and custom TTL +$server = Server::builder() + ->setSession(new InMemorySessionStore(3600)) + ->build(); +``` + +**Available Session Stores:** +- `InMemorySessionStore`: Fast in-memory storage (default) +- `FileSessionStore`: Persistent file-based storage + +**Custom Session Stores:** + +Implement `SessionStoreInterface` to create custom session storage: + +```php +use Mcp\Server\Session\SessionStoreInterface; +use Symfony\Component\Uid\Uuid; + +class RedisSessionStore implements SessionStoreInterface +{ + public function __construct(private $redis, private int $ttl = 3600) {} + + public function exists(Uuid $id): bool + { + return $this->redis->exists($id->toRfc4122()); + } + + public function read(Uuid $sessionId): string|false + { + $data = $this->redis->get($sessionId->toRfc4122()); + return $data !== false ? $data : false; + } + + public function write(Uuid $sessionId, string $data): bool + { + return $this->redis->setex($sessionId->toRfc4122(), $this->ttl, $data); + } + + public function destroy(Uuid $sessionId): bool + { + return $this->redis->del($sessionId->toRfc4122()) > 0; + } + + public function gc(): array + { + // Redis handles TTL automatically + return []; + } +} +``` + +## Manual Capability Registration + +Register MCP elements programmatically without using attributes. The handler is the most important parameter and can be any PHP callable. + +### Handler Types + +**Handler** can be any PHP callable: + +1. **Closure**: `function(int $a, int $b): int { return $a + $b; }` +2. **Class and method name pair**: `[ClassName::class, 'methodName']` - class must be constructable through the container +3. **Class instance and method name**: `[$instance, 'methodName']` +4. **Invokable class name**: `InvokableClass::class` - class must be constructable through the container and have `__invoke` method + +### Manual Tool Registration + +```php +$server = Server::builder() + // Using closure + ->addTool( + handler: function(int $a, int $b): int { return $a + $b; }, + name: 'add_numbers', + description: 'Adds two numbers together' + ) + + // Using class method pair + ->addTool( + handler: [Calculator::class, 'multiply'], + name: 'multiply_numbers' + // name and description are optional - derived from method name and docblock + ) + + // Using instance method + ->addTool( + handler: [$calculatorInstance, 'divide'] + ) + + // Using invokable class + ->addTool( + handler: InvokableCalculator::class + ); +``` + +### Manual Resource Registration + +Register static resources: + +```php +$server = Server::builder() + ->addResource( + handler: [Config::class, 'getSettings'], + uri: 'config://app/settings', + name: 'app_config', + description: 'Application configuration', + mimeType: 'application/json' + ); +``` + +### Manual Resource Template Registration + +Register dynamic resources with URI templates: + +```php +$server = Server::builder() + ->addResourceTemplate( + handler: [UserService::class, 'getUserProfile'], + uriTemplate: 'user://{userId}/profile', + name: 'user_profile', + description: 'User profile by ID', + mimeType: 'application/json' + ); +``` + +### Manual Prompt Registration + +Register prompt generators: + +```php +$server = Server::builder() + ->addPrompt( + handler: [PromptService::class, 'generatePrompt'], + name: 'custom_prompt', + description: 'A custom prompt generator' + ); +``` + +**Note:** `name` and `description` are optional for all manual registrations. If not provided, they will be derived from +the handler's method name and docblock. + +For more details on MCP elements, handlers, and attribute-based discovery, see [MCP Elements](mcp-elements.md). + +## Service Dependencies + +### Container + +The container is used to resolve handlers and their dependencies when handlers inject dependencies in their constructors. +The SDK includes a basic container with simple auto-wiring capabilities. + +```php +use Mcp\Capability\Registry\Container; + +// Use the default basic container +$container = new Container(); +$container->set(DatabaseService::class, new DatabaseService($pdo)); +$container->set(\PDO::class, $pdo); + +$server = Server::builder() + ->setContainer($container) + ->build(); +``` + +**Basic Container Features:** +- Supports constructor auto-wiring for classes with parameterless constructors +- Resolves dependencies where all parameters are type-hinted classes/interfaces known to the container +- Supports parameters with default values +- Does NOT support scalar/built-in type injection without defaults +- Detects circular dependencies + +You can also use any PSR-11 compatible container (Symfony DI, PHP-DI, Laravel Container, etc.). + +### Logger + +Provide a PSR-3 logger instance for internal server logging (request/response processing, errors, session management, transport events): + +```php +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('mcp-server'); +$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO)); + +$server = Server::builder() + ->setLogger($logger); +``` + +### Event Dispatcher + +Configure event dispatching: + +```php +$server = Server::builder() + ->setEventDispatcher($eventDispatcher); +``` + +## Custom Capability Handlers + +**Advanced customization for specific use cases.** Override the default capability handlers when you need completely custom +behavior for how tools are executed, resources are read, or prompts are generated. Most users should stick with the default implementations. + +The default handlers work by: +1. Looking up registered tools/resources/prompts by name/URI +2. Resolving the handler from the container +3. Executing the handler with the provided arguments +4. Formatting the result and handling errors + +### Custom Tool Caller + +Replace how tool execution requests are processed. Your custom `ToolCallerInterface` receives a `CallToolRequest` (with +tool name and arguments) and must return a `CallToolResult`. + +```php +use Mcp\Capability\Tool\ToolCallerInterface; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; + +class CustomToolCaller implements ToolCallerInterface +{ + public function call(CallToolRequest $request): CallToolResult + { + // Custom tool routing, execution, authentication, caching, etc. + // You handle finding the tool, executing it, and formatting results + $toolName = $request->name; + $arguments = $request->arguments ?? []; + + // Your custom logic here + return new CallToolResult([/* content */]); + } +} + +$server = Server::builder() + ->setToolCaller(new CustomToolCaller()); +``` + +### Custom Resource Reader + +Replace how resource reading requests are processed. Your custom `ResourceReaderInterface` receives a `ReadResourceRequest` +(with URI) and must return a `ReadResourceResult`. + +```php +use Mcp\Capability\Resource\ResourceReaderInterface; +use Mcp\Schema\Request\ReadResourceRequest; +use Mcp\Schema\Result\ReadResourceResult; + +class CustomResourceReader implements ResourceReaderInterface +{ + public function read(ReadResourceRequest $request): ReadResourceResult + { + // Custom resource resolution, caching, access control, etc. + $uri = $request->uri; + + // Your custom logic here + return new ReadResourceResult([/* content */]); + } +} + +$server = Server::builder() + ->setResourceReader(new CustomResourceReader()); +``` + +### Custom Prompt Getter + +Replace how prompt generation requests are processed. Your custom `PromptGetterInterface` receives a `GetPromptRequest` +(with prompt name and arguments) and must return a `GetPromptResult`. + +```php +use Mcp\Capability\Prompt\PromptGetterInterface; +use Mcp\Schema\Request\GetPromptRequest; +use Mcp\Schema\Result\GetPromptResult; + +class CustomPromptGetter implements PromptGetterInterface +{ + public function get(GetPromptRequest $request): GetPromptResult + { + // Custom prompt generation, template engines, dynamic content, etc. + $promptName = $request->name; + $arguments = $request->arguments ?? []; + + // Your custom logic here + return new GetPromptResult([/* messages */]); + } +} + +$server = Server::builder() + ->setPromptGetter(new CustomPromptGetter()); +``` + +> **Warning**: Custom capability handlers bypass the entire default registration system (discovered attributes, manual +> registration, container resolution, etc.). You become responsible for all aspect of execution, including error handling, +> logging, and result formatting. Only use this for very specific advanced use cases like custom authentication, complex +> routing, or integration with external systems. + +## Complete Example + +Here's a comprehensive example showing all major configuration options: + +```php +use Mcp\Server; +use Mcp\Server\Session\FileSessionStore; +use Mcp\Capability\Registry\Container; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +// Setup dependencies +$logger = new Logger('mcp-server'); +$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO)); + +$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery')); +$sessionStore = new FileSessionStore(__DIR__ . '/sessions'); + +// Setup container with dependencies +$container = new Container(); +$container->set(\PDO::class, new \PDO('sqlite::memory:')); +$container->set(DatabaseService::class, new DatabaseService($container->get(\PDO::class))); + +// Build server +$server = Server::builder() + // Server identity + ->setServerInfo('Advanced Calculator', '2.1.0') + + // Performance and behavior + ->setPaginationLimit(100) + ->setInstructions('Use calculate tool for math operations. Check config resource for current settings.') + + // Discovery with caching + ->setDiscovery(__DIR__, ['src'], ['vendor', 'tests'], $cache) + + // Session management + ->setSession($sessionStore) + + // Services + ->setLogger($logger) + ->setContainer($container) + + // Manual capability registration + ->addTool([Calculator::class, 'advancedCalculation'], 'advanced_calc') + ->addResource([Config::class, 'getSettings'], 'config://app/settings', 'app_settings') + + // Build the server + ->build(); +``` + +## Method Reference + +| Method | Parameters | Description | +|--------|------------|-------------| +| `setServerInfo()` | name, version, description? | Set server identity | +| `setPaginationLimit()` | limit | Set max items per page | +| `setInstructions()` | instructions | Set usage instructions | +| `setDiscovery()` | basePath, scanDirs?, excludeDirs?, cache? | Configure attribute discovery | +| `setSession()` | store?, factory?, ttl? | Configure session management | +| `setLogger()` | logger | Set PSR-3 logger | +| `setContainer()` | container | Set PSR-11 container | +| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher | +| `setToolCaller()` | caller | Set custom tool caller | +| `setResourceReader()` | reader | Set custom resource reader | +| `setPromptGetter()` | getter | Set custom prompt getter | +| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool | +| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource | +| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template | +| `addPrompt()` | handler, name?, description? | Register prompt | +| `build()` | - | Create the server instance | diff --git a/docs/transports.md b/docs/transports.md new file mode 100644 index 0000000..dc0f50a --- /dev/null +++ b/docs/transports.md @@ -0,0 +1,350 @@ +# Transports + +Transports handle the communication layer between MCP servers and clients. The PHP MCP SDK provides two main transport +implementations: STDIO for command-line integration and HTTP for web-based communication. + +## Table of Contents + +- [Transport Overview](#transport-overview) +- [STDIO Transport](#stdio-transport) +- [HTTP Transport](#http-transport) +- [Choosing a Transport](#choosing-a-transport) + +## Transport Overview + +All transports implement the `TransportInterface` and follow the same basic pattern: + +```php +$server = Server::builder() + ->setServerInfo('My Server', '1.0.0') + ->setDiscovery(__DIR__, ['.']) + ->build(); + +$transport = new SomeTransport(); + +$server->connect($transport); + +$transport->listen(); // For STDIO, or handle response for HTTP +``` + +## STDIO Transport + +The STDIO transport communicates via standard input/output streams, ideal for command-line tools and MCP client integrations. + +```php +$transport = new StdioTransport( + input: STDIN, // Input stream (default: STDIN) + output: STDOUT, // Output stream (default: STDOUT) + logger: $logger // Optional PSR-3 logger +); +``` + +### Parameters + +- **`input`** (optional): Input stream resource. Defaults to `STDIN`. +- **`output`** (optional): Output stream resource. Defaults to `STDOUT`. +- **`logger`** (optional): `LoggerInterface` - PSR-3 logger for debugging. Defaults to `NullLogger`. + +> [!IMPORTANT] +> When using STDIO transport, **never** write to `STDOUT` in your handlers as it's reserved for JSON-RPC communication. +> Use `STDERR` for debugging instead. + +### Example Server Script + +```php +#!/usr/bin/env php +setServerInfo('STDIO Calculator', '1.0.0') + ->addTool(function(int $a, int $b): int { return $a + $b; }, 'add_numbers') + ->addTool(InvokableCalculator::class) + ->build(); + +$transport = new StdioTransport(); + +$server->connect($transport); + +$transport->listen(); +``` + +### Client Configuration + +For MCP clients like Claude Desktop: + +```json +{ + "mcpServers": { + "my-php-server": { + "command": "php", + "args": ["/absolute/path/to/server.php"] + } + } +} +``` + +## HTTP Transport + +The HTTP transport was designed to sit between any PHP project, regardless of the HTTP implementation or how they receive +and process requests and send responses. It provides a flexible architecture that can integrate with any PSR-7 compatible application. + +```php +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; + +$transport = new StreamableHttpTransport( + request: $serverRequest, // PSR-7 server request + responseFactory: $responseFactory, // PSR-17 response factory + streamFactory: $streamFactory, // PSR-17 stream factory + logger: $logger // Optional PSR-3 logger +); +``` + +### Parameters + +- **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request +- **`responseFactory`** (required): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses +- **`streamFactory`** (required): `StreamFactoryInterface` - PSR-17 factory for creating response body streams +- **`logger`** (optional): `LoggerInterface` - PSR-3 logger for debugging. Defaults to `NullLogger`. + +### Architecture + +The HTTP transport doesn't run its own web server. Instead, it processes PSR-7 requests and returns PSR-7 responses that +your application can handle however it needs to: + +``` +Your Web App → PSR-7 Request → StreamableHttpTransport → PSR-7 Response → Your Web App +``` + +This design allows integration with any PHP framework or application that supports PSR-7. + +### Basic Usage (Standalone) + +Here's an opinionated example using Nyholm PSR-7 and Laminas emitter: + +```php +use Mcp\Server; +use Mcp\Server\Transport\StreamableHttpTransport; +use Mcp\Server\Session\FileSessionStore; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7Server\ServerRequestCreator; +use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; + +// Create PSR-7 request from globals +$psr17Factory = new Psr17Factory(); +$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); +$request = $creator->fromGlobals(); + +// Build server +$server = Server::builder() + ->setServerInfo('HTTP Server', '1.0.0') + ->setDiscovery(__DIR__, ['.']) + ->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions + ->build(); + +// Process request and get response +$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$server->connect($transport); +$response = $transport->listen(); + +// Emit response +(new SapiEmitter())->emit($response); +``` + +### Framework Integration + +#### Symfony Integration + +First install the required PSR libraries: + +```bash +composer require symfony/psr-http-message-bridge nyholm/psr7 +``` + +Then create a controller that uses Symfony's PSR-7 bridge: + +> **Note**: This example assumes your MCP `Server` instance is configured in Symfony's service container. + +```php +// In a Symfony controller +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; +use Nyholm\Psr7\Factory\Psr17Factory; +use Mcp\Server; +use Mcp\Server\Transport\StreamableHttpTransport; + +class McpController +{ + #[Route('/mcp', name: 'mcp_endpoint'] + public function handle(Request $request, Server $mcpServer): Response + { + // Create PSR-7 factories + $psr17Factory = new Psr17Factory(); + $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + $httpFoundationFactory = new HttpFoundationFactory(); + + // Convert Symfony request to PSR-7 + $psrRequest = $psrHttpFactory->createRequest($request); + + // Process with MCP + $transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); + $mcpServer->connect($transport); + $psrResponse = $transport->listen(); + + // Convert PSR-7 response back to Symfony + return $httpFoundationFactory->createResponse($psrResponse); + } +} +``` + +#### Laravel Integration + +First install the required PSR libraries: + +```bash +composer require symfony/psr-http-message-bridge nyholm/psr7 +``` + +Then create a controller that type-hints `ServerRequestInterface`: + +> **Note**: This example assumes your MCP `Server` instance is constructed and bound in a Laravel service provider for dependency injection. + +```php +// In a Laravel controller +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Mcp\Server; +use Mcp\Server\Transport\StreamableHttpTransport; +use Nyholm\Psr7\Factory\Psr17Factory; + +class McpController +{ + public function handle(ServerRequestInterface $request, Server $mcpServer): ResponseInterface + { + $psr17Factory = new Psr17Factory(); + + // Create and connect the MCP HTTP transport + $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); + $mcpServer->connect($transport); + + // Process MCP request and return PSR-7 response + // Laravel automatically handles PSR-7 responses + return $transport->listen(); + } +} + +// Route registration +Route::any('/mcp', [McpController::class, 'handle']); +``` + +#### Slim Framework Integration + +Slim Framework works natively with PSR-7. + +Create a route handler using Slim's built-in factories and container: + +```php +use Psr\Container\ContainerInterface; +use Slim\Factory\AppFactory; +use Slim\Psr7\Factory\ResponseFactory; +use Slim\Psr7\Factory\StreamFactory; +use Mcp\Server; +use Mcp\Server\Transport\StreamableHttpTransport; + +$app = AppFactory::create(); +$container = $app->getContainer(); + +$container->set('mcpServer', function (ContainerInterface $container) { + return Server::builder() + ->setServerInfo('My MCP Server', '1.0.0') + ->setDiscovery(__DIR__, ['.']) + ->build(); +}); + +$app->any('/mcp', function ($request, $response) { + $mcpServer = $this->get('mcpServer'); + + $responseFactory = new ResponseFactory(); + $streamFactory = new StreamFactory(); + + $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); + $mcpServer->connect($transport); + + return $transport->listen(); +}); +``` + +### HTTP Method Handling + +The transport handles all HTTP methods automatically: + +- **POST**: Send MCP requests +- **GET**: Not implemented (returns 405) +- **DELETE**: End session +- **OPTIONS**: CORS preflight + +You should route **all methods** to your MCP endpoint, not just POST. + +### Session Management + +HTTP transport requires persistent sessions since PHP doesn't maintain state between requests. Unlike STDIO transport +where in-memory sessions work fine, HTTP transport needs a persistent session store: + +```php +use Mcp\Server\Session\FileSessionStore; + +// ✅ Good for HTTP +$server = Server::builder() + ->setSession(new FileSessionStore(__DIR__ . '/sessions')) + ->build(); + +// ❌ Not recommended for HTTP (sessions lost between requests) +$server = Server::builder() + ->setSession(new InMemorySessionStore()) + ->build(); +``` + +### Recommended Route + +It's recommended to mount the MCP endpoint at `/mcp`, but this is not enforced: + +```php +// Recommended +Route::any('/mcp', [McpController::class, 'handle']); + +// Also valid +Route::any('/', [McpController::class, 'handle']); +Route::any('/api/mcp', [McpController::class, 'handle']); +``` + +### Testing HTTP Transport + +Use the MCP Inspector to test HTTP servers: + +```bash +# Start your PHP server +php -S localhost:8000 server.php + +# Connect with MCP Inspector +npx @modelcontextprotocol/inspector http://localhost:8000 +``` + +## Choosing a Transport + +The choice between STDIO and HTTP transport depends on the client you want to integrate with. +If you are integrating with a client that is running **locally** (like Claude Desktop), use STDIO. +If you are building a server in a distributed environment and need to integrate with a **remote** client, use Streamable HTTP. + +One additiona difference to consider is that STDIO is process-based (one session per process) while HTTP is +request-based (multiple sessions via headers). diff --git a/examples/README.md b/examples/README.md index b621c22..96b8773 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,16 +2,15 @@ This directory contains various examples of how to use the PHP MCP SDK. -You can run examples 01-08 with the dependencies already installed in the root directory of the SDK. For example 09, see the -README in the `examples/09-standalone-cli` directory. +You can run the examples with the dependencies already installed in the root directory of the SDK. For running an example, you execute the `server.php` like this: ```bash # For examples using STDIO transport -php examples/01-discovery-stdio-calculator/server.php +php examples/stdio-discovery-calculator/server.php # For examples using Streamable HTTP transport -php -S localhost:8000 examples/02-discovery-http-userprofile/server.php +php -S localhost:8000 examples/http-discovery-userprofile/server.php ``` You will see debug outputs to help you understand what is happening. @@ -19,7 +18,7 @@ You will see debug outputs to help you understand what is happening. Run with Inspector: ```bash -npx @modelcontextprotocol/inspector php examples/01-discovery-stdio-calculator/server.php +npx @modelcontextprotocol/inspector php examples/stdio-discovery-calculator/server.php ``` ## Debugging @@ -30,5 +29,5 @@ directory. With the Inspector you can set the environment variables like this: ```bash -npx @modelcontextprotocol/inspector -e DEBUG=1 -e FILE_LOG=1 php examples/01-discovery-stdio-calculator/server.php +npx @modelcontextprotocol/inspector -e DEBUG=1 -e FILE_LOG=1 php examples/stdio-discovery-calculator/server.php ```