php-sdk/docs/mcp-elements.md
Mateu Aguiló Bosch 0347dc85b4
[Server] Allow runtime-resolved element handlers (#294)
* [Server] feat: allow runtime handlers

Adds Mcp\Server\Handler\RunTimeHandlerInterface so integrators
(e.g. configuration-driven element sources) can register handlers
whose arguments and execution are resolved at runtime rather than
via PHP reflection of a callable.

ReferenceHandler::handle() dispatches instanceof RunTimeHandler
ahead of the callable/array/string branches, calls filterArguments
to narrow the argument map, and forwards execute() a ClientGateway
built from the active session.

The Registry-layer surface (ElementReference and the four
*Reference subclasses, RegistryInterface, Registry::register*,
ArrayLoader's handler-description helper) is widened to accept the
new type. The Builder/ArrayLoader path keeps its narrow callable
union; exposing runtime handlers there requires reworking
HandlerResolver and is left for a follow-up.

* feat(server): add runtime handler metadata API

Extend RunTimeHandlerInterface with four nullable metadata
accessors (getInputSchema, getOutputSchema, getPromptArguments,
getCompletionProviders) so the loader can obtain element
metadata without reflection.

Add RunTimeHandlerTrait providing null defaults for all four
accessors so single-purpose handlers can override only the
methods relevant to their element kind.

* feat(server): expose runtime handlers via Builder

Widen Builder::addTool, addResource, addResourceTemplate, and
addPrompt to accept RunTimeHandlerInterface instances alongside
the existing closure/array/string handler forms.

Branch ArrayLoader::load() on instanceof RunTimeHandlerInterface
in each of the four registration loops so runtime handlers skip
the reflection-based HandlerResolver/HandlerSchema path and pull
metadata from the interface accessors instead. Require explicit
name and description for runtime handlers and raise
ConfigurationException when missing or when a runtime tool has
no input schema source.

Replace direct $data['handler'] use in error log context with
getHandlerDescription() to avoid serializing large runtime
handler object graphs into log records.

Document the runtime handler API in docs/server-builder.md with a
new Runtime Handlers subsection and add a cross-reference from
docs/mcp-elements.md.

* test(server): cover runtime handler Builder path

Add ArrayLoaderRunTimeHandlerTest covering:
- successful tool registration with handler-supplied input schema,
  with kwarg input schema (precedence over the handler), and the
  same precedence rule for output schema
- ConfigurationException for tools missing name, description, or
  any input schema source
- successful resource registration via Builder kwargs and the
  matching missing-name failure mode
- successful resource template registration with completion
  providers sourced from the handler and the missing-description
  failure mode
- successful prompt registration with arguments and completion
  providers sourced from the handler, plus the missing-name
  failure mode
- a trait-only fixture verifying the four metadata accessors
  return null by default

Update existing ReferenceHandlerTest anonymous classes to use
RunTimeHandlerTrait so they satisfy the widened interface.

* refactor(server): split runtime handler interface

Splits RunTimeHandlerInterface into a minimal base contract (only
execute()) plus element-specific subtypes: RunTimeToolHandlerInterface,
RunTimePromptHandlerInterface, and
RunTimeResourceTemplateHandlerInterface. Each subtype declares only
the metadata accessors relevant to its element kind, so a tool
handler no longer has to no-op out prompt or template methods.

Drops the now-redundant RunTimeHandlerTrait and removes
filterArguments; execute() receives the full argument map directly.

Updates ArrayLoader, ReferenceHandler, and Builder type signatures
to use the per-element interfaces, and rewrites the Runtime
Handlers documentation accordingly.

* refactor(server): rename runtime handler interfaces to Runtime casing

Renames RunTimeHandlerInterface and its element-specific subtypes to
Runtime casing, adds a dedicated RuntimeResourceHandlerInterface so
addResource() rejects mismatched runtime handlers at the type level,
and strips _session and _request from arguments forwarded to runtime
handlers. Hardens ArrayLoader to re-throw ConfigurationException
without wrapping, and extracts inline test fixtures into
tests/Fixtures/Runtime/.

* chore: please the linting gods

* test(server): use Uuid::v4 for symfony/uid 5.4 compatibility

Uuid::v7() was added in symfony/uid 6.2 and breaks the Symfony 5.4
matrix job. Match the convention used elsewhere in the suite.

* refactor(server): split ArrayLoader registration

Extract per-element preparation into dedicated private prepare*
methods for tools, resources, templates, and prompts. Factor the
shared required-field assertion and reflected name/description
resolution into helpers.

* refactor(server): non-null runtime handler types

Drop nullable return types on RuntimeToolHandlerInterface::
getInputSchema(), RuntimePromptHandlerInterface::
getPromptArguments()/getCompletionProviders(), and
RuntimeResourceTemplateHandlerInterface::
getCompletionProviders() in favor of plain arrays.
ArrayLoader no longer needs the null-coalescing
fallbacks, and NullSchemaToolHandler plus its
ConfigurationException test become redundant.

* doc: update docs/server-builder.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* feat: peer revie feedback

Introduce a new `Builder::add(...)` pattern for a more generic runtime
handler.

* refactor(server): apply PR 294 review feedback

Move per-interface handler dispatch from ReferenceHandler into
ExplicitElementLoader by wrapping each ElementHandlerInterface in a
Closure that satisfies the existing callable contract, revert the
*Reference / Registry handler-type widening PR 294 introduced, throw
Mcp\Exception\InvalidArgumentException (not \TypeError) on mismatched
Builder::add() pairings, and rename two classes for clarity:

* Mcp\Capability\Registry\Loader\ArrayLoader -> ReflectedElementLoader
* Mcp\Schema\Resource -> Mcp\Schema\ResourceDefinition (no alias)

ReferenceHandler::prepareArguments() now injects ClientGateway by type
and binds an `array $arguments` / `array $variables` closure parameter
from the leftover inbound arguments. This addresses Antoine's coupling
concern (ReferenceHandler no longer knows about the four handler
interfaces) and Chris's package-exception preference, and clears the
phpdoc reserved-word collision Antoine flagged on the resource schema
class. Existing tests are re-targeted to exercise the closure path
through ExplicitElementLoader / ReferenceHandler.

* doc: address PR 294 review feedback

Apply Chris's line-level suggestions on docs/server-builder.md
(rewrap to 120 chars, smoothed Drupal mcp module name-drop in the
explicit-element registration section, exception name reads
Mcp\Exception\InvalidArgumentException), rewrap docs/mcp-elements.md
to 120 chars, and add 0.6.0 CHANGELOG entries covering the
Builder::add() entry point, the new handler interfaces, the new
ExplicitElementLoader, plus three BC breaks (Resource ->
ResourceDefinition rename, ArrayLoader -> ReflectedElementLoader
rename, mismatched-pairing exception swap).

* refactor: address self-review feedback

* CHANGELOG: drop spurious [BC Break] entry for the exception
  type Builder::add() raises (method is new in this PR).
* docs/server-builder: revert wrap-only edits to pre-existing
  main content; keep 120ch wrapping only for new sections.
* ExplicitElementLoader: trim verbose class docblock.
* ReferenceHandler: rename SPLAT_PARAMETER_NAMES constant to
  ARGUMENT_BAG_PARAMETERS.

* refactor: rename CallableHandler to Handler

* style: fix phpdoc param alignment

* refactor: inline $reference->handler access

* fix: maintainer review feedback

docs: mention ExplicitElementLoader in comment

docs: move Drupal reference to README list

docs: drop implementation-only changelog entry

docs: revert rewrap noise from mcp-elements.md

* style: fix phpdoc param alignment

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
2026-05-29 07:16:12 +02:00

799 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).
For runtime, config-driven elements whose shape is not known at compile time, see
[Explicit element registration](server-builder.md#explicit-element-registration) in the Server Builder docs.
## 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.