[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:
parent
0963d243bc
commit
f1b471a219
3 changed files with 1973 additions and 445 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue