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:
parent
cbd1b298e9
commit
e94fd82061
7 changed files with 2070 additions and 166 deletions
238
README.md
238
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
|
||||
|
||||
## ⚡ 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.
|
||||
|
|
|
|||
|
|
@ -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
272
docs/examples.md
Normal 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
744
docs/mcp-elements.md
Normal 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
515
docs/server-builder.md
Normal 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
350
docs/transports.md
Normal 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).
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in a new issue