Skip to content

Commit 0c6d7bf

Browse files
committed
Introducing schema component
1 parent 88c8067 commit 0c6d7bf

File tree

76 files changed

+5421
-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.

76 files changed

+5421
-0
lines changed

composer.json

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

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

src/Schema/ClientCapabilities.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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;
13+
14+
/**
15+
* Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set:
16+
* any client can define its own, additional capabilities.
17+
*
18+
* @author Kyrian Obikwelu <[email protected]>
19+
*/
20+
class ClientCapabilities implements \JsonSerializable
21+
{
22+
/**
23+
* @param array<string, mixed> $experimental
24+
*/
25+
public function __construct(
26+
public readonly ?bool $roots = false,
27+
public readonly ?bool $rootsListChanged = null,
28+
public readonly ?bool $sampling = null,
29+
public readonly ?array $experimental = null,
30+
) {
31+
}
32+
33+
/**
34+
* @param array{
35+
* roots?: array{
36+
* listChanged?: bool,
37+
* },
38+
* sampling?: bool,
39+
* experimental?: array<string, mixed>,
40+
* } $data
41+
*/
42+
public static function fromArray(array $data): self
43+
{
44+
$rootsEnabled = isset($data['roots']);
45+
$rootsListChanged = null;
46+
if ($rootsEnabled) {
47+
if (\is_array($data['roots']) && \array_key_exists('listChanged', $data['roots'])) {
48+
$rootsListChanged = (bool) $data['roots']['listChanged'];
49+
} elseif (\is_object($data['roots']) && property_exists($data['roots'], 'listChanged')) {
50+
$rootsListChanged = (bool) $data['roots']->listChanged;
51+
}
52+
}
53+
54+
$sampling = null;
55+
if (isset($data['sampling'])) {
56+
$sampling = true;
57+
}
58+
59+
return new self(
60+
$rootsEnabled,
61+
$rootsListChanged,
62+
$sampling,
63+
$data['experimental'] ?? null
64+
);
65+
}
66+
67+
/**
68+
* @return array{
69+
* roots?: object,
70+
* sampling?: object,
71+
* experimental?: object,
72+
* }
73+
*/
74+
public function jsonSerialize(): array
75+
{
76+
$data = [];
77+
if ($this->roots || $this->rootsListChanged) {
78+
$data['roots'] = new \stdClass();
79+
if ($this->rootsListChanged) {
80+
$data['roots']->listChanged = $this->rootsListChanged;
81+
}
82+
}
83+
84+
if ($this->sampling) {
85+
$data['sampling'] = new \stdClass();
86+
}
87+
88+
if ($this->experimental) {
89+
$data['experimental'] = (object) $this->experimental;
90+
}
91+
92+
return $data;
93+
}
94+
}

src/Schema/Constants.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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;
13+
14+
/**
15+
* @author Kyrian Obikwelu <[email protected]>
16+
*/
17+
interface Constants
18+
{
19+
public const LATEST_PROTOCOL_VERSION = '2025-03-26';
20+
public const JSONRPC_VERSION = '2.0';
21+
22+
public const PARSE_ERROR = -32700;
23+
public const INVALID_REQUEST = -32600;
24+
public const METHOD_NOT_FOUND = -32601;
25+
public const INVALID_PARAMS = -32602;
26+
public const INTERNAL_ERROR = -32603;
27+
28+
public const SERVER_ERROR = -32000;
29+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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\Exception\InvalidArgumentException;
15+
use Mcp\Exception\RuntimeException;
16+
use Mcp\Schema\Annotations;
17+
18+
/**
19+
* Represents audio content in MCP.
20+
*
21+
* @phpstan-import-type AnnotationsData from Annotations
22+
*
23+
* @phpstan-type AudioContentData = array{
24+
* type: 'audio',
25+
* data: string,
26+
* mimeType: string,
27+
* annotations?: AnnotationsData,
28+
* }
29+
*
30+
* @author Kyrian Obikwelu <[email protected]>
31+
*/
32+
class AudioContent extends Content
33+
{
34+
public function __construct(
35+
public readonly string $data,
36+
public readonly string $mimeType,
37+
public readonly ?Annotations $annotations = null,
38+
) {
39+
parent::__construct('audio');
40+
}
41+
42+
/**
43+
* @param AudioContentData $data
44+
*/
45+
public static function fromArray(array $data): self
46+
{
47+
if (!isset($data['data']) || !isset($data['mimeType'])) {
48+
throw new InvalidArgumentException('Invalid or missing "data" or "mimeType" in AudioContent data.');
49+
}
50+
51+
return new self(
52+
$data['data'],
53+
$data['mimeType'],
54+
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
55+
);
56+
}
57+
58+
/**
59+
* Create a new AudioContent from a file path.
60+
*
61+
* @param string $path Path to the audio file
62+
* @param string|null $mimeType Optional MIME type override
63+
* @param ?Annotations $annotations Optional annotations describing the content
64+
*
65+
* @throws InvalidArgumentException If the file doesn't exist
66+
*/
67+
public static function fromFile(string $path, ?string $mimeType = null, ?Annotations $annotations = null): self
68+
{
69+
if (!file_exists($path)) {
70+
throw new InvalidArgumentException(\sprintf('Audio file not found: "%s".', $path));
71+
}
72+
73+
$content = file_get_contents($path);
74+
if (false === $content) {
75+
throw new RuntimeException(\sprintf('Could not read audio file: "%s".', $path));
76+
}
77+
$data = base64_encode($content);
78+
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'application/octet-stream';
79+
80+
return new self($data, $detectedMime, $annotations);
81+
}
82+
83+
/**
84+
* Create a new AudioContent from a string.
85+
*
86+
* @param string $data The audio data
87+
* @param string $mimeType MIME type of the audio
88+
* @param ?Annotations $annotations Optional annotations describing the content
89+
*/
90+
public static function fromString(string $data, string $mimeType, ?Annotations $annotations = null): self
91+
{
92+
return new self(base64_encode($data), $mimeType, $annotations);
93+
}
94+
95+
/**
96+
* @return array{
97+
* type: 'audio',
98+
* data: string,
99+
* mimeType: string,
100+
* annotations?: Annotations,
101+
* }
102+
*/
103+
public function jsonSerialize(): array
104+
{
105+
$result = [
106+
'type' => 'audio',
107+
'data' => $this->data,
108+
'mimeType' => $this->mimeType,
109+
];
110+
111+
if (null !== $this->annotations) {
112+
$result['annotations'] = $this->annotations;
113+
}
114+
115+
return $result;
116+
}
117+
}

0 commit comments

Comments
 (0)