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
|
# 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]
|
> [!IMPORTANT]
|
||||||
> Currently, we are still in the process of merging [Symfony's MCP SDK](https://github.com/symfony/mcp-sdk) and
|
> This SDK is currently in active development with ongoing refinement of its architecture and features. While
|
||||||
> [PHP-MCP](https://github.com/php-mcp) components. Not all code paths are fully tested or complete, and this package
|
> functional, the API may experience changes as we work toward stabilization.
|
||||||
> may still contain duplicate functionality or dead code.
|
|
||||||
>
|
>
|
||||||
> If you want to help us stabilize the SDK, please see the
|
> If you want to help us stabilize the SDK, please see the [issue tracker](https://github.com/modelcontextprotocol/php-sdk/issues).
|
||||||
> [issue tracker](https://github.com/modelcontextprotocol/php-sdk/issues).
|
|
||||||
|
|
||||||
This project is a collaboration between [the PHP Foundation](https://thephp.foundation/) and the
|
This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts
|
||||||
[Symfony project](https://symfony.com/). It adopts development practices and standards from the Symfony project,
|
development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the
|
||||||
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).
|
[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html).
|
||||||
|
|
||||||
Until the first major release, this SDK is considered
|
Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html).
|
||||||
[experimental](https://symfony.com/doc/current/contributing/code/experimental.html).
|
|
||||||
|
|
||||||
## 🚧 Roadmap
|
## Roadmap
|
||||||
|
|
||||||
Features
|
**Features**
|
||||||
- [x] Bring back PHP-MCP examples
|
- [ ] Stabilize server component with all needed handlers and functional tests
|
||||||
- [x] Glue handler, registry and reference handlers
|
- [ ] Extend documentation, including integration guides for popular frameworks
|
||||||
- [x] Revive `ServerBuilder`
|
- [ ] Implement Client component
|
||||||
- [x] Revive transports
|
- [ ] Support multiple schema versions
|
||||||
- [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
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -38,19 +29,13 @@ Features
|
||||||
composer require mcp/sdk
|
composer require mcp/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
Since this package has no tagged releases yet, it is required to extend your `composer.json`:
|
## Quick Start
|
||||||
```json
|
|
||||||
"minimum-stability": "dev",
|
|
||||||
"prefer-stable": true
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚡ 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 a class with MCP capabilities using attributes:
|
||||||
|
|
||||||
Create `src/CalculatorElements.php`:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -58,20 +43,53 @@ Create `src/CalculatorElements.php`:
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Mcp\Capability\Attribute\McpTool;
|
use Mcp\Capability\Attribute\McpTool;
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
|
||||||
class CalculatorElements
|
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
|
public function add(int $a, int $b): int
|
||||||
{
|
{
|
||||||
return $a + $b;
|
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
|
```php
|
||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
|
|
@ -84,54 +102,166 @@ require_once __DIR__ . '/vendor/autoload.php';
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Transport\StdioTransport;
|
use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
Server::builder()
|
$server = Server::builder()
|
||||||
->setServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
|
->setServerInfo('Calculator Server', '1.0.0')
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new StdioTransport());
|
|
||||||
|
$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
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"php-calculator": {
|
"php-calculator": {
|
||||||
"command": "php",
|
"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:
|
```bash
|
||||||
- `add_numbers` - Add two integers
|
# 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
|
## 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 documentation](https://modelcontextprotocol.io)
|
||||||
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
|
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
|
||||||
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)
|
- [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
|
## Contributing
|
||||||
|
|
||||||
We are passionate about supporting contributors of all levels of experience and would love to see you get involved in
|
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
|
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).
|
||||||
[report issues](https://github.com/modelcontextprotocol/php-sdk/issues) and
|
|
||||||
[send pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls).
|
|
||||||
|
|
||||||
## Credits
|
## 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
|
## 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.
|
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
|
You can run the examples with the dependencies already installed in the root directory of the SDK.
|
||||||
README in the `examples/09-standalone-cli` directory.
|
|
||||||
|
|
||||||
For running an example, you execute the `server.php` like this:
|
For running an example, you execute the `server.php` like this:
|
||||||
```bash
|
```bash
|
||||||
# For examples using STDIO transport
|
# 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
|
# 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.
|
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:
|
Run with Inspector:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @modelcontextprotocol/inspector php examples/01-discovery-stdio-calculator/server.php
|
npx @modelcontextprotocol/inspector php examples/stdio-discovery-calculator/server.php
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
@ -30,5 +29,5 @@ directory.
|
||||||
|
|
||||||
With the Inspector you can set the environment variables like this:
|
With the Inspector you can set the environment variables like this:
|
||||||
```bash
|
```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