[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
3 changed files with 1973 additions and 445 deletions

View file

@ -59,7 +59,13 @@ abstract class Request implements HasMethodInterface, MessageInterface
$request->id = $data['id'];
if (isset($data['params']['_meta'])) {
$request->meta = $data['params']['_meta'];
$meta = $data['params']['_meta'];
if ($meta instanceof \stdClass) {
$meta = (array) $meta;
}
if (\is_array($meta)) {
$request->meta = $meta;
}
}
return $request;
@ -75,6 +81,33 @@ abstract class Request implements HasMethodInterface, MessageInterface
return $this->id;
}
/**
* @return array<string, mixed>|null
*/
public function getMeta(): ?array
{
return $this->meta;
}
public function withId(string|int $id): static
{
$clone = clone $this;
$clone->id = $id;
return $clone;
}
/**
* @param array<string, mixed>|null $meta
*/
public function withMeta(?array $meta): static
{
$clone = clone $this;
$clone->meta = $meta;
return $clone;
}
/**
* @return RequestData
*/

View file

@ -14,23 +14,26 @@ namespace Mcp\Schema\JsonRpc;
use Mcp\Exception\InvalidArgumentException;
/**
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
* @template TResult
*
* @phpstan-type ResponseData array{
* jsonrpc: string,
* id: string|int,
* result: array<string, mixed>,
* }
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class Response implements MessageInterface
{
/**
* @param string|int $id this MUST be the same as the value of the id member in the Request Object
* @param ResultInterface|array<string, mixed> $result the value of this member is determined by the method invoked on the Server
* @param string|int $id this MUST be the same as the value of the id member in the Request Object
* @param TResult $result the value of this member is determined by the method invoked on the Server
*/
public function __construct(
public readonly string|int $id,
public readonly ResultInterface|array $result,
/** @var TResult */
public readonly mixed $result,
) {
}
@ -41,6 +44,8 @@ class Response implements MessageInterface
/**
* @param ResponseData $data
*
* @return self<array<string, mixed>>
*/
public static function fromArray(array $data): self
{
@ -67,7 +72,7 @@ class Response implements MessageInterface
* @return array{
* jsonrpc: string,
* id: string|int,
* result: ResultInterface,
* result: mixed,
* }
*/
public function jsonSerialize(): array

View file

@ -11,6 +11,7 @@
namespace Mcp\Schema\Result;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Content\AudioContent;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\TextContent;
@ -40,6 +41,51 @@ class CreateSamplingMessageResult implements ResultInterface
) {
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
if (!isset($data['role']) || !\is_string($data['role'])) {
throw new InvalidArgumentException('Missing or invalid "role" in CreateSamplingMessageResult data.');
}
if (!isset($data['content']) || !\is_array($data['content'])) {
throw new InvalidArgumentException('Missing or invalid "content" in CreateSamplingMessageResult data.');
}
if (!isset($data['model']) || !\is_string($data['model'])) {
throw new InvalidArgumentException('Missing or invalid "model" in CreateSamplingMessageResult data.');
}
$role = Role::from($data['role']);
$contentPayload = $data['content'];
$content = self::hydrateContent($contentPayload);
$stopReason = isset($data['stopReason']) && \is_string($data['stopReason']) ? $data['stopReason'] : null;
return new self($role, $content, $data['model'], $stopReason);
}
/**
* @param array<string, mixed> $contentData
*/
private static function hydrateContent(array $contentData): TextContent|ImageContent|AudioContent
{
$type = $contentData['type'] ?? null;
if (!\is_string($type)) {
throw new InvalidArgumentException('Missing or invalid "type" in sampling content payload.');
}
return match ($type) {
'text' => TextContent::fromArray($contentData),
'image' => ImageContent::fromArray($contentData),
'audio' => AudioContent::fromArray($contentData),
default => throw new InvalidArgumentException(\sprintf('Unsupported sampling content type "%s".', $type)),
};
}
/**
* @return array{
* role: string,