[Server] Fix: standardize error handling across handlers with MCP specificatio… (#124)

* fix: standardize error handling across handlers with MCP specification compliance

* Update mcp-elements.md

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

* docs: clarify error handling behavior for all element types

* docs: remove unnecessary whitespace in mcp-elements.md

* fix: simplify exception signatures to accept simple messages

* refactor: use specific exceptions in examples for better error handling

* feat: throw exceptions from reference provider instead of returning null

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This commit is contained in:
Kyrian Obikwelu 2025-10-30 17:30:58 +01:00 • committed by GitHub
commit ca18caf29d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 278 additions and 185 deletions

View file

@ -23,6 +23,9 @@ use Mcp\Event\ResourceListChangedEvent;
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;
@ -209,43 +212,41 @@ final class Registry implements ReferenceProviderInterface, ReferenceRegistryInt
}
}
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

View file

@ -11,6 +11,9 @@
namespace Mcp\Capability\Registry;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page;
/**
@ -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.

View file

@ -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);
}
}

View file

@ -11,16 +11,14 @@
namespace Mcp\Exception;
use Mcp\Schema\Request\GetPromptRequest;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
{
public function __construct(
public readonly GetPromptRequest $request,
public readonly string $name,
) {
parent::__construct(\sprintf('Prompt not found for name: "%s".', $request->name));
parent::__construct(\sprintf('Prompt not found: "%s".', $name));
}
}

View file

@ -11,16 +11,14 @@
namespace Mcp\Exception;
use Mcp\Schema\Request\ReadResourceRequest;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
{
public function __construct(
public readonly ReadResourceRequest $request,
public readonly string $uri,
) {
parent::__construct(\sprintf('Resource not found for uri: "%s".', $request->uri));
parent::__construct(\sprintf('Resource not found for uri: "%s".', $uri));
}
}

View file

@ -11,17 +11,9 @@
namespace Mcp\Exception;
use Mcp\Schema\Request\ReadResourceRequest;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ResourceReadException extends \RuntimeException implements ExceptionInterface
{
public function __construct(
public readonly ReadResourceRequest $request,
?\Throwable $previous = null,
) {
parent::__construct(\sprintf('Reading resource "%s" failed with error: "%s".', $request->uri, $previous?->getMessage() ?? ''), previous: $previous);
}
}

View file

@ -11,17 +11,9 @@
namespace Mcp\Exception;
use Mcp\Schema\Request\CallToolRequest;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ToolCallException extends \RuntimeException implements ExceptionInterface
{
public function __construct(
public readonly CallToolRequest $request,
?\Throwable $previous = null,
) {
parent::__construct(\sprintf('Tool call "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous);
}
}

View file

@ -11,16 +11,14 @@
namespace Mcp\Exception;
use Mcp\Schema\Request\CallToolRequest;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
{
public function __construct(
public readonly CallToolRequest $request,
public readonly string $name,
) {
parent::__construct(\sprintf('Tool not found for call: "%s".', $request->name));
parent::__construct(\sprintf('Tool not found: "%s".', $name));
}
}

View file

@ -106,6 +106,11 @@ class Error implements MessageInterface
return new self($id, self::SERVER_ERROR, $message);
}
public static function forResourceNotFound(string $message, string|int $id = ''): self
{
return new self($id, self::RESOURCE_NOT_FOUND, $message);
}
public function getId(): string|int
{
return $this->id;

View file

@ -13,9 +13,9 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
@ -59,9 +59,6 @@ final class CallToolHandler implements RequestHandlerInterface
try {
$reference = $this->referenceProvider->getTool($toolName);
if (null === $reference) {
throw new ToolNotFoundException($request);
}
$arguments['_session'] = $session;
@ -77,17 +74,19 @@ final class CallToolHandler implements RequestHandlerInterface
]);
return new Response($request->getId(), $result);
} catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]);
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (ToolCallException|ExceptionInterface $e) {
} catch (ToolCallException $e) {
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
'tool' => $toolName,
'arguments' => $arguments,
]);
return Error::forInternalError('Error while executing tool', $request->getId());
$errorContent = [new TextContent($e->getMessage())];
return new Response($request->getId(), CallToolResult::error($errorContent));
} catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]);
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (\Throwable $e) {
$this->logger->error('Unhandled error during tool execution', [
'name' => $toolName,

View file

@ -13,10 +13,14 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\PromptReference;
use Mcp\Schema\Request\CompletionCompleteRequest;
use Mcp\Schema\ResourceReference;
use Mcp\Schema\Result\CompletionCompleteResult;
use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface;
@ -51,41 +55,38 @@ final class CompletionCompleteHandler implements RequestHandlerInterface
$name = $request->argument['name'] ?? '';
$value = $request->argument['value'] ?? '';
$reference = match ($request->ref->type) {
'ref/prompt' => $this->referenceProvider->getPrompt($request->ref->name),
'ref/resource' => $this->referenceProvider->getResourceTemplate($request->ref->uri),
default => null,
};
if (null === $reference) {
return new Response($request->getId(), new CompletionCompleteResult([]));
}
$providers = $reference->completionProviders;
$provider = $providers[$name] ?? null;
if (null === $provider) {
return new Response($request->getId(), new CompletionCompleteResult([]));
}
if (\is_string($provider)) {
if (!class_exists($provider)) {
return Error::forInternalError('Invalid completion provider', $request->getId());
}
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
}
if (!$provider instanceof ProviderInterface) {
return Error::forInternalError('Invalid completion provider type', $request->getId());
}
try {
$reference = match (true) {
$request->ref instanceof PromptReference => $this->referenceProvider->getPrompt($request->ref->name),
$request->ref instanceof ResourceReference => $this->referenceProvider->getResource($request->ref->uri),
};
$providers = $reference->completionProviders;
$provider = $providers[$name] ?? null;
if (null === $provider) {
return new Response($request->getId(), new CompletionCompleteResult([]));
}
if (\is_string($provider)) {
if (!class_exists($provider)) {
return Error::forInternalError('Invalid completion provider', $request->getId());
}
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
}
if (!$provider instanceof ProviderInterface) {
return Error::forInternalError('Invalid completion provider type', $request->getId());
}
$completions = $provider->getCompletions($value);
$total = \count($completions);
$hasMore = $total > 100;
$paged = \array_slice($completions, 0, 100);
return new Response($request->getId(), new CompletionCompleteResult($paged, $total, $hasMore));
} catch (\Throwable) {
} catch (PromptNotFoundException|ResourceNotFoundException $e) {
return Error::forResourceNotFound($e->getMessage(), $request->getId());
} catch (\Throwable $e) {
return Error::forInternalError('Error while handling completion request', $request->getId());
}
}

View file

@ -13,7 +13,6 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\PromptGetException;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Schema\JsonRpc\Error;
@ -56,9 +55,6 @@ final class GetPromptHandler implements RequestHandlerInterface
try {
$reference = $this->referenceProvider->getPrompt($promptName);
if (null === $reference) {
throw new PromptNotFoundException($request);
}
$arguments['_session'] = $session;
@ -67,18 +63,18 @@ final class GetPromptHandler implements RequestHandlerInterface
$formatted = $reference->formatResult($result);
return new Response($request->getId(), new GetPromptResult($formatted));
} catch (PromptGetException $e) {
$this->logger->error(\sprintf('Error while handling prompt "%s": "%s".', $promptName, $e->getMessage()));
return Error::forInternalError($e->getMessage(), $request->getId());
} catch (PromptNotFoundException $e) {
$this->logger->error('Prompt not found', ['prompt_name' => $promptName]);
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (PromptGetException|ExceptionInterface $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt: '.$e->getMessage(), $request->getId());
return Error::forResourceNotFound($e->getMessage(), $request->getId());
} catch (\Throwable $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
$this->logger->error(\sprintf('Unexpected error while handling prompt "%s": "%s".', $promptName, $e->getMessage()));
return Error::forInternalError('Error while handling prompt: '.$e->getMessage(), $request->getId());
return Error::forInternalError('Error while handling prompt', $request->getId());
}
}
}

View file

@ -15,6 +15,7 @@ use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ResourceTemplateReference;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ResourceReadException;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
@ -56,9 +57,6 @@ final class ReadResourceHandler implements RequestHandlerInterface
try {
$reference = $this->referenceProvider->getResource($uri);
if (null === $reference) {
throw new ResourceNotFoundException($request);
}
$arguments = [
'uri' => $uri,
@ -77,12 +75,16 @@ final class ReadResourceHandler implements RequestHandlerInterface
}
return new Response($request->getId(), new ReadResourceResult($formatted));
} catch (ResourceReadException $e) {
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()));
return Error::forInternalError($e->getMessage(), $request->getId());
} catch (ResourceNotFoundException $e) {
$this->logger->error('Resource not found', ['uri' => $uri]);
return new Error($request->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
return Error::forResourceNotFound($e->getMessage(), $request->getId());
} catch (\Throwable $e) {
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()));
$this->logger->error(\sprintf('Unexpected error while reading resource "%s": "%s".', $uri, $e->getMessage()));
return Error::forInternalError('Error while reading resource', $request->getId());
}