docs: add more about building servers and updating front readme (#81)

* docs: add more about building servers and updating front readme

* Overhaul documentation structure and content

* fix: remove redundant emoji on docs

* Remove composer install --dev hint

* Add linebreak to make it readable in IDE

* rebase after example renaming

---------

Co-authored-by: Kyrian Obikwelu <koshnawaza@gmail.com>
This commit is contained in:
Christopher Hertel 2025-10-07 23:42:02 +02:00 • committed by GitHub
commit e94fd82061
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 2070 additions and 166 deletions

238
README.md
View file

@ -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
## ⚡ Quick Start: Stdio Server with Discovery
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
<?php
@ -58,20 +43,53 @@ Create `src/CalculatorElements.php`:
namespace App;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\McpResource;
class CalculatorElements
{
#[McpTool(name: 'add_numbers')]
/**
* Adds two numbers together.
*
* @param int $a The first number
* @param int $b The second number
* @return int The sum of the two numbers
*/
#[McpTool]
public function add(int $a, int $b): int
{
return $a + $b;
}
/**
* Performs basic arithmetic operations.
*/
#[McpTool(name: 'calculate')]
public function calculate(float $a, float $b, string $operation): float|string
{
return match($operation) {
'add' => $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
Your AI assistant can now call:
- `add_numbers` - Add two integers
```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
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.

View file

@ -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

272
docs/examples.md Normal file
View file

@ -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
```

744
docs/mcp-elements.md Normal file
View file

@ -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('<?php echo "Hello";', 'php');
}
public function getMarkdown(): TextContent
{
return new TextContent('# Title\n\nMarkdown content');
}
public function getImage(): ImageContent
{
return new ImageContent(
data: base64_encode(file_get_contents('image.png')),
mimeType: 'image/png'
);
}
public function getAudio(): AudioContent
{
return new AudioContent(
data: base64_encode(file_get_contents('audio.mp3')),
mimeType: 'audio/mpeg'
);
}
public function getEmbeddedResource(): EmbeddedResource
{
return new EmbeddedResource(
type: 'resource',
resource: ['uri' => '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.

515
docs/server-builder.md Normal file
View file

@ -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 |

350
docs/transports.md Normal file
View file

@ -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
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
$server = Server::builder()
->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).

View file

@ -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
```