Skip to content

Commit 9a7ad2d

Browse files
committed
Introducing schema component
1 parent 63dc146 commit 9a7ad2d

File tree

82 files changed

+5451
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+5451
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
}
1919
],
2020
"require": {
21+
"ext-fileinfo": "*",
2122
"php": "^8.2",
2223
"psr/log": "^1.0 || ^2.0 || ^3.0",
2324
"symfony/uid": "^6.4 || ^7.0"

src/Exception/RuntimeException.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Exception;
13+
14+
/**
15+
* @author Christopher Hertel <[email protected]>
16+
*/
17+
class RuntimeException extends \RuntimeException implements ExceptionInterface
18+
{
19+
}

src/Schema/Annotations.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Mcp\Schema;
4+
5+
use Mcp\Schema\Enum\Role;
6+
use JsonSerializable;
7+
8+
/**
9+
* Optional annotations for the client. The client can use annotations
10+
* to inform how objects are used or displayed.
11+
*
12+
* @author Kyrian Obikwelu <[email protected]>
13+
*/
14+
class Annotations implements JsonSerializable
15+
{
16+
/**
17+
* @param Role[]|null $audience Describes who the intended customer of this object or data is.
18+
*
19+
* It can include multiple entries to indicate content useful for multiple audiences (e.g., `[Role::User, Role::Assistant]`).
20+
*
21+
* @param float|null $priority Describes how important this data is for operating the server.
22+
*
23+
* A value of 1 means "most important," and indicates that the data is
24+
* effectively required, while 0 means "least important," and indicates that
25+
* the data is entirely optional.
26+
*/
27+
public function __construct(
28+
public readonly ?array $audience = null,
29+
public readonly ?float $priority = null
30+
) {
31+
if ($this->priority !== null && ($this->priority < 0 || $this->priority > 1)) {
32+
throw new \InvalidArgumentException("Annotation priority must be between 0 and 1.");
33+
}
34+
if ($this->audience !== null) {
35+
foreach ($this->audience as $role) {
36+
if (!($role instanceof Role)) {
37+
throw new \InvalidArgumentException("All audience members must be instances of Role enum.");
38+
}
39+
}
40+
}
41+
}
42+
43+
/**
44+
* @param Role[]|null $audience Describes who the intended customer of this object or data is.
45+
*
46+
* It can include multiple entries to indicate content useful for multiple audiences (e.g., `[Role::User, Role::Assistant]`).
47+
*
48+
* @param float|null $priority Describes how important this data is for operating the server.
49+
*
50+
* A value of 1 means "most important," and indicates that the data is
51+
* effectively required, while 0 means "least important," and indicates that
52+
* the data is entirely optional.
53+
*/
54+
public static function make(array $audience = null, float $priority = null): static
55+
{
56+
return new static($audience, $priority);
57+
}
58+
59+
public function toArray(): array
60+
{
61+
$data = [];
62+
if ($this->audience !== null) {
63+
$data['audience'] = array_map(fn (Role $r) => $r->value, $this->audience);
64+
}
65+
if ($this->priority !== null) {
66+
$data['priority'] = $this->priority;
67+
}
68+
return $data;
69+
}
70+
71+
public static function fromArray(array $data): static
72+
{
73+
$audience = null;
74+
if (isset($data['audience']) && is_array($data['audience'])) {
75+
$audience = array_map(fn (string $r) => Role::from($r), $data['audience']);
76+
}
77+
return new static(
78+
$audience,
79+
isset($data['priority']) ? (float)$data['priority'] : null
80+
);
81+
}
82+
83+
public function jsonSerialize(): array
84+
{
85+
return $this->toArray();
86+
}
87+
}

src/Schema/ClientCapabilities.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Mcp\Schema;
4+
5+
use JsonSerializable;
6+
7+
/**
8+
* 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.
9+
*
10+
* @author Kyrian Obikwelu <[email protected]>
11+
*/
12+
class ClientCapabilities implements JsonSerializable
13+
{
14+
public function __construct(
15+
public readonly ?bool $roots = false,
16+
public readonly ?bool $rootsListChanged = null,
17+
public readonly ?bool $sampling = null,
18+
public readonly ?array $experimental = null
19+
) {}
20+
21+
public static function make(?bool $roots = false, ?bool $rootsListChanged = null, ?bool $sampling = null, ?array $experimental = null): static
22+
{
23+
return new static($roots, $rootsListChanged, $sampling, $experimental);
24+
}
25+
26+
public function toArray(): array
27+
{
28+
$data = [];
29+
if ($this->roots || $this->rootsListChanged) {
30+
$data['roots'] = new \stdClass();
31+
if ($this->rootsListChanged) {
32+
$data['roots']->listChanged = $this->rootsListChanged;
33+
}
34+
}
35+
36+
if ($this->sampling) {
37+
$data['sampling'] = new \stdClass();
38+
}
39+
40+
if ($this->experimental) {
41+
$data['experimental'] = (object) $this->experimental;
42+
}
43+
44+
return $data;
45+
}
46+
47+
public static function fromArray(array $data): static
48+
{
49+
$rootsEnabled = isset($data['roots']);
50+
$rootsListChanged = null;
51+
if ($rootsEnabled) {
52+
if (is_array($data['roots']) && array_key_exists('listChanged', $data['roots'])) {
53+
$rootsListChanged = (bool) $data['roots']['listChanged'];
54+
} elseif (is_object($data['roots']) && property_exists($data['roots'], 'listChanged')) {
55+
$rootsListChanged = (bool) $data['roots']->listChanged;
56+
}
57+
}
58+
59+
$sampling = null;
60+
if (isset($data['sampling'])) {
61+
$sampling = true;
62+
}
63+
64+
return new static(
65+
$rootsListChanged,
66+
$sampling,
67+
$data['experimental'] ?? null
68+
);
69+
}
70+
71+
public function jsonSerialize(): array
72+
{
73+
return $this->toArray();
74+
}
75+
}

src/Schema/Constants.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Mcp\Schema;
4+
5+
/**
6+
* @author Kyrian Obikwelu <[email protected]>
7+
*/
8+
final class Constants
9+
{
10+
public const LATEST_PROTOCOL_VERSION = "2025-03-26";
11+
public const JSONRPC_VERSION = "2.0";
12+
13+
public const PARSE_ERROR = -32700;
14+
public const INVALID_REQUEST = -32600;
15+
public const METHOD_NOT_FOUND = -32601;
16+
public const INVALID_PARAMS = -32602;
17+
public const INTERNAL_ERROR = -32603;
18+
19+
public const SERVER_ERROR = -32000;
20+
private function __construct() {}
21+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema\Content;
13+
14+
use Mcp\Schema\Annotations;
15+
16+
/**
17+
* Represents audio content in MCP.
18+
*
19+
* @author Kyrian Obikwelu <[email protected]>
20+
*/
21+
class AudioContent extends Content
22+
{
23+
public function __construct(
24+
public readonly string $data,
25+
public readonly string $mimeType,
26+
public readonly ?Annotations $annotations = null,
27+
) {
28+
parent::__construct('audio');
29+
}
30+
31+
public static function make(string $data, string $mimeType, ?Annotations $annotations = null): static
32+
{
33+
return new static($data, $mimeType, $annotations);
34+
}
35+
36+
/**
37+
* Convert the content to an array.
38+
*
39+
* @return array{type: string, data: string, mimeType: string, annotations?: array}
40+
*/
41+
public function toArray(): array
42+
{
43+
$result = [
44+
'type' => 'audio',
45+
'data' => $this->data,
46+
'mimeType' => $this->mimeType,
47+
];
48+
49+
if (null !== $this->annotations) {
50+
$result['annotations'] = $this->annotations->toArray();
51+
}
52+
53+
return $result;
54+
}
55+
56+
public static function fromArray(array $data): static
57+
{
58+
if (!isset($data['data']) || !isset($data['mimeType'])) {
59+
throw new \InvalidArgumentException("Invalid or missing 'data' or 'mimeType' in AudioContent data.");
60+
}
61+
62+
return new static($data['data'], $data['mimeType'], $data['annotations'] ?? null);
63+
}
64+
65+
/**
66+
* Create a new AudioContent from a file path.
67+
*
68+
* @param string $path Path to the audio file
69+
* @param string|null $mimeType Optional MIME type override
70+
* @param ?Annotations $annotations Optional annotations describing the content
71+
*
72+
* @throws \InvalidArgumentException If the file doesn't exist
73+
*/
74+
public static function fromFile(string $path, ?string $mimeType = null, ?Annotations $annotations = null): static
75+
{
76+
if (!file_exists($path)) {
77+
throw new \InvalidArgumentException("Audio file not found: {$path}");
78+
}
79+
80+
$content = file_get_contents($path);
81+
if (false === $content) {
82+
throw new \RuntimeException("Could not read audio file: {$path}");
83+
}
84+
$data = base64_encode($content);
85+
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'application/octet-stream';
86+
87+
return new static($data, $detectedMime, $annotations);
88+
}
89+
90+
/**
91+
* Create a new AudioContent from a string.
92+
*
93+
* @param string $data The audio data
94+
* @param string $mimeType MIME type of the audio
95+
* @param ?Annotations $annotations Optional annotations describing the content
96+
*/
97+
public static function fromString(string $data, string $mimeType, ?Annotations $annotations = null): static
98+
{
99+
return new static(base64_encode($data), $mimeType, $annotations);
100+
}
101+
102+
public function jsonSerialize(): array
103+
{
104+
return $this->toArray();
105+
}
106+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Mcp\Schema\Content;
4+
5+
/**
6+
* Represents blob resource contents in MCP.
7+
*
8+
* @author Kyrian Obikwelu <[email protected]>
9+
*/
10+
class BlobResourceContents extends ResourceContents
11+
{
12+
/**
13+
* @param string $uri The URI of the resource or sub-resource.
14+
* @param string|null $mimeType The MIME type of the resource or sub-resource.
15+
* @param string $blob A base64-encoded string representing the binary data of the item.
16+
*/
17+
public function __construct(
18+
string $uri,
19+
?string $mimeType,
20+
public readonly string $blob
21+
) {
22+
parent::__construct($uri, $mimeType);
23+
}
24+
25+
public static function make(string $uri, ?string $mimeType, string $blob): static
26+
{
27+
return new static($uri, $mimeType, $blob);
28+
}
29+
30+
public function toArray(): array
31+
{
32+
return [
33+
'blob' => $this->blob,
34+
...parent::toArray(),
35+
];
36+
}
37+
38+
public static function fromArray(array $data): static
39+
{
40+
if (!isset($data['uri']) || !is_string($data['uri'])) {
41+
throw new \InvalidArgumentException("Missing or invalid uri for BlobResourceContents");
42+
}
43+
if (!isset($data['blob']) || !is_string($data['blob'])) {
44+
throw new \InvalidArgumentException("Missing or invalid blob for BlobResourceContents");
45+
}
46+
47+
return new static($data['uri'], $data['mimeType'] ?? null, $data['blob']);
48+
}
49+
50+
public static function fromStream(string $uri, $stream, string $mimeType): static
51+
{
52+
$blob = stream_get_contents($stream);
53+
return new static($uri, $mimeType, base64_encode($blob));
54+
}
55+
56+
public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null): static
57+
{
58+
$mimeType = $explicitMimeType ?? mime_content_type($file->getPathname());
59+
$blob = file_get_contents($file->getPathname());
60+
return new static($uri, $mimeType, base64_encode($blob));
61+
}
62+
}

0 commit comments

Comments
 (0)