Introduce schema component

This commit is contained in:
Christopher Hertel 2025-08-01 16:41:16 +02:00
commit 6025f890f9
No known key found for this signature in database
GPG key ID: 111F6BFF7B6B5AA3
76 changed files with 5419 additions and 0 deletions

View file

@ -19,6 +19,7 @@
],
"require": {
"php": "^8.1",
"ext-fileinfo": "*",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/uid": "^6.4 || ^7.0"
},

View 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
{
}

View 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;
}
}

View 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
View 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;
}

View 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;
}
}

View 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(),
];
}
}

View 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,
) {
}
}

View 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']);
}
}

View 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,
];
}
}

View 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,
];
}
}

View 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;
}
}

View 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,
];
}
}

View 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;
}
}

View 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(),
];
}
}

View 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
View 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';
}

View 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,
];
}
}

View 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;
}
}

View 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