Kick off the SDK based on Symfony's MCP SDK
This commit is contained in:
parent
0044ffb365
commit
ee0dffe369
19 changed files with 3924 additions and 0 deletions
163
src/Server/JsonRpcHandler.php
Normal file
163
src/Server/JsonRpcHandler.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server;
|
||||
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Exception\HandlerNotFoundException;
|
||||
use Mcp\Exception\InvalidInputMessageException;
|
||||
use Mcp\Exception\NotFoundExceptionInterface;
|
||||
use Mcp\Message\Error;
|
||||
use Mcp\Message\Factory;
|
||||
use Mcp\Message\Notification;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
class JsonRpcHandler
|
||||
{
|
||||
/**
|
||||
* @var array<int, RequestHandlerInterface>
|
||||
*/
|
||||
private readonly array $requestHandlers;
|
||||
|
||||
/**
|
||||
* @var array<int, NotificationHandlerInterface>
|
||||
*/
|
||||
private readonly array $notificationHandlers;
|
||||
|
||||
/**
|
||||
* @param iterable<RequestHandlerInterface> $requestHandlers
|
||||
* @param iterable<NotificationHandlerInterface> $notificationHandlers
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Factory $messageFactory,
|
||||
iterable $requestHandlers,
|
||||
iterable $notificationHandlers,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
$this->requestHandlers = $requestHandlers instanceof \Traversable ? iterator_to_array($requestHandlers) : $requestHandlers;
|
||||
$this->notificationHandlers = $notificationHandlers instanceof \Traversable ? iterator_to_array($notificationHandlers) : $notificationHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string|null>
|
||||
*
|
||||
* @throws ExceptionInterface When a handler throws an exception during message processing
|
||||
* @throws \JsonException When JSON encoding of the response fails
|
||||
*/
|
||||
public function process(string $input): iterable
|
||||
{
|
||||
$this->logger->info('Received message to process', ['message' => $input]);
|
||||
|
||||
try {
|
||||
$messages = $this->messageFactory->create($input);
|
||||
} catch (\JsonException $e) {
|
||||
$this->logger->warning('Failed to decode json message', ['exception' => $e]);
|
||||
|
||||
yield $this->encodeResponse(Error::parseError($e->getMessage()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
if ($message instanceof InvalidInputMessageException) {
|
||||
$this->logger->warning('Failed to create message', ['exception' => $message]);
|
||||
yield $this->encodeResponse(Error::invalidRequest(0, $message->getMessage()));
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger->info('Decoded incoming message', ['message' => $message]);
|
||||
|
||||
try {
|
||||
yield $message instanceof Notification
|
||||
? $this->handleNotification($message)
|
||||
: $this->encodeResponse($this->handleRequest($message));
|
||||
} catch (\DomainException) {
|
||||
yield null;
|
||||
} catch (NotFoundExceptionInterface $e) {
|
||||
$this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e]);
|
||||
|
||||
yield $this->encodeResponse(Error::methodNotFound($message->id, $e->getMessage()));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
|
||||
|
||||
yield $this->encodeResponse(Error::invalidParams($message->id, $e->getMessage()));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
|
||||
|
||||
yield $this->encodeResponse(Error::internalError($message->id, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException When JSON encoding fails
|
||||
*/
|
||||
private function encodeResponse(Response|Error|null $response): ?string
|
||||
{
|
||||
if (null === $response) {
|
||||
$this->logger->warning('Response is null');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->info('Encoding response', ['response' => $response]);
|
||||
|
||||
if ($response instanceof Response && [] === $response->result) {
|
||||
return json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT);
|
||||
}
|
||||
|
||||
return json_encode($response, \JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
*
|
||||
* @throws ExceptionInterface When a notification handler throws an exception
|
||||
*/
|
||||
private function handleNotification(Notification $notification)
|
||||
{
|
||||
$handled = false;
|
||||
foreach ($this->notificationHandlers as $handler) {
|
||||
if ($handler->supports($notification)) {
|
||||
$handler->handle($notification);
|
||||
$handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$handled) {
|
||||
$this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFoundExceptionInterface When no handler is found for the request method
|
||||
* @throws ExceptionInterface When a request handler throws an exception
|
||||
*/
|
||||
private function handleRequest(Request $request): Response|Error
|
||||
{
|
||||
foreach ($this->requestHandlers as $handler) {
|
||||
if ($handler->supports($request)) {
|
||||
return $handler->createResponse($request);
|
||||
}
|
||||
}
|
||||
|
||||
throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $request->method));
|
||||
}
|
||||
}
|
||||
28
src/Server/NotificationHandler/BaseNotificationHandler.php
Normal file
28
src/Server/NotificationHandler/BaseNotificationHandler.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\NotificationHandler;
|
||||
|
||||
use Mcp\Message\Notification;
|
||||
use Mcp\Server\NotificationHandlerInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
abstract class BaseNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
public function supports(Notification $message): bool
|
||||
{
|
||||
return $message->method === \sprintf('notifications/%s', $this->supportedNotification());
|
||||
}
|
||||
|
||||
abstract protected function supportedNotification(): string;
|
||||
}
|
||||
29
src/Server/NotificationHandler/InitializedHandler.php
Normal file
29
src/Server/NotificationHandler/InitializedHandler.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\NotificationHandler;
|
||||
|
||||
use Mcp\Message\Notification;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class InitializedHandler extends BaseNotificationHandler
|
||||
{
|
||||
protected function supportedNotification(): string
|
||||
{
|
||||
return 'initialized';
|
||||
}
|
||||
|
||||
public function handle(Notification $notification): void
|
||||
{
|
||||
}
|
||||
}
|
||||
28
src/Server/NotificationHandlerInterface.php
Normal file
28
src/Server/NotificationHandlerInterface.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server;
|
||||
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Message\Notification;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface NotificationHandlerInterface
|
||||
{
|
||||
public function supports(Notification $message): bool;
|
||||
|
||||
/**
|
||||
* @throws ExceptionInterface When the handler encounters an error processing the notification
|
||||
*/
|
||||
public function handle(Notification $notification): void;
|
||||
}
|
||||
28
src/Server/RequestHandler/BaseRequestHandler.php
Normal file
28
src/Server/RequestHandler/BaseRequestHandler.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
abstract class BaseRequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function supports(Request $message): bool
|
||||
{
|
||||
return $message->method === $this->supportedMethod();
|
||||
}
|
||||
|
||||
abstract protected function supportedMethod(): string;
|
||||
}
|
||||
45
src/Server/RequestHandler/InitializeHandler.php
Normal file
45
src/Server/RequestHandler/InitializeHandler.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class InitializeHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name = 'app',
|
||||
private readonly string $version = 'dev',
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response
|
||||
{
|
||||
return new Response($message->id, [
|
||||
'protocolVersion' => '2025-03-26',
|
||||
'capabilities' => [
|
||||
'prompts' => ['listChanged' => false],
|
||||
'tools' => ['listChanged' => false],
|
||||
'resources' => ['listChanged' => false, 'subscribe' => false],
|
||||
],
|
||||
'serverInfo' => ['name' => $this->name, 'version' => $this->version],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'initialize';
|
||||
}
|
||||
}
|
||||
31
src/Server/RequestHandler/PingHandler.php
Normal file
31
src/Server/RequestHandler/PingHandler.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class PingHandler extends BaseRequestHandler
|
||||
{
|
||||
public function createResponse(Request $message): Response
|
||||
{
|
||||
return new Response($message->id, []);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'ping';
|
||||
}
|
||||
}
|
||||
83
src/Server/RequestHandler/PromptGetHandler.php
Normal file
83
src/Server/RequestHandler/PromptGetHandler.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Prompt\PromptGet;
|
||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Message\Error;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class PromptGetHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PromptGetterInterface $getter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response|Error
|
||||
{
|
||||
$name = $message->params['name'];
|
||||
$arguments = $message->params['arguments'] ?? [];
|
||||
|
||||
try {
|
||||
$result = $this->getter->get(new PromptGet(uniqid('', true), $name, $arguments));
|
||||
} catch (ExceptionInterface) {
|
||||
return Error::internalError($message->id, 'Error while handling prompt');
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
foreach ($result->messages as $resultMessage) {
|
||||
$content = match ($resultMessage->type) {
|
||||
'text' => [
|
||||
'type' => 'text',
|
||||
'text' => $resultMessage->result,
|
||||
],
|
||||
'image', 'audio' => [
|
||||
'type' => $resultMessage->type,
|
||||
'data' => $resultMessage->result,
|
||||
'mimeType' => $resultMessage->mimeType,
|
||||
],
|
||||
'resource' => [
|
||||
'type' => 'resource',
|
||||
'resource' => [
|
||||
'uri' => $resultMessage->uri,
|
||||
'mimeType' => $resultMessage->mimeType,
|
||||
'text' => $resultMessage->result,
|
||||
],
|
||||
],
|
||||
// TODO better exception
|
||||
default => throw new InvalidArgumentException('Unsupported PromptGet result type: '.$resultMessage->type),
|
||||
};
|
||||
|
||||
$messages[] = [
|
||||
'role' => $resultMessage->role,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
|
||||
return new Response($message->id, [
|
||||
'description' => $result->description,
|
||||
'messages' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'prompts/get';
|
||||
}
|
||||
}
|
||||
85
src/Server/RequestHandler/PromptListHandler.php
Normal file
85
src/Server/RequestHandler/PromptListHandler.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Prompt\CollectionInterface;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class PromptListHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response
|
||||
{
|
||||
$nextCursor = null;
|
||||
$prompts = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata(
|
||||
$this->pageSize,
|
||||
$message->params['cursor'] ?? null
|
||||
);
|
||||
|
||||
foreach ($metadataList as $metadata) {
|
||||
$nextCursor = $metadata->getName();
|
||||
$result = [
|
||||
'name' => $metadata->getName(),
|
||||
];
|
||||
|
||||
$description = $metadata->getDescription();
|
||||
if (null !== $description) {
|
||||
$result['description'] = $description;
|
||||
}
|
||||
|
||||
$arguments = [];
|
||||
foreach ($metadata->getArguments() as $data) {
|
||||
$argument = [
|
||||
'name' => $data['name'],
|
||||
'required' => $data['required'] ?? false,
|
||||
];
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$argument['description'] = $data['description'];
|
||||
}
|
||||
$arguments[] = $argument;
|
||||
}
|
||||
|
||||
if ([] !== $arguments) {
|
||||
$result['arguments'] = $arguments;
|
||||
}
|
||||
|
||||
$prompts[] = $result;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'prompts' => $prompts,
|
||||
];
|
||||
|
||||
if (null !== $nextCursor && \count($prompts) === $this->pageSize) {
|
||||
$result['nextCursor'] = $nextCursor;
|
||||
}
|
||||
|
||||
return new Response($message->id, $result);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'prompts/list';
|
||||
}
|
||||
}
|
||||
79
src/Server/RequestHandler/ResourceListHandler.php
Normal file
79
src/Server/RequestHandler/ResourceListHandler.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Resource\CollectionInterface;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ResourceListHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response
|
||||
{
|
||||
$nextCursor = null;
|
||||
$resources = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata(
|
||||
$this->pageSize,
|
||||
$message->params['cursor'] ?? null
|
||||
);
|
||||
|
||||
foreach ($metadataList as $metadata) {
|
||||
$nextCursor = $metadata->getUri();
|
||||
$result = [
|
||||
'uri' => $metadata->getUri(),
|
||||
'name' => $metadata->getName(),
|
||||
];
|
||||
|
||||
$description = $metadata->getDescription();
|
||||
if (null !== $description) {
|
||||
$result['description'] = $description;
|
||||
}
|
||||
|
||||
$mimeType = $metadata->getMimeType();
|
||||
if (null !== $mimeType) {
|
||||
$result['mimeType'] = $mimeType;
|
||||
}
|
||||
|
||||
$size = $metadata->getSize();
|
||||
if (null !== $size) {
|
||||
$result['size'] = $size;
|
||||
}
|
||||
|
||||
$resources[] = $result;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'resources' => $resources,
|
||||
];
|
||||
|
||||
if (null !== $nextCursor && \count($resources) === $this->pageSize) {
|
||||
$result['nextCursor'] = $nextCursor;
|
||||
}
|
||||
|
||||
return new Response($message->id, $result);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'resources/list';
|
||||
}
|
||||
}
|
||||
59
src/Server/RequestHandler/ResourceReadHandler.php
Normal file
59
src/Server/RequestHandler/ResourceReadHandler.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Resource\ResourceRead;
|
||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Exception\ResourceNotFoundException;
|
||||
use Mcp\Message\Error;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ResourceReadHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ResourceReaderInterface $reader,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response|Error
|
||||
{
|
||||
$uri = $message->params['uri'];
|
||||
|
||||
try {
|
||||
$result = $this->reader->read(new ResourceRead(uniqid('', true), $uri));
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
return new Error($message->id, Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
||||
} catch (ExceptionInterface) {
|
||||
return Error::internalError($message->id, 'Error while reading resource');
|
||||
}
|
||||
|
||||
return new Response($message->id, [
|
||||
'contents' => [
|
||||
[
|
||||
'uri' => $result->uri,
|
||||
'mimeType' => $result->mimeType,
|
||||
$result->type => $result->result,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'resources/read';
|
||||
}
|
||||
}
|
||||
76
src/Server/RequestHandler/ToolCallHandler.php
Normal file
76
src/Server/RequestHandler/ToolCallHandler.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Tool\ToolCall;
|
||||
use Mcp\Capability\Tool\ToolExecutorInterface;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Message\Error;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ToolCallHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ToolExecutorInterface $toolExecutor,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response|Error
|
||||
{
|
||||
$name = $message->params['name'];
|
||||
$arguments = $message->params['arguments'] ?? [];
|
||||
|
||||
try {
|
||||
$result = $this->toolExecutor->call(new ToolCall(uniqid('', true), $name, $arguments));
|
||||
} catch (ExceptionInterface) {
|
||||
return Error::internalError($message->id, 'Error while executing tool');
|
||||
}
|
||||
|
||||
$content = match ($result->type) {
|
||||
'text' => [
|
||||
'type' => 'text',
|
||||
'text' => $result->result,
|
||||
],
|
||||
'image', 'audio' => [
|
||||
'type' => $result->type,
|
||||
'data' => $result->result,
|
||||
'mimeType' => $result->mimeType,
|
||||
],
|
||||
'resource' => [
|
||||
'type' => 'resource',
|
||||
'resource' => [
|
||||
'uri' => $result->uri,
|
||||
'mimeType' => $result->mimeType,
|
||||
'text' => $result->result,
|
||||
],
|
||||
],
|
||||
// TODO better exception
|
||||
default => throw new InvalidArgumentException('Unsupported tool result type: '.$result->type),
|
||||
};
|
||||
|
||||
return new Response($message->id, [
|
||||
'content' => [$content], // TODO: allow multiple `ToolCallResult`s in the future
|
||||
'isError' => $result->isError,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'tools/call';
|
||||
}
|
||||
}
|
||||
68
src/Server/RequestHandler/ToolListHandler.php
Normal file
68
src/Server/RequestHandler/ToolListHandler.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Tool\CollectionInterface;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ToolListHandler extends BaseRequestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createResponse(Request $message): Response
|
||||
{
|
||||
$nextCursor = null;
|
||||
$tools = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata(
|
||||
$this->pageSize,
|
||||
$message->params['cursor'] ?? null
|
||||
);
|
||||
|
||||
foreach ($metadataList as $tool) {
|
||||
$nextCursor = $tool->getName();
|
||||
$inputSchema = $tool->getInputSchema();
|
||||
$tools[] = [
|
||||
'name' => $tool->getName(),
|
||||
'description' => $tool->getDescription(),
|
||||
'inputSchema' => [] === $inputSchema ? [
|
||||
'type' => 'object',
|
||||
'$schema' => 'http://json-schema.org/draft-07/schema#',
|
||||
] : $inputSchema,
|
||||
];
|
||||
}
|
||||
|
||||
$result = [
|
||||
'tools' => $tools,
|
||||
];
|
||||
|
||||
if (null !== $nextCursor && \count($tools) === $this->pageSize) {
|
||||
$result['nextCursor'] = $nextCursor;
|
||||
}
|
||||
|
||||
return new Response($message->id, $result);
|
||||
}
|
||||
|
||||
protected function supportedMethod(): string
|
||||
{
|
||||
return 'tools/list';
|
||||
}
|
||||
}
|
||||
30
src/Server/RequestHandlerInterface.php
Normal file
30
src/Server/RequestHandlerInterface.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server;
|
||||
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Message\Error;
|
||||
use Mcp\Message\Request;
|
||||
use Mcp\Message\Response;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface RequestHandlerInterface
|
||||
{
|
||||
public function supports(Request $message): bool;
|
||||
|
||||
/**
|
||||
* @throws ExceptionInterface When the handler encounters an error processing the request
|
||||
*/
|
||||
public function createResponse(Request $message): Response|Error;
|
||||
}
|
||||
65
src/Server/Transport/Sse/Store/CachePoolStore.php
Normal file
65
src/Server/Transport/Sse/Store/CachePoolStore.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\Transport\Sse\Store;
|
||||
|
||||
use Mcp\Server\Transport\Sse\StoreInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class CachePoolStore implements StoreInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $cachePool,
|
||||
) {
|
||||
}
|
||||
|
||||
public function push(Uuid $id, string $message): void
|
||||
{
|
||||
$item = $this->cachePool->getItem($this->getCacheKey($id));
|
||||
|
||||
$messages = $item->isHit() ? $item->get() : [];
|
||||
$messages[] = $message;
|
||||
$item->set($messages);
|
||||
|
||||
$this->cachePool->save($item);
|
||||
}
|
||||
|
||||
public function pop(Uuid $id): ?string
|
||||
{
|
||||
$item = $this->cachePool->getItem($this->getCacheKey($id));
|
||||
|
||||
if (!$item->isHit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$messages = $item->get();
|
||||
$message = array_shift($messages);
|
||||
|
||||
$item->set($messages);
|
||||
$this->cachePool->save($item);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function remove(Uuid $id): void
|
||||
{
|
||||
$this->cachePool->deleteItem($this->getCacheKey($id));
|
||||
}
|
||||
|
||||
private function getCacheKey(Uuid $id): string
|
||||
{
|
||||
return 'message_'.$id->toRfc4122();
|
||||
}
|
||||
}
|
||||
26
src/Server/Transport/Sse/StoreInterface.php
Normal file
26
src/Server/Transport/Sse/StoreInterface.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\Transport\Sse;
|
||||
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface StoreInterface
|
||||
{
|
||||
public function push(Uuid $id, string $message): void;
|
||||
|
||||
public function pop(Uuid $id): ?string;
|
||||
|
||||
public function remove(Uuid $id): void;
|
||||
}
|
||||
65
src/Server/Transport/Sse/StreamTransport.php
Normal file
65
src/Server/Transport/Sse/StreamTransport.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\Transport\Sse;
|
||||
|
||||
use Mcp\Server\TransportInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class StreamTransport implements TransportInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $messageEndpoint,
|
||||
private readonly StoreInterface $store,
|
||||
private readonly Uuid $id,
|
||||
) {
|
||||
}
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
ignore_user_abort(true);
|
||||
$this->flushEvent('endpoint', $this->messageEndpoint);
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return 0 === connection_aborted();
|
||||
}
|
||||
|
||||
public function receive(): \Generator
|
||||
{
|
||||
yield $this->store->pop($this->id);
|
||||
}
|
||||
|
||||
public function send(string $data): void
|
||||
{
|
||||
$this->flushEvent('message', $data);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
$this->store->remove($this->id);
|
||||
}
|
||||
|
||||
private function flushEvent(string $event, string $data): void
|
||||
{
|
||||
echo \sprintf('event: %s', $event).\PHP_EOL;
|
||||
echo \sprintf('data: %s', $data).\PHP_EOL;
|
||||
echo \PHP_EOL;
|
||||
if (false !== ob_get_length()) {
|
||||
ob_flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
}
|
||||
65
src/Server/Transport/Stdio/SymfonyConsoleTransport.php
Normal file
65
src/Server/Transport/Stdio/SymfonyConsoleTransport.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\Transport\Stdio;
|
||||
|
||||
use Mcp\Server\TransportInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\StreamableInputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Heavily inspired by https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents.
|
||||
*/
|
||||
final class SymfonyConsoleTransport implements TransportInterface
|
||||
{
|
||||
private string $buffer = '';
|
||||
|
||||
public function __construct(
|
||||
private readonly InputInterface $input,
|
||||
private readonly OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function receive(): \Generator
|
||||
{
|
||||
$stream = $this->input instanceof StreamableInputInterface ? $this->input->getStream() ?? \STDIN : \STDIN;
|
||||
$line = fgets($stream);
|
||||
if (false === $line) {
|
||||
return;
|
||||
}
|
||||
$this->buffer .= \STDIN === $stream ? rtrim($line).\PHP_EOL : $line;
|
||||
if (str_contains($this->buffer, \PHP_EOL)) {
|
||||
$lines = explode(\PHP_EOL, $this->buffer);
|
||||
$this->buffer = array_pop($lines);
|
||||
|
||||
yield from $lines;
|
||||
}
|
||||
}
|
||||
|
||||
public function send(string $data): void
|
||||
{
|
||||
$this->output->writeln($data);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
28
src/Server/TransportInterface.php
Normal file
28
src/Server/TransportInterface.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface TransportInterface
|
||||
{
|
||||
public function initialize(): void;
|
||||
|
||||
public function isConnected(): bool;
|
||||
|
||||
public function receive(): \Generator;
|
||||
|
||||
public function send(string $data): void;
|
||||
|
||||
public function close(): void;
|
||||
}
|
||||
Loading…
Reference in a new issue