php-sdk/docs/mcp-elements.md
Tobias Nyholm a1e0f61b6b
Add RequestContext and logging (#176)
* Implemented MCP logging specification with auto-injection

* Add RequestContext

* fix tests and style

* Better deprecation

* fix: minor

* Remove ClientAwareInterface

* fix cs

* fix docs

---------

Co-authored-by: Adam Jamiu <jamiuadam120@gmail.com>
2025-12-16 00:12:02 +01:00

796 lines
23 KiB
Markdown

# 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)
- [Logging](#logging)
- [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.
- **`icons`** (optional): Array of `Icon` objects for visual representation.
- **`meta`** (optional): Arbitrary key-value pairs for custom 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
Tool handlers can throw any exception, but the type determines how it's handled:
- **`ToolCallException`**: Converted to JSON-RPC response with `CallToolResult` where `isError: true`, allowing the LLM to see the error message and self-correct
- **Any other exception**: Converted to JSON-RPC error response, but with a generic error message
```php
use Mcp\Exception\ToolCallException;
#[McpTool]
public function divideNumbers(float $a, float $b): float
{
if ($b === 0.0) {
throw new ToolCallException('Division by zero is not allowed');
}
return $a / $b;
}
#[McpTool]
public function processFile(string $filename): string
{
if (!file_exists($filename)) {
throw new ToolCallException("File not found: {$filename}");
}
return file_get_contents($filename);
}
```
**Recommendation**: Use `ToolCallException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
## 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.
- **`annotations`** (optional): Additional metadata.
- **`icons`** (optional): Array of `Icon` objects for visual representation.
- **`meta`** (optional): Arbitrary key-value pairs for custom metadata.
**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 any exception, but the type determines how it's handled:
- **`ResourceReadException`**: Converted to JSON-RPC error response with the actual exception message
- **Any other exception**: Converted to JSON-RPC error response, but with a generic error message
```php
use Mcp\Exception\ResourceReadException;
#[McpResource(uri: 'file://{path}')]
public function getFile(string $path): string
{
if (!file_exists($path)) {
throw new ResourceReadException("File not found: {$path}");
}
if (!is_readable($path)) {
throw new ResourceReadException("File not readable: {$path}");
}
return file_get_contents($path);
}
```
**Recommendation**: Use `ResourceReadException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
## 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.
- **`icons`** (optional): Array of `Icon` objects for visual representation.
- **`meta`** (optional): Arbitrary key-value pairs for custom metadata.
### 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')])
];
}
```
The SDK automatically validates that all messages have valid roles and converts the result into the appropriate MCP prompt message format.
#### Valid Message Roles
- **`user`**: User input or questions
- **`assistant`**: Assistant responses/system
#### Error Handling
Prompt handlers can throw any exception, but the type determines how it's handled:
- **`PromptGetException`**: Converted to JSON-RPC error response with the actual exception message
- **Any other exception**: Converted to JSON-RPC error response, but with a generic error message
```php
use Mcp\Exception\PromptGetException;
#[McpPrompt]
public function generatePrompt(string $topic, string $style): array
{
$validStyles = ['casual', 'formal', 'technical'];
if (!in_array($style, $validStyles)) {
throw new PromptGetException(
"Invalid style '{$style}'. Must be one of: " . implode(', ', $validStyles)
);
}
return [
['role' => 'user', 'content' => "Write about {$topic} in a {$style} style"]
];
}
```
**Recommendation**: Use `PromptGetException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
## Logging
The SDK provides support to send structured log messages to clients. All standard PSR-3 log levels are supported.
Level **warning** as the default level.
### Usage
The SDK automatically injects a `RequestContext` instance into handlers. This can be used to create a `ClientLogger`.
```php
use Mcp\Capability\Logger\ClientLogger;
use Mcp\Server\RequestContext;
#[McpTool]
public function processData(string $input, RequestContext $context): array {
$logger = $context->getClientLogger();
$logger->info('Processing started', ['input' => $input]);
$logger->warning('Deprecated API used');
// ... processing logic ...
$logger->info('Processing completed');
return ['result' => 'processed'];
}
```
## 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.