Introduce schema component
This commit is contained in:
parent
ee0dffe369
commit
6025f890f9
76 changed files with 5419 additions and 0 deletions
|
|
@ -19,6 +19,7 @@
|
|||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-fileinfo": "*",
|
||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||
"symfony/uid": "^6.4 || ^7.0"
|
||||
},
|
||||
|
|
|
|||
19
src/Exception/RuntimeException.php
Normal file
19
src/Exception/RuntimeException.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
87
src/Schema/Annotations.php
Normal file
87
src/Schema/Annotations.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?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\Schema;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
|
||||
/**
|
||||
* Optional annotations for the client. The client can use annotations
|
||||
* to inform how objects are used or displayed.
|
||||
*
|
||||
* @phpstan-type AnnotationsData array{
|
||||
* audience?: string[],
|
||||
* priority?: float
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class Annotations implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param Role[]|null $audience Describes who the intended customer of this object or data is.
|
||||
*
|
||||
* It can include multiple entries to indicate content useful for multiple audiences (e.g., `[Role::User, Role::Assistant]`).
|
||||
* @param float|null $priority Describes how important this data is for operating the server.
|
||||
*
|
||||
* A value of 1 means "most important," and indicates that the data is
|
||||
* effectively required, while 0 means "least important," and indicates that
|
||||
* the data is entirely optional.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?array $audience = null,
|
||||
public readonly ?float $priority = null,
|
||||
) {
|
||||
if (null !== $this->priority && ($this->priority < 0 || $this->priority > 1)) {
|
||||
throw new InvalidArgumentException('Annotation priority must be between 0 and 1.');
|
||||
}
|
||||
if (null !== $this->audience) {
|
||||
foreach ($this->audience as $role) {
|
||||
if (!($role instanceof Role)) {
|
||||
throw new InvalidArgumentException('All audience members must be instances of Role enum.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AnnotationsData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$audience = null;
|
||||
if (isset($data['audience']) && \is_array($data['audience'])) {
|
||||
$audience = array_map(fn (string $r) => Role::from($r), $data['audience']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
$audience,
|
||||
isset($data['priority']) ? (float) $data['priority'] : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AnnotationsData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [];
|
||||
if (null !== $this->audience) {
|
||||
$data['audience'] = array_map(fn (Role $r) => $r->value, $this->audience);
|
||||
}
|
||||
if (null !== $this->priority) {
|
||||
$data['priority'] = $this->priority;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
94
src/Schema/ClientCapabilities.php
Normal file
94
src/Schema/ClientCapabilities.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?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\Schema;
|
||||
|
||||
/**
|
||||
* Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set:
|
||||
* any client can define its own, additional capabilities.
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class ClientCapabilities implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $experimental
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?bool $roots = false,
|
||||
public readonly ?bool $rootsListChanged = null,
|
||||
public readonly ?bool $sampling = null,
|
||||
public readonly ?array $experimental = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* roots?: array{
|
||||
* listChanged?: bool,
|
||||
* },
|
||||
* sampling?: bool,
|
||||
* experimental?: array<string, mixed>,
|
||||
* } $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$rootsEnabled = isset($data['roots']);
|
||||
$rootsListChanged = null;
|
||||
if ($rootsEnabled) {
|
||||
if (\is_array($data['roots']) && \array_key_exists('listChanged', $data['roots'])) {
|
||||
$rootsListChanged = (bool) $data['roots']['listChanged'];
|
||||
} elseif (\is_object($data['roots']) && property_exists($data['roots'], 'listChanged')) {
|
||||
$rootsListChanged = (bool) $data['roots']->listChanged;
|
||||
}
|
||||
}
|
||||
|
||||
$sampling = null;
|
||||
if (isset($data['sampling'])) {
|
||||
$sampling = true;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$rootsEnabled,
|
||||
$rootsListChanged,
|
||||
$sampling,
|
||||
$data['experimental'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* roots?: object,
|
||||
* sampling?: object,
|
||||
* experimental?: object,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [];
|
||||
if ($this->roots || $this->rootsListChanged) {
|
||||
$data['roots'] = new \stdClass();
|
||||
if ($this->rootsListChanged) {
|
||||
$data['roots']->listChanged = $this->rootsListChanged;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sampling) {
|
||||
$data['sampling'] = new \stdClass();
|
||||
}
|
||||
|
||||
if ($this->experimental) {
|
||||
$data['experimental'] = (object) $this->experimental;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
29
src/Schema/Constants.php
Normal file
29
src/Schema/Constants.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\Schema;
|
||||
|
||||
/**
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
interface Constants
|
||||
{
|
||||
public const LATEST_PROTOCOL_VERSION = '2025-03-26';
|
||||
public const JSONRPC_VERSION = '2.0';
|
||||
|
||||
public const PARSE_ERROR = -32700;
|
||||
public const INVALID_REQUEST = -32600;
|
||||
public const METHOD_NOT_FOUND = -32601;
|
||||
public const INVALID_PARAMS = -32602;
|
||||
public const INTERNAL_ERROR = -32603;
|
||||
|
||||
public const SERVER_ERROR = -32000;
|
||||
}
|
||||
117
src/Schema/Content/AudioContent.php
Normal file
117
src/Schema/Content/AudioContent.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Exception\RuntimeException;
|
||||
use Mcp\Schema\Annotations;
|
||||
|
||||
/**
|
||||
* Represents audio content in MCP.
|
||||
*
|
||||
* @phpstan-import-type AnnotationsData from Annotations
|
||||
*
|
||||
* @phpstan-type AudioContentData = array{
|
||||
* type: 'audio',
|
||||
* data: string,
|
||||
* mimeType: string,
|
||||
* annotations?: AnnotationsData,
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class AudioContent extends Content
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $data,
|
||||
public readonly string $mimeType,
|
||||
public readonly ?Annotations $annotations = null,
|
||||
) {
|
||||
parent::__construct('audio');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AudioContentData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['data']) || !isset($data['mimeType'])) {
|
||||
throw new InvalidArgumentException('Invalid or missing "data" or "mimeType" in AudioContent data.');
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['data'],
|
||||
$data['mimeType'],
|
||||
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new AudioContent from a file path.
|
||||
*
|
||||
* @param string $path Path to the audio file
|
||||
* @param string|null $mimeType Optional MIME type override
|
||||
* @param ?Annotations $annotations Optional annotations describing the content
|
||||
*
|
||||
* @throws InvalidArgumentException If the file doesn't exist
|
||||
*/
|
||||
public static function fromFile(string $path, ?string $mimeType = null, ?Annotations $annotations = null): self
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
throw new InvalidArgumentException(\sprintf('Audio file not found: "%s".', $path));
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
if (false === $content) {
|
||||
throw new RuntimeException(\sprintf('Could not read audio file: "%s".', $path));
|
||||
}
|
||||
$data = base64_encode($content);
|
||||
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'application/octet-stream';
|
||||
|
||||
return new self($data, $detectedMime, $annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new AudioContent from a string.
|
||||
*
|
||||
* @param string $data The audio data
|
||||
* @param string $mimeType MIME type of the audio
|
||||
* @param ?Annotations $annotations Optional annotations describing the content
|
||||
*/
|
||||
public static function fromString(string $data, string $mimeType, ?Annotations $annotations = null): self
|
||||
{
|
||||
return new self(base64_encode($data), $mimeType, $annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* type: 'audio',
|
||||
* data: string,
|
||||
* mimeType: string,
|
||||
* annotations?: Annotations,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$result = [
|
||||
'type' => 'audio',
|
||||
'data' => $this->data,
|
||||
'mimeType' => $this->mimeType,
|
||||
];
|
||||
|
||||
if (null !== $this->annotations) {
|
||||
$result['annotations'] = $this->annotations;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
85
src/Schema/Content/BlobResourceContents.php
Normal file
85
src/Schema/Content/BlobResourceContents.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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents blob resource contents in MCP.
|
||||
*
|
||||
* @phpstan-type BlobResourceContentsData array{
|
||||
* uri: string,
|
||||
* mimeType?: string|null,
|
||||
* blob: string
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class BlobResourceContents extends ResourceContents
|
||||
{
|
||||
/**
|
||||
* @param string $uri the URI of the resource or sub-resource
|
||||
* @param string|null $mimeType the MIME type of the resource or sub-resource
|
||||
* @param string $blob a base64-encoded string representing the binary data of the item
|
||||
*/
|
||||
public function __construct(
|
||||
string $uri,
|
||||
?string $mimeType,
|
||||
public readonly string $blob,
|
||||
) {
|
||||
parent::__construct($uri, $mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BlobResourceContentsData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['uri']) || !\is_string($data['uri'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "uri" for BlobResourceContents.');
|
||||
}
|
||||
if (!isset($data['blob']) || !\is_string($data['blob'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "blob" for BlobResourceContents.');
|
||||
}
|
||||
|
||||
return new self($data['uri'], $data['mimeType'] ?? null, $data['blob']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
*/
|
||||
public static function fromStream(string $uri, $stream, string $mimeType): self
|
||||
{
|
||||
$blob = stream_get_contents($stream);
|
||||
|
||||
return new self($uri, $mimeType, base64_encode($blob));
|
||||
}
|
||||
|
||||
public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null): self
|
||||
{
|
||||
$mimeType = $explicitMimeType ?? mime_content_type($file->getPathname());
|
||||
$blob = file_get_contents($file->getPathname());
|
||||
|
||||
return new self($uri, $mimeType, base64_encode($blob));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BlobResourceContentsData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'blob' => $this->blob,
|
||||
...$this->jsonSerialize(),
|
||||
];
|
||||
}
|
||||
}
|
||||
25
src/Schema/Content/Content.php
Normal file
25
src/Schema/Content/Content.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
/**
|
||||
* Base class for all content types in MCP.
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
abstract class Content implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $type,
|
||||
) {
|
||||
}
|
||||
}
|
||||
152
src/Schema/Content/EmbeddedResource.php
Normal file
152
src/Schema/Content/EmbeddedResource.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Exception\RuntimeException;
|
||||
use Mcp\Schema\Annotations;
|
||||
|
||||
/**
|
||||
* Represents embedded resource content within a message.
|
||||
*
|
||||
* @phpstan-import-type AnnotationsData from Annotations
|
||||
* @phpstan-import-type TextResourceContentsData from TextResourceContents
|
||||
* @phpstan-import-type BlobResourceContentsData from BlobResourceContents
|
||||
*
|
||||
* @phpstan-type EmbeddedResourceData = array{
|
||||
* type: 'resource',
|
||||
* resource: TextResourceContentsData|BlobResourceContentsData,
|
||||
* annotations?: AnnotationsData
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class EmbeddedResource extends Content
|
||||
{
|
||||
public function __construct(
|
||||
public readonly TextResourceContents|BlobResourceContents $resource,
|
||||
public readonly ?Annotations $annotations = null,
|
||||
) {
|
||||
parent::__construct('resource');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EmbeddedResourceData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (($data['type'] ?? null) !== 'resource') {
|
||||
throw new InvalidArgumentException('Invalid type for EmbeddedResource.');
|
||||
}
|
||||
if (!isset($data['resource']) || !\is_array($data['resource'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "resource" field for EmbeddedResource.');
|
||||
}
|
||||
|
||||
$resourceData = $data['resource'];
|
||||
if (isset($resourceData['text'])) {
|
||||
$resourceInstance = TextResourceContents::fromArray($resourceData);
|
||||
} elseif (isset($resourceData['blob'])) {
|
||||
$resourceInstance = BlobResourceContents::fromArray($resourceData);
|
||||
} else {
|
||||
throw new InvalidArgumentException('EmbeddedResource "resource" field must contain "text" or "blob".');
|
||||
}
|
||||
|
||||
return new self(
|
||||
$resourceInstance,
|
||||
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromText(string $uri, string $text, ?string $mimeType = 'text/plain', ?Annotations $annotations = null): self
|
||||
{
|
||||
$textContent = new TextResourceContents($uri, $mimeType, $text);
|
||||
|
||||
return new self($textContent, $annotations);
|
||||
}
|
||||
|
||||
public static function fromBlob(string $uri, string $base64Blob, string $mimeType, ?Annotations $annotations = null): self
|
||||
{
|
||||
$blobContent = new BlobResourceContents($uri, $mimeType, $base64Blob);
|
||||
|
||||
return new self($blobContent, $annotations);
|
||||
}
|
||||
|
||||
public static function fromFile(string $uri, string $path, ?string $explicitMimeType = null, ?Annotations $annotations = null): self
|
||||
{
|
||||
if (!file_exists($path) || !is_readable($path)) {
|
||||
throw new InvalidArgumentException(\sprintf('File not found or not readable: "%s".', $path));
|
||||
}
|
||||
$content = file_get_contents($path);
|
||||
if (false === $content) {
|
||||
throw new RuntimeException(\sprintf('Could not read file: "%s".', $path));
|
||||
}
|
||||
|
||||
$guessedMimeType = $explicitMimeType ?? mime_content_type($path) ?: 'application/octet-stream';
|
||||
|
||||
if (self::isTextMimeTypeHeuristic($guessedMimeType) && mb_check_encoding($content, 'UTF-8')) {
|
||||
$resourceContent = new TextResourceContents($uri, $guessedMimeType, $content);
|
||||
} else {
|
||||
$resourceContent = new BlobResourceContents($uri, $guessedMimeType, base64_encode($content));
|
||||
}
|
||||
|
||||
return new self($resourceContent, $annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
*/
|
||||
public static function fromStream(string $uri, $stream, string $mimeType, ?Annotations $annotations = null): self
|
||||
{
|
||||
$content = stream_get_contents($stream);
|
||||
if (false === $content) {
|
||||
throw new RuntimeException('Could not read stream.');
|
||||
}
|
||||
|
||||
return new self(new BlobResourceContents($uri, $mimeType, base64_encode($content)), $annotations);
|
||||
}
|
||||
|
||||
public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null, ?Annotations $annotations = null): self
|
||||
{
|
||||
$content = file_get_contents($file->getPathname());
|
||||
if (false === $content) {
|
||||
throw new RuntimeException(\sprintf('Could not read file: "%s".', $file->getPathname()));
|
||||
}
|
||||
|
||||
return new self(new BlobResourceContents($uri, $explicitMimeType ?? mime_content_type($file->getPathname()), base64_encode($content)), $annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* type: 'resource',
|
||||
* resource: TextResourceContents|BlobResourceContents,
|
||||
* annotations?: Annotations,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [
|
||||
'type' => $this->type,
|
||||
'resource' => $this->resource,
|
||||
];
|
||||
if (null !== $this->annotations) {
|
||||
$data['annotations'] = $this->annotations;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private static function isTextMimeTypeHeuristic(string $mimeType): bool
|
||||
{
|
||||
return str_starts_with($mimeType, 'text/')
|
||||
|| \in_array(strtolower($mimeType), ['application/json', 'application/xml', 'application/javascript', 'application/yaml']);
|
||||
}
|
||||
}
|
||||
93
src/Schema/Content/ImageContent.php
Normal file
93
src/Schema/Content/ImageContent.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @phpstan-type ImageContentData array{
|
||||
* type: 'image',
|
||||
* data: string,
|
||||
* mimeType: string
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class ImageContent extends Content
|
||||
{
|
||||
/**
|
||||
* Create a new ImageContent instance.
|
||||
*
|
||||
* @param string $data Base64-encoded image data
|
||||
* @param string $mimeType The MIME type of the image
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $data,
|
||||
public readonly string $mimeType,
|
||||
) {
|
||||
parent::__construct('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ImageContentData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['data']) || !\is_string($data['data'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "data" in ImageContent data.');
|
||||
}
|
||||
if (!isset($data['mimeType']) || !\is_string($data['mimeType'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "mimeType" in ImageContent data.');
|
||||
}
|
||||
|
||||
return new self($data['data'], $data['mimeType']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ImageContent from a file path.
|
||||
*
|
||||
* @param string $path Path to the image file
|
||||
* @param string|null $mimeType Optional MIME type override
|
||||
*
|
||||
* @throws InvalidArgumentException If the file doesn't exist
|
||||
*/
|
||||
public static function fromFile(string $path, ?string $mimeType = null): self
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
throw new InvalidArgumentException(\sprintf('Image file not found: "%s".', $path));
|
||||
}
|
||||
|
||||
$data = base64_encode(file_get_contents($path));
|
||||
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'image/png';
|
||||
|
||||
return new self($data, $detectedMime);
|
||||
}
|
||||
|
||||
public static function fromString(string $data, string $mimeType): self
|
||||
{
|
||||
return new self(base64_encode($data), $mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the content to an array.
|
||||
*
|
||||
* @return ImageContentData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type,
|
||||
'data' => $this->data,
|
||||
'mimeType' => $this->mimeType,
|
||||
];
|
||||
}
|
||||
}
|
||||
88
src/Schema/Content/PromptMessage.php
Normal file
88
src/Schema/Content/PromptMessage.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
|
||||
/**
|
||||
* Describes a message returned as part of a prompt.
|
||||
*
|
||||
* @phpstan-import-type TextContentData from TextContent
|
||||
* @phpstan-import-type ImageContentData from ImageContent
|
||||
* @phpstan-import-type AudioContentData from AudioContent
|
||||
* @phpstan-import-type EmbeddedResourceData from EmbeddedResource
|
||||
*
|
||||
* @phpstan-type PromptMessageData array{
|
||||
* role: string,
|
||||
* content: TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData,
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class PromptMessage extends Content
|
||||
{
|
||||
/**
|
||||
* Create a new PromptMessage instance.
|
||||
*
|
||||
* @param Role $role The role of the message
|
||||
* @param TextContent|ImageContent|AudioContent|EmbeddedResource $content The content of the message
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Role $role,
|
||||
public readonly TextContent|ImageContent|AudioContent|EmbeddedResource $content,
|
||||
) {
|
||||
parent::__construct('prompt');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromptMessageData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['role']) || !\is_string($data['role'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "role" in PromptMessage data.');
|
||||
}
|
||||
if (!isset($data['content']) || !\is_array($data['content'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "content" in PromptMessage data.');
|
||||
}
|
||||
|
||||
$contentData = $data['content'];
|
||||
$contentType = $contentData['type'] ?? null;
|
||||
|
||||
$content = match ($contentType) {
|
||||
'text' => TextContent::fromArray($contentData),
|
||||
'image' => ImageContent::fromArray($contentData),
|
||||
'audio' => AudioContent::fromArray($contentData),
|
||||
'resource' => EmbeddedResource::fromArray($contentData),
|
||||
default => throw new InvalidArgumentException(\sprintf('Invalid content type "%s" for PromptMessage.', $contentType)),
|
||||
};
|
||||
|
||||
return new self(Role::from($data['role']), $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the message to an array.
|
||||
*
|
||||
* @return array{
|
||||
* role: string,
|
||||
* content: TextContent|ImageContent|AudioContent|EmbeddedResource
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'role' => $this->role->value,
|
||||
'content' => $this->content,
|
||||
];
|
||||
}
|
||||
}
|
||||
48
src/Schema/Content/ResourceContents.php
Normal file
48
src/Schema/Content/ResourceContents.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
/**
|
||||
* The contents of a specific resource or sub-resource.
|
||||
*
|
||||
* @phpstan-type ResourceContentsData = array{
|
||||
* uri: string,
|
||||
* mimeType?: string|null
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
abstract class ResourceContents implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param string $uri the URI of the resource or sub-resource
|
||||
* @param string|null $mimeType the MIME type of the resource or sub-resource
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $uri,
|
||||
public readonly ?string $mimeType = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ResourceContentsData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = ['uri' => $this->uri];
|
||||
if (null !== $this->mimeType) {
|
||||
$data['mimeType'] = $this->mimeType;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
72
src/Schema/Content/SamplingMessage.php
Normal file
72
src/Schema/Content/SamplingMessage.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
|
||||
/**
|
||||
* Describes a message issued to or received from an LLM API during sampling.
|
||||
*
|
||||
* @phpstan-type SamplingMessageData = array{
|
||||
* role: string,
|
||||
* content: TextContent|ImageContent|AudioContent
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class SamplingMessage extends Content
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Role $role,
|
||||
public readonly TextContent|ImageContent|AudioContent $content,
|
||||
) {
|
||||
parent::__construct('sampling');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SamplingMessageData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['role']) || !\is_string($data['role'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "role" in SamplingMessage data.');
|
||||
}
|
||||
if (!isset($data['content']) || !\is_array($data['content'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "content" in SamplingMessage data.');
|
||||
}
|
||||
|
||||
$role = Role::from($data['role']);
|
||||
$contentData = $data['content'];
|
||||
$contentType = $contentData['type'] ?? null;
|
||||
|
||||
$contentInstance = match ($contentType) {
|
||||
'text' => TextContent::fromArray($contentData),
|
||||
'image' => ImageContent::fromArray($contentData),
|
||||
'audio' => AudioContent::fromArray($contentData),
|
||||
default => throw new InvalidArgumentException(\sprintf('Invalid content type "%s" for SamplingMessage.', $contentType)),
|
||||
};
|
||||
|
||||
return new self($role, $contentInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SamplingMessageData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'role' => $this->role->value,
|
||||
'content' => $this->content,
|
||||
];
|
||||
}
|
||||
}
|
||||
96
src/Schema/Content/TextContent.php
Normal file
96
src/Schema/Content/TextContent.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Annotations;
|
||||
|
||||
/**
|
||||
* Represents text content in MCP.
|
||||
*
|
||||
* @phpstan-import-type AnnotationsData from Annotations
|
||||
*
|
||||
* @phpstan-type TextContentData array{
|
||||
* type: 'text',
|
||||
* text: string,
|
||||
* annotations?: AnnotationsData,
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class TextContent extends Content
|
||||
{
|
||||
/**
|
||||
* Create a new TextContent instance from any value.
|
||||
*
|
||||
* @param mixed $text The value to convert to text
|
||||
* @param ?Annotations $annotations Optional annotations describing the content
|
||||
*/
|
||||
public function __construct(
|
||||
public mixed $text,
|
||||
public readonly ?Annotations $annotations = null,
|
||||
) {
|
||||
$this->text = (\is_array($text) || \is_object($text))
|
||||
? json_encode($text, \JSON_PRETTY_PRINT) : (string) $text;
|
||||
|
||||
parent::__construct('text');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TextContentData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['text']) || !\is_string($data['text'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "text" in TextContent data.');
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['text'],
|
||||
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new TextContent with markdown formatted code.
|
||||
*
|
||||
* @param string $code The code to format
|
||||
* @param string $language The language for syntax highlighting
|
||||
*/
|
||||
public static function code(string $code, string $language = '', ?Annotations $annotations = null): self
|
||||
{
|
||||
return new self("```{$language}\n{$code}\n```", $annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the content to an array.
|
||||
*
|
||||
* @return array{
|
||||
* type: 'text',
|
||||
* text: string,
|
||||
* annotations?: Annotations,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$result = [
|
||||
'type' => 'text',
|
||||
'text' => $this->text,
|
||||
];
|
||||
|
||||
if (null !== $this->annotations) {
|
||||
$result['annotations'] = $this->annotations;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
67
src/Schema/Content/TextResourceContents.php
Normal file
67
src/Schema/Content/TextResourceContents.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?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\Schema\Content;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents text resource contents in MCP.
|
||||
*
|
||||
* @phpstan-type TextResourceContentsData array{
|
||||
* uri: string,
|
||||
* mimeType?: string|null,
|
||||
* text: string
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class TextResourceContents extends ResourceContents
|
||||
{
|
||||
/**
|
||||
* @param string $uri the URI of the resource or sub-resource
|
||||
* @param string|null $mimeType the MIME type of the resource or sub-resource
|
||||
* @param string $text The text of the item. This must only be set if the item can actually be represented as text (not binary data).
|
||||
*/
|
||||
public function __construct(
|
||||
string $uri,
|
||||
?string $mimeType,
|
||||
public readonly string $text,
|
||||
) {
|
||||
parent::__construct($uri, $mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TextResourceContentsData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (!isset($data['uri']) || !\is_string($data['uri'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "uri" for TextResourceContents.');
|
||||
}
|
||||
if (!isset($data['text']) || !\is_string($data['text'])) {
|
||||
throw new InvalidArgumentException('Missing or invalid "text" for TextResourceContents.');
|
||||
}
|
||||
|
||||
return new self($data['uri'], $data['mimeType'] ?? null, $data['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TextResourceContentsData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'text' => $this->text,
|
||||
...$this->jsonSerialize(),
|
||||
];
|
||||
}
|
||||
}
|
||||
32
src/Schema/Enum/LoggingLevel.php
Normal file
32
src/Schema/Enum/LoggingLevel.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?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\Schema\Enum;
|
||||
|
||||
/**
|
||||
* The severity of a log message.
|
||||
*
|
||||
* These map to syslog message severities, as specified in RFC-5424:
|
||||
* https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
enum LoggingLevel: string
|
||||
{
|
||||
case Debug = 'debug';
|
||||
case Info = 'info';
|
||||
case Notice = 'notice';
|
||||
case Warning = 'warning';
|
||||
case Error = 'error';
|
||||
case Critical = 'critical';
|
||||
case Alert = 'alert';
|
||||
case Emergency = 'emergency';
|
||||
}
|
||||
23
src/Schema/Enum/Role.php
Normal file
23
src/Schema/Enum/Role.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?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\Schema\Enum;
|
||||
|
||||
/**
|
||||
* The sender or recipient of messages and data in a conversation.
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
enum Role: string
|
||||
{
|
||||
case User = 'user';
|
||||
case Assistant = 'assistant';
|
||||
}
|
||||
60
src/Schema/Implementation.php
Normal file
60
src/Schema/Implementation.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?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\Schema;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Describes the name and version of an MCP implementation.
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class Implementation implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name = 'app',
|
||||
public readonly string $version = 'dev',
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* name: string,
|
||||
* version: string,
|
||||
* } $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (empty($data['name']) || !\is_string($data['name'])) {
|
||||
throw new InvalidArgumentException('Invalid or missing "name" in Implementation data.');
|
||||
}
|
||||
if (empty($data['version']) || !\is_string($data['version'])) {
|
||||
throw new InvalidArgumentException('Invalid or missing "version" in Implementation data.');
|
||||
}
|
||||
|
||||
return new self($data['name'], $data['version']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* name: string,
|
||||
* version: string,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'version' => $this->version,
|
||||
];
|
||||
}
|
||||
}
|
||||
164
src/Schema/JsonRpc/BatchRequest.php
Normal file
164
src/Schema/JsonRpc/BatchRequest.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?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\Schema\JsonRpc;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A JSON-RPC batch request, as described in https://www.jsonrpc.org/specification#batch.
|
||||
*
|
||||
* @phpstan-import-type RequestData from Request
|
||||
* @phpstan-import-type NotificationData from Notification
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class BatchRequest implements MessageInterface, \Countable
|
||||
{
|
||||
/**
|
||||
* Create a new JSON-RPC 2.0 batch of requests/notifications.
|
||||
*
|
||||
* @param array<Request|Notification> $items
|
||||
*/
|
||||
public function __construct(public readonly array $items)
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
if (!($item instanceof Request || $item instanceof Notification)) {
|
||||
throw new InvalidArgumentException('All items in BatchRequest must be instances of Request or Notification.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<RequestData|NotificationData> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (empty($data)) {
|
||||
throw new InvalidArgumentException('BatchRequest data array must not be empty.');
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($data as $itemData) {
|
||||
if (!\is_array($itemData)) {
|
||||
throw new InvalidArgumentException('BatchRequest item data must be an array.');
|
||||
}
|
||||
if (isset($itemData['id'])) {
|
||||
$items[] = Request::fromArray($itemData);
|
||||
} elseif (isset($itemData['method'])) {
|
||||
$items[] = Notification::fromArray($itemData);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid item in BatchRequest data: missing "method" or "id".');
|
||||
}
|
||||
}
|
||||
|
||||
return new self($items);
|
||||
}
|
||||
|
||||
public function getId(): string|int
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Request) {
|
||||
return $item->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this batch has any requests.
|
||||
*/
|
||||
public function hasRequests(): bool
|
||||
{
|
||||
$hasRequests = false;
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Request) {
|
||||
$hasRequests = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this batch has any notifications.
|
||||
*/
|
||||
public function hasNotifications(): bool
|
||||
{
|
||||
$hasNotifications = false;
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Notification) {
|
||||
$hasNotifications = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all requests in this batch.
|
||||
*
|
||||
* @return array<Request>
|
||||
*/
|
||||
public function getRequests(): array
|
||||
{
|
||||
return array_filter($this->items, fn ($item) => $item instanceof Request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications in this batch.
|
||||
*
|
||||
* @return array<Notification>
|
||||
*/
|
||||
public function getNotifications(): array
|
||||
{
|
||||
return array_filter($this->items, fn ($item) => $item instanceof Notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all elements in this batch.
|
||||
*
|
||||
* @return array<Request|Notification>
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the total number of elements in this batch.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->items);
|
||||
}
|
||||
|
||||
public function nRequests(): int
|
||||
{
|
||||
return \count($this->getRequests());
|
||||
}
|
||||
|
||||
public function nNotifications(): int
|
||||
{
|
||||
return \count($this->getNotifications());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Request|Notification>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
138
src/Schema/JsonRpc/BatchResponse.php
Normal file
138
src/Schema/JsonRpc/BatchResponse.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?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\Schema\JsonRpc;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A JSON-RPC batch response, as described in https://www.jsonrpc.org/specification#batch.
|
||||
*
|
||||
* @phpstan-import-type ResponseData from Response
|
||||
* @phpstan-import-type ErrorData from Error
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class BatchResponse implements MessageInterface, \Countable
|
||||
{
|
||||
/**
|
||||
* @param array<Response|Error> $items the individual responses/errors in this batch
|
||||
*/
|
||||
public function __construct(
|
||||
public array $items,
|
||||
) {
|
||||
foreach ($items as $item) {
|
||||
if (!($item instanceof Response || $item instanceof Error)) {
|
||||
throw new InvalidArgumentException('All items in BatchResponse must be instances of Response or Error.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<ErrorData|ResponseData> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (empty($data)) {
|
||||
throw new InvalidArgumentException('BatchResponse data array must not be empty.');
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($data as $itemData) {
|
||||
if (!\is_array($itemData)) {
|
||||
throw new InvalidArgumentException('BatchResponse item data must be an array.');
|
||||
}
|
||||
if (isset($itemData['id'])) {
|
||||
$items[] = Response::fromArray($itemData);
|
||||
} elseif (isset($itemData['error'])) {
|
||||
$items[] = Error::fromArray($itemData);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid item in BatchResponse data: missing "id" or "error".');
|
||||
}
|
||||
}
|
||||
|
||||
return new self($items);
|
||||
}
|
||||
|
||||
public function getId(): string|int
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Response) {
|
||||
return $item->getId();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('BatchResponse does not contain any Response items with an "id".');
|
||||
}
|
||||
|
||||
public function hasResponses(): bool
|
||||
{
|
||||
$hasResponses = false;
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Response) {
|
||||
$hasResponses = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasResponses;
|
||||
}
|
||||
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
$hasErrors = false;
|
||||
foreach ($this->items as $item) {
|
||||
if ($item instanceof Error) {
|
||||
$hasErrors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response[]
|
||||
*/
|
||||
public function getResponses(): array
|
||||
{
|
||||
return array_filter($this->items, fn ($item) => $item instanceof Response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return array_filter($this->items, fn ($item) => $item instanceof Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]|Response[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error[]|Response[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue