Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/JsonRpc/MessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ private function createMessage(array $data): MessageInterface
throw new InvalidInputMessageException('Invalid JSON-RPC message: missing "method", "result", or "error" field.');
}

if (!\is_string($data['method'])) {
throw new InvalidInputMessageException('Invalid JSON-RPC message: "method" must be a string.');
}

$messageClass = $this->findMessageClassByMethod($data['method']);

return $messageClass::fromArray($data);
Expand Down
33 changes: 32 additions & 1 deletion src/Schema/Annotations.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,20 @@ public static function fromArray(array $data): self
{
$audience = null;
if (isset($data['audience']) && \is_array($data['audience'])) {
$audience = array_map(static fn (string $r) => Role::from($r), $data['audience']);
$audience = array_map(
static function (mixed $role): Role {
if (!\is_string($role) || null === $case = Role::tryFrom($role)) {
throw new InvalidArgumentException('Each entry in "audience" must be a valid role.');
}

return $case;
},
$data['audience'],
);
}

if (isset($data['priority']) && !\is_float($data['priority']) && !\is_int($data['priority'])) {
throw new InvalidArgumentException('Invalid "priority" in Annotations data; expected a number.');
}

return new self(
Expand All @@ -69,6 +82,24 @@ public static function fromArray(array $data): self
);
}

/**
* Hydrates an optional "annotations" field, rejecting a value that is present but not an object.
*
* @param string $context the surrounding schema type, used for the error message
*/
public static function tryFromArray(mixed $data, string $context): ?self
{
if (null === $data) {
return null;
}

if (!\is_array($data)) {
throw new InvalidArgumentException(\sprintf('Invalid "annotations" in %s data; expected an array.', $context));
}

return self::fromArray($data);
}

/**
* @return AnnotationsData
*/
Expand Down
9 changes: 6 additions & 3 deletions src/Schema/Content/AudioContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ public function __construct(
*/
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.');
if (!isset($data['data']) || !\is_string($data['data'])) {
throw new InvalidArgumentException('Missing or invalid "data" in AudioContent data.');
}
if (!isset($data['mimeType']) || !\is_string($data['mimeType'])) {
throw new InvalidArgumentException('Missing or invalid "mimeType" in AudioContent data.');
}

return new self(
$data['data'],
$data['mimeType'],
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
Annotations::tryFromArray($data['annotations'] ?? null, 'AudioContent')
);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Schema/Content/BlobResourceContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public static function fromArray(array $data): self
throw new InvalidArgumentException('Missing or invalid "blob" for BlobResourceContents.');
}

if (isset($data['mimeType']) && !\is_string($data['mimeType'])) {
throw new InvalidArgumentException('Invalid "mimeType" for BlobResourceContents.');
}
if (isset($data['_meta']) && !\is_array($data['_meta'])) {
throw new InvalidArgumentException('Invalid "_meta" for BlobResourceContents.');
}

return new self($data['uri'], $data['mimeType'] ?? null, $data['blob'], $data['_meta'] ?? null);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/EmbeddedResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static function fromArray(array $data): self

return new self(
$resourceInstance,
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
Annotations::tryFromArray($data['annotations'] ?? null, 'EmbeddedResource'),
);
}

Expand Down
9 changes: 8 additions & 1 deletion src/Schema/Content/PromptMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public static function fromArray(array $data): self

$contentData = $data['content'];
$contentType = $contentData['type'] ?? null;
if (!\is_string($contentType)) {
throw new InvalidArgumentException('Missing or invalid content "type" for PromptMessage.');
}

$content = match ($contentType) {
'text' => TextContent::fromArray($contentData),
Expand All @@ -67,7 +70,11 @@ public static function fromArray(array $data): self
default => throw new InvalidArgumentException(\sprintf('Invalid content type "%s" for PromptMessage.', $contentType)),
};

return new self(Role::from($data['role']), $content);
if (null === $role = Role::tryFrom($data['role'])) {
throw new InvalidArgumentException(\sprintf('Invalid "role" value "%s" in PromptMessage data.', $data['role']));
}

return new self($role, $content);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/Schema/Content/SamplingMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ public static function fromArray(array $data): self
throw new InvalidArgumentException('Missing or invalid "content" in SamplingMessage data.');
}

$role = Role::from($data['role']);
if (null === $role = Role::tryFrom($data['role'])) {
throw new InvalidArgumentException(\sprintf('Invalid "role" value "%s" in SamplingMessage data.', $data['role']));
}

$contentData = $data['content'];
$contentType = $contentData['type'] ?? null;
if (!\is_string($contentType)) {
throw new InvalidArgumentException('Missing or invalid content "type" for SamplingMessage.');
}

$contentInstance = match ($contentType) {
'text' => TextContent::fromArray($contentData),
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/TextContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static function fromArray(array $data): self

return new self(
$data['text'],
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
Annotations::tryFromArray($data['annotations'] ?? null, 'TextContent')
);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Schema/Content/TextResourceContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public static function fromArray(array $data): self
throw new InvalidArgumentException('Missing or invalid "text" for TextResourceContents.');
}

if (isset($data['mimeType']) && !\is_string($data['mimeType'])) {
throw new InvalidArgumentException('Invalid "mimeType" for TextResourceContents.');
}
if (isset($data['_meta']) && !\is_array($data['_meta'])) {
throw new InvalidArgumentException('Invalid "_meta" for TextResourceContents.');
}

return new self($data['uri'], $data['mimeType'] ?? null, $data['text'], $data['_meta'] ?? null);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Schema/Elicitation/ElicitationSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public function __construct(
}

foreach ($required as $name) {
if (!\is_string($name)) {
throw new InvalidArgumentException('Each entry in "required" must be a string.');
}
if (!\array_key_exists($name, $properties)) {
throw new InvalidArgumentException(\sprintf('Required property "%s" is not defined in properties.', $name));
}
Expand Down Expand Up @@ -68,6 +71,10 @@ public static function fromArray(array $data): self
$properties[$name] = self::createSchemaDefinition($propertyData);
}

if (isset($data['required']) && !\is_array($data['required'])) {
throw new InvalidArgumentException('Invalid "required" for elicitation schema; expected an array.');
}

return new self(
properties: $properties,
required: $data['required'] ?? [],
Expand Down
15 changes: 15 additions & 0 deletions src/Schema/Extension/Apps/UiResourceContentMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Mcp\Schema\Extension\Apps;

use Mcp\Exception\InvalidArgumentException;

/**
* Metadata for the _meta.ui field on resource content in a resources/read response.
*
Expand Down Expand Up @@ -43,6 +45,19 @@ public function __construct(
*/
public static function fromArray(array $data): self
{
if (isset($data['csp']) && !\is_array($data['csp'])) {
throw new InvalidArgumentException('Invalid "csp" in UiResourceContentMeta data; expected an array.');
}
if (isset($data['permissions']) && !\is_array($data['permissions'])) {
throw new InvalidArgumentException('Invalid "permissions" in UiResourceContentMeta data; expected an array.');
}
if (isset($data['domain']) && !\is_string($data['domain'])) {
throw new InvalidArgumentException('Invalid "domain" in UiResourceContentMeta data.');
}
if (isset($data['prefersBorder']) && !\is_bool($data['prefersBorder'])) {
throw new InvalidArgumentException('Invalid "prefersBorder" in UiResourceContentMeta data.');
}

return new self(
csp: isset($data['csp']) ? UiResourceCsp::fromArray($data['csp']) : null,
permissions: isset($data['permissions']) ? UiResourcePermissions::fromArray($data['permissions']) : null,
Expand Down
8 changes: 8 additions & 0 deletions src/Schema/Extension/Apps/UiResourceCsp.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Mcp\Schema\Extension\Apps;

use Mcp\Exception\InvalidArgumentException;

/**
* Content Security Policy configuration for MCP App resources.
*
Expand Down Expand Up @@ -47,6 +49,12 @@ public function __construct(
*/
public static function fromArray(array $data): self
{
foreach (['connectDomains', 'resourceDomains', 'frameDomains', 'baseUriDomains'] as $key) {
if (isset($data[$key]) && !\is_array($data[$key])) {
throw new InvalidArgumentException(\sprintf('Invalid "%s" in UiResourceCsp data; expected an array.', $key));
}
}

return new self(
connectDomains: $data['connectDomains'] ?? null,
resourceDomains: $data['resourceDomains'] ?? null,
Expand Down
20 changes: 19 additions & 1 deletion src/Schema/Extension/Apps/UiToolMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Mcp\Schema\Extension\Apps;

use Mcp\Exception\InvalidArgumentException;

/**
* Metadata for the _meta.ui field on a Tool, linking it to a UI resource.
*
Expand Down Expand Up @@ -39,9 +41,25 @@ public function __construct(
*/
public static function fromArray(array $data): self
{
if (isset($data['resourceUri']) && !\is_string($data['resourceUri'])) {
throw new InvalidArgumentException('Invalid "resourceUri" in UiToolMeta data.');
}
if (isset($data['visibility']) && !\is_array($data['visibility'])) {
throw new InvalidArgumentException('Invalid "visibility" in UiToolMeta data; expected an array.');
}

return new self(
resourceUri: $data['resourceUri'] ?? null,
visibility: isset($data['visibility']) ? array_map(ToolVisibility::from(...), $data['visibility']) : null,
visibility: isset($data['visibility']) ? array_map(
static function (mixed $entry): ToolVisibility {
if (!\is_string($entry) || null === $case = ToolVisibility::tryFrom($entry)) {
throw new InvalidArgumentException('Each entry in "visibility" of UiToolMeta data must be a valid tool visibility.');
}

return $case;
},
$data['visibility'],
) : null,
);
}

Expand Down
30 changes: 29 additions & 1 deletion src/Schema/Icon.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,36 @@ public static function fromArray(array $data): self
if (empty($data['src']) || !\is_string($data['src'])) {
throw new InvalidArgumentException('Invalid or missing "src" in Icon data.');
}
if (isset($data['mimeType']) && !\is_string($data['mimeType'])) {
throw new InvalidArgumentException('Invalid "mimeType" in Icon data.');
}
if (isset($data['sizes']) && !\is_array($data['sizes'])) {
throw new InvalidArgumentException('Invalid "sizes" in Icon data.');
}

return new self($data['src'], $data['mimeType'] ?? null, $data['sizes'] ?? null);
}

/**
* Hydrates an "icons" list, rejecting entries that are not objects.
*
* @param array<mixed> $icons
* @param string $context the surrounding schema type, used for the error message
*
* @return self[]
*/
public static function listFromArray(array $icons, string $context): array
{
return array_map(
static function (mixed $icon) use ($context): self {
if (!\is_array($icon)) {
throw new InvalidArgumentException(\sprintf('Each entry in "icons" of %s data must be an array.', $context));
}

return new self($data['src'], $data['mimeTypes'] ?? null, $data['sizes'] ?? null);
return self::fromArray($icon);
},
$icons,
);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion src/Schema/Implementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ public static function fromArray(array $data): self
throw new InvalidArgumentException('Invalid "icons" in Implementation data; expected an array.');
}

$data['icons'] = array_map(Icon::fromArray(...), $data['icons']);
$data['icons'] = Icon::listFromArray($data['icons'], 'Implementation');
}

if (isset($data['description']) && !\is_string($data['description'])) {
throw new InvalidArgumentException('Invalid "description" in Implementation data.');
}
if (isset($data['websiteUrl']) && !\is_string($data['websiteUrl'])) {
throw new InvalidArgumentException('Invalid "websiteUrl" in Implementation data.');
}

return new self(
Expand Down
29 changes: 26 additions & 3 deletions src/Schema/ModelPreferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Mcp\Schema;

use Mcp\Exception\InvalidArgumentException;

/**
* The server's preferences for model selection, requested of the client during sampling.
*
Expand Down Expand Up @@ -61,14 +63,35 @@ public function __construct(
*/
public static function fromArray(array $preferences): self
{
if (isset($preferences['hints']) && !\is_array($preferences['hints'])) {
throw new InvalidArgumentException('Invalid "hints" in ModelPreferences data.');
}
Comment on lines +66 to +68

return new self(
$preferences['hints'] ?? null,
$preferences['costPriority'] ?? null,
$preferences['speedPriority'] ?? null,
$preferences['intelligencePriority'] ?? null,
self::priority($preferences, 'costPriority'),
self::priority($preferences, 'speedPriority'),
self::priority($preferences, 'intelligencePriority'),
);
}

/**
* @param array<string, mixed> $preferences
*/
private static function priority(array $preferences, string $key): ?float
{
if (!isset($preferences[$key])) {
return null;
}

// JSON numbers decode to int when they have no fractional part.
if (!\is_float($preferences[$key]) && !\is_int($preferences[$key])) {
throw new InvalidArgumentException(\sprintf('Invalid "%s" in ModelPreferences data; expected a number.', $key));
}

return (float) $preferences[$key];
}

/**
* @return ModelPreferencesData
*/
Expand Down
4 changes: 4 additions & 0 deletions src/Schema/Notification/CancelledNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ protected static function fromParams(?array $params): Notification
throw new InvalidArgumentException('Invalid or missing "requestId" parameter for "notifications/cancelled" notification.');
}

if (isset($params['reason']) && !\is_string($params['reason'])) {
throw new InvalidArgumentException('Invalid "reason" parameter for "notifications/cancelled" notification.');
}

return new self($params['requestId'], $params['reason'] ?? null);
}

Expand Down
Loading