* [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>
799 lines
23 KiB
Markdown
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.
|