-
Notifications
You must be signed in to change notification settings - Fork 168
[Server] Add outgoing (elicitation, sampling) request and client response events #386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
f221c38
6c0f6fb
25f3deb
1c07c98
a626b04
40286f3
96cfe12
7aa5c7c
f53d660
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?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\Event; | ||
|
|
||
| use Mcp\Schema\JsonRpc\Error; | ||
| use Mcp\Schema\JsonRpc\Response; | ||
| use Mcp\Server\Session\SessionInterface; | ||
|
|
||
| /** | ||
| * Event dispatched when the server receives a client response to a prior outgoing request. | ||
| * | ||
| * @author Olivier Mouren <mouren.olivier@gmail.com> | ||
| */ | ||
| final class ClientResponseEvent | ||
| { | ||
| /** | ||
| * @param Response<mixed>|Error $response | ||
| */ | ||
| public function __construct( | ||
| private readonly Response|Error $response, | ||
| private readonly SessionInterface $session, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * @return Response<mixed>|Error | ||
| */ | ||
| public function getResponse(): Response|Error | ||
| { | ||
| return $this->response; | ||
| } | ||
|
|
||
| public function getSession(): SessionInterface | ||
| { | ||
| return $this->session; | ||
| } | ||
|
|
||
| public function getId(): string|int | ||
|
omouren marked this conversation as resolved.
Outdated
|
||
| { | ||
| return $this->response->getId(); | ||
| } | ||
|
|
||
| public function isError(): bool | ||
| { | ||
| return $this->response instanceof Error; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| <?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\Event; | ||
|
|
||
| use Mcp\Schema\JsonRpc\Request; | ||
| use Mcp\Server\Session\SessionInterface; | ||
|
|
||
| /** | ||
| * Event dispatched when the server sends a request to the client (e.g. elicitation/create, sampling/create). | ||
| * | ||
| * @author Olivier Mouren <mouren.olivier@gmail.com> | ||
| */ | ||
| final class ServerRequestEvent | ||
| { | ||
| public function __construct( | ||
| private readonly Request $request, | ||
| private readonly int $timeout, | ||
| private readonly SessionInterface $session, | ||
| ) { | ||
| } | ||
|
|
||
| public function getRequest(): Request | ||
| { | ||
| return $this->request; | ||
| } | ||
|
|
||
| public function getSession(): SessionInterface | ||
| { | ||
| return $this->session; | ||
| } | ||
|
|
||
| public function getTimeout(): int | ||
| { | ||
| return $this->timeout; | ||
| } | ||
|
|
||
| public function getMethod(): string | ||
| { | ||
| return $this->request::getMethod(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,10 +11,12 @@ | |
|
|
||
| namespace Mcp\Server; | ||
|
|
||
| use Mcp\Event\ClientResponseEvent; | ||
| use Mcp\Event\ErrorEvent; | ||
| use Mcp\Event\NotificationEvent; | ||
| use Mcp\Event\RequestEvent; | ||
| use Mcp\Event\ResponseEvent; | ||
| use Mcp\Event\ServerRequestEvent; | ||
| use Mcp\Exception\InvalidInputMessageException; | ||
| use Mcp\JsonRpc\MessageFactory; | ||
| use Mcp\Schema\JsonRpc\Error; | ||
|
|
@@ -58,6 +60,9 @@ class Protocol | |
| /** Session key for outgoing message queue */ | ||
| private const SESSION_OUTGOING_QUEUE = '_mcp.outgoing_queue'; | ||
|
|
||
| /** Session key for the client request that started a suspended Fiber */ | ||
| private const SESSION_FIBER_PARENT_REQUEST = '_mcp.fiber_parent_request'; | ||
|
|
||
| /** Session key for active request meta */ | ||
| public const SESSION_ACTIVE_REQUEST_META = '_mcp.active_request_meta'; | ||
|
|
||
|
|
@@ -106,6 +111,8 @@ public function connect(TransportInterface $transport): void | |
|
|
||
| $transport->setFiberYieldHandler($this->handleFiberYield(...)); | ||
|
|
||
| $transport->setFiberTerminationHandler($this->handleFiberTermination(...)); | ||
|
|
||
| $this->logger->info('Protocol connected to transport', ['transport' => $transport::class]); | ||
| } | ||
|
|
||
|
|
@@ -298,6 +305,8 @@ private function handleRequest(TransportInterface $transport, Request $request, | |
| $result = $fiber->start(); | ||
|
|
||
| if ($fiber->isSuspended()) { | ||
| $session->set(self::SESSION_FIBER_PARENT_REQUEST, $request->jsonSerialize()); | ||
|
|
||
| if (\is_array($result) && isset($result['type'])) { | ||
| if ('notification' === $result['type']) { | ||
| $notification = $result['notification']; | ||
|
|
@@ -361,6 +370,8 @@ private function handleResponse(Response|Error $response, SessionInterface $sess | |
| { | ||
| $this->logger->info('Handling response from client.', ['response' => $response]); | ||
|
|
||
| $this->dispatchEvent(new ClientResponseEvent($response, $session)); | ||
|
|
||
| $messageId = $response->getId(); | ||
|
|
||
| if (null === $messageId) { | ||
|
|
@@ -408,6 +419,8 @@ public function sendRequest(Request $request, int $timeout, SessionInterface $se | |
|
|
||
| $requestWithId = $request->withId($requestId); | ||
|
|
||
| $this->dispatchEvent(new ServerRequestEvent($requestWithId, $timeout, $session)); | ||
|
|
||
| $this->logger->info('Queueing server request to client', [ | ||
| 'request_id' => $requestId, | ||
| 'method' => $request::getMethod(), | ||
|
|
@@ -645,6 +658,49 @@ public function handleFiberYield(mixed $yieldedValue, ?Uuid $sessionId): void | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle the final result of a suspended Fiber when it completes. | ||
| * | ||
| * Dispatches ResponseEvent or ErrorEvent for the original client request that | ||
| * started the Fiber, allowing listeners to observe deferred responses. | ||
| * | ||
| * @phpstan-param Response<mixed>|Error $finalResult | ||
| * | ||
| * @phpstan-return Response<mixed>|Error | ||
| */ | ||
| public function handleFiberTermination(Response|Error $finalResult, Uuid $sessionId): Response|Error | ||
| { | ||
| $session = $this->sessionManager->createWithId($sessionId); | ||
| $parentRequest = $this->resolveFiberParentRequest( | ||
| $session->pull(self::SESSION_FIBER_PARENT_REQUEST) | ||
| ); | ||
|
|
||
| if (null !== $parentRequest) { | ||
| if ($finalResult instanceof Response) { | ||
| $responseEvent = $this->dispatchEvent(new ResponseEvent($finalResult, $parentRequest, $session)); | ||
| $finalResult = $responseEvent->getResponse(); | ||
| } else { | ||
| $errorEvent = $this->dispatchEvent(new ErrorEvent($finalResult, $parentRequest, $session, null)); | ||
| $finalResult = $errorEvent->getError(); | ||
| } | ||
| } | ||
|
|
||
| $session->save(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be moved into the or maybe just invert that to keep the if small if (null === $parentRequest) {
return $finalResult;
}
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chr-hertel |
||
|
|
||
| return $finalResult; | ||
| } | ||
|
|
||
| private function resolveFiberParentRequest(mixed $data): ?Request | ||
| { | ||
| if (!\is_array($data)) { | ||
| return null; | ||
| } | ||
|
|
||
| $message = $this->messageFactory->createFromArray($data); | ||
|
|
||
| return $message instanceof Request ? $message : null; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<int, mixed> $messages | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.