[Server] Add Bidirectional Client Communication Support (#109)

* feat(server): add bidirectional client communication support

* fix(server): use correct session ID string conversion method

* fix: clean up docblocks for request handler methods in Builder

* fix: merge artifacts
This commit is contained in:
Kyrian Obikwelu 2025-10-26 22:48:17 +01:00 • committed by GitHub
commit f1b471a219
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1973 additions and 445 deletions

View file

@ -26,6 +26,8 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @implements RequestHandlerInterface<CallToolResult>
*
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
@ -43,6 +45,9 @@ final class CallToolHandler implements RequestHandlerInterface
return $request instanceof CallToolRequest;
}
/**
* @return Response<CallToolResult>|Error
*/
public function handle(Request $request, SessionInterface $session): Response|Error
{
\assert($request instanceof CallToolRequest);
@ -58,6 +63,8 @@ final class CallToolHandler implements RequestHandlerInterface
throw new ToolNotFoundException($request);
}
$arguments['_session'] = $session;
$result = $this->referenceHandler->handle($reference, $arguments);
if (!$result instanceof CallToolResult) {

View file

@ -24,6 +24,8 @@ use Psr\Container\ContainerInterface;
/**
* Handles completion/complete requests.
*
* @implements RequestHandlerInterface<CompletionCompleteResult>
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
final class CompletionCompleteHandler implements RequestHandlerInterface
@ -39,6 +41,9 @@ final class CompletionCompleteHandler implements RequestHandlerInterface
return $request instanceof CompletionCompleteRequest;
}
/**
* @return Response<CompletionCompleteResult>|Error
*/
public function handle(Request $request, SessionInterface $session): Response|Error
{
\assert($request instanceof CompletionCompleteRequest);

View file

@ -26,6 +26,8 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @implements RequestHandlerInterface<GetPromptResult>
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class GetPromptHandler implements RequestHandlerInterface
@ -42,6 +44,9 @@ final class GetPromptHandler implements RequestHandlerInterface
return $request instanceof GetPromptRequest;
}
/**
* @return Response<GetPromptResult>|Error
*/
public function handle(Request $request, SessionInterface $session): Response|Error
{
\assert($request instanceof GetPromptRequest);
@ -55,6 +60,8 @@ final class GetPromptHandler implements RequestHandlerInterface
throw new PromptNotFoundException($request);
}
$arguments['_session'] = $session;
$result = $this->referenceHandler->handle($reference, $arguments);
$formatted = $reference->formatResult($result);
@ -67,11 +74,11 @@ final class GetPromptHandler implements RequestHandlerInterface
} catch (PromptGetException|ExceptionInterface $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $request->getId());
return Error::forInternalError('Error while handling prompt: '.$e->getMessage(), $request->getId());
} catch (\Throwable $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $request->getId());
return Error::forInternalError('Error while handling prompt: '.$e->getMessage(), $request->getId());
}
}
}

View file

@ -21,6 +21,8 @@ use Mcp\Server\Configuration;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<InitializeResult>
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class InitializeHandler implements RequestHandlerInterface
@ -35,6 +37,9 @@ final class InitializeHandler implements RequestHandlerInterface
return $request instanceof InitializeRequest;
}
/**
* @return Response<InitializeResult>
*/
public function handle(Request $request, SessionInterface $session): Response
{
\assert($request instanceof InitializeRequest);

View file

@ -20,6 +20,8 @@ use Mcp\Schema\Result\ListPromptsResult;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<ListPromptsResult>
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ListPromptsHandler implements RequestHandlerInterface
@ -36,6 +38,8 @@ final class ListPromptsHandler implements RequestHandlerInterface
}
/**
* @return Response<ListPromptsResult>
*
* @throws InvalidCursorException
*/
public function handle(Request $request, SessionInterface $session): Response

View file

@ -20,6 +20,8 @@ use Mcp\Schema\Result\ListResourceTemplatesResult;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<ListResourceTemplatesResult>
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class ListResourceTemplatesHandler implements RequestHandlerInterface
@ -36,6 +38,8 @@ final class ListResourceTemplatesHandler implements RequestHandlerInterface
}
/**
* @return Response<ListResourceTemplatesResult>
*
* @throws InvalidCursorException
*/
public function handle(Request $request, SessionInterface $session): Response

View file

@ -20,6 +20,8 @@ use Mcp\Schema\Result\ListResourcesResult;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<ListResourcesResult>
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ListResourcesHandler implements RequestHandlerInterface
@ -36,6 +38,8 @@ final class ListResourcesHandler implements RequestHandlerInterface
}
/**
* @return Response<ListResourcesResult>
*
* @throws InvalidCursorException
*/
public function handle(Request $request, SessionInterface $session): Response

View file

@ -20,6 +20,8 @@ use Mcp\Schema\Result\ListToolsResult;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<ListToolsResult>
*
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
@ -37,6 +39,8 @@ final class ListToolsHandler implements RequestHandlerInterface
}
/**
* @return Response<ListToolsResult>
*
* @throws InvalidCursorException When the cursor is invalid
*/
public function handle(Request $request, SessionInterface $session): Response

View file

@ -18,6 +18,8 @@ use Mcp\Schema\Result\EmptyResult;
use Mcp\Server\Session\SessionInterface;
/**
* @implements RequestHandlerInterface<EmptyResult>
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class PingHandler implements RequestHandlerInterface
@ -27,6 +29,9 @@ final class PingHandler implements RequestHandlerInterface
return $request instanceof PingRequest;
}
/**
* @return Response<EmptyResult>
*/
public function handle(Request $request, SessionInterface $session): Response
{
\assert($request instanceof PingRequest);

View file

@ -25,6 +25,8 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @implements RequestHandlerInterface<ReadResourceResult>
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ReadResourceHandler implements RequestHandlerInterface
@ -41,6 +43,9 @@ final class ReadResourceHandler implements RequestHandlerInterface
return $request instanceof ReadResourceRequest;
}
/**
* @return Response<ReadResourceResult>|Error
*/
public function handle(Request $request, SessionInterface $session): Response|Error
{
\assert($request instanceof ReadResourceRequest);
@ -55,7 +60,12 @@ final class ReadResourceHandler implements RequestHandlerInterface
throw new ResourceNotFoundException($request);
}
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
$arguments = [
'uri' => $uri,
'_session' => $session,
];
$result = $this->referenceHandler->handle($reference, $arguments);
if ($reference instanceof ResourceTemplateReference) {
$formatted = $reference->formatResult($result, $uri, $reference->resourceTemplate->mimeType);

View file

@ -17,11 +17,16 @@ use Mcp\Schema\JsonRpc\Response;
use Mcp\Server\Session\SessionInterface;
/**
* @template TResult
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
interface RequestHandlerInterface
{
public function supports(Request $request): bool;
/**
* @return Response<TResult>|Error
*/
public function handle(Request $request, SessionInterface $session): Response|Error;
}