Skip to content
55 changes: 36 additions & 19 deletions docs/mcp-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,31 +154,37 @@ public function getMultipleContent(): array

#### Error Handling

Tools can throw exceptions which are automatically converted to proper JSON-RPC error responses:
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 \InvalidArgumentException('Division by zero is not allowed');
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 \InvalidArgumentException("File not found: {$filename}");
throw new ToolCallException("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.
**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

Expand Down Expand Up @@ -298,24 +304,31 @@ public function getMultipleResources(): array

#### Error Handling

Resource handlers can throw exceptions for error cases:
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 \InvalidArgumentException("File not found: {$path}");
throw new ResourceReadException("File not found: {$path}");
}

if (!is_readable($path)) {
throw new \RuntimeException("File not 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
Expand Down Expand Up @@ -449,40 +462,44 @@ public function explicitMessages(): array
}
```

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 exceptions for invalid inputs:
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 \InvalidArgumentException(
throw new PromptGetException(
"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.
**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.

## 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 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

Expand Down
3 changes: 2 additions & 1 deletion examples/http-client-communication/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Exception\ToolCallException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
Expand Down Expand Up @@ -64,7 +65,7 @@ function (string $projectName, array $milestones, ClientGateway $client): array
);

if ($response instanceof JsonRpcError) {
throw new RuntimeException(sprintf('Sampling request failed (%d): %s', $response->code, $response->message));
throw new ToolCallException(sprintf('Sampling request failed (%d): %s', $response->code, $response->message));
}

$result = $response->result;
Expand Down
11 changes: 6 additions & 5 deletions examples/http-discovery-userprofile/McpElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpResourceTemplate;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\PromptGetException;
use Mcp\Exception\ResourceReadException;
use Psr\Log\LoggerInterface;

/**
Expand Down Expand Up @@ -48,7 +49,7 @@ public function __construct(
*
* @return User user profile data
*
* @throws InvalidArgumentException if the user is not found
* @throws ResourceReadException if the user is not found
*/
#[McpResourceTemplate(
uriTemplate: 'user://{userId}/profile',
Expand All @@ -62,7 +63,7 @@ public function getUserProfile(
): array {
$this->logger->info('Reading resource: user profile', ['userId' => $userId]);
if (!isset($this->users[$userId])) {
throw new InvalidArgumentException("User profile not found for ID: {$userId}");
throw new ResourceReadException("User not found for ID: {$userId}");
}

return $this->users[$userId];
Expand Down Expand Up @@ -130,7 +131,7 @@ public function testToolWithoutParams(): array
*
* @return array<string, string>[] prompt messages
*
* @throws InvalidArgumentException if user not found
* @throws PromptGetException if user not found
*/
#[McpPrompt(name: 'generate_bio_prompt')]
public function generateBio(
Expand All @@ -140,7 +141,7 @@ public function generateBio(
): array {
$this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
if (!isset($this->users[$userId])) {
throw new InvalidArgumentException("User not found for bio prompt: {$userId}");
throw new PromptGetException("User not found for bio prompt: {$userId}");
}
$user = $this->users[$userId];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Mcp\Example\StdioCachedDiscovery;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\ToolCallException;

/**
* Example MCP elements for demonstrating cached discovery.
Expand All @@ -39,7 +40,7 @@ public function multiply(int $a, int $b): int
public function divide(int $a, int $b): float
{
if (0 === $b) {
throw new \InvalidArgumentException('Division by zero is not allowed');
throw new ToolCallException('Division by zero is not allowed');
}

return $a / $b;
Expand Down
11 changes: 6 additions & 5 deletions examples/stdio-discovery-calculator/McpElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\ToolCallException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

Expand Down Expand Up @@ -44,10 +45,10 @@ public function __construct(
* @param float $b the second operand
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
*
* @return float|string the result of the calculation, or an error message string
* @return float the result of the calculation
*/
#[McpTool(name: 'calculate')]
public function calculate(float $a, float $b, string $operation): float|string
public function calculate(float $a, float $b, string $operation): float
{
$this->logger->info(\sprintf('Calculating: %f %s %f', $a, $operation, $b));

Expand All @@ -65,16 +66,16 @@ public function calculate(float $a, float $b, string $operation): float|string
break;
case 'divide':
if (0 == $b) {
return 'Error: Division by zero.';
throw new ToolCallException('Division by zero is not allowed.');
}
$result = $a / $b;
break;
default:
return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
throw new ToolCallException("Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.");
}

if (!$this->config['allow_negative'] && $result < 0) {
return 'Error: Negative results are disabled.';
throw new ToolCallException('Negative results are disabled.');
}

return round($result, $this->config['precision']);
Expand Down
31 changes: 16 additions & 15 deletions src/Capability/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
use Mcp\Event\ResourceTemplateListChangedEvent;
use Mcp\Event\ToolListChangedEvent;
use Mcp\Exception\InvalidCursorException;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
Expand Down Expand Up @@ -209,43 +212,41 @@ public function clear(): void
}
}

public function getTool(string $name): ?ToolReference
public function getTool(string $name): ToolReference
{
return $this->tools[$name] ?? null;
return $this->tools[$name] ?? throw new ToolNotFoundException($name);
}

public function getResource(
string $uri,
bool $includeTemplates = true,
): ResourceReference|ResourceTemplateReference|null {
): ResourceReference|ResourceTemplateReference {
$registration = $this->resources[$uri] ?? null;
if ($registration) {
return $registration;
}

if (!$includeTemplates) {
return null;
}

foreach ($this->resourceTemplates as $template) {
if ($template->matches($uri)) {
return $template;
if ($includeTemplates) {
foreach ($this->resourceTemplates as $template) {
if ($template->matches($uri)) {
return $template;
}
}
}

$this->logger->debug('No resource matched URI.', ['uri' => $uri]);

return null;
throw new ResourceNotFoundException($uri);
}

public function getResourceTemplate(string $uriTemplate): ?ResourceTemplateReference
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
{
return $this->resourceTemplates[$uriTemplate] ?? null;
return $this->resourceTemplates[$uriTemplate] ?? throw new ResourceNotFoundException($uriTemplate);
}

public function getPrompt(string $name): ?PromptReference
public function getPrompt(string $name): PromptReference
{
return $this->prompts[$name] ?? null;
return $this->prompts[$name] ?? throw new PromptNotFoundException($name);
}

public function getTools(?int $limit = null, ?string $cursor = null): Page
Expand Down
19 changes: 15 additions & 4 deletions src/Capability/Registry/ReferenceProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace Mcp\Capability\Registry;

use Mcp\Exception\PromptNotFoundException;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page;

/**
Expand All @@ -23,23 +26,31 @@ interface ReferenceProviderInterface
{
/**
* Gets a tool reference by name.
*
* @throws ToolNotFoundException
*/
public function getTool(string $name): ?ToolReference;
public function getTool(string $name): ToolReference;

/**
* Gets a resource reference by URI (includes template matching if enabled).
*
* @throws ResourceNotFoundException
*/
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference|null;
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference;

/**
* Gets a resource template reference by URI template.
*
* @throws ResourceNotFoundException
*/
public function getResourceTemplate(string $uriTemplate): ?ResourceTemplateReference;
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference;

/**
* Gets a prompt reference by name.
*
* @throws PromptNotFoundException
*/
public function getPrompt(string $name): ?PromptReference;
public function getPrompt(string $name): PromptReference;

/**
* Gets all registered tools.
Expand Down
8 changes: 0 additions & 8 deletions src/Exception/PromptGetException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,9 @@

namespace Mcp\Exception;

use Mcp\Schema\Request\GetPromptRequest;

/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PromptGetException extends \RuntimeException implements ExceptionInterface
{
public function __construct(
public readonly GetPromptRequest $request,
?\Throwable $previous = null,
) {
parent::__construct(\sprintf('Handling prompt "%s" failed with error: "%s".', $request->name, $previous->getMessage()), previous: $previous);
}
}
Loading