diff --git a/src/Capability/Attribute/McpPrompt.php b/src/Capability/Attribute/McpPrompt.php index 73677e33..8488e608 100644 --- a/src/Capability/Attribute/McpPrompt.php +++ b/src/Capability/Attribute/McpPrompt.php @@ -21,12 +21,14 @@ final class McpPrompt { /** - * @param ?string $name overrides the prompt name (defaults to method name) - * @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary. + * @param ?string $name overrides the prompt name (defaults to method name) + * @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary. + * @param ?array $meta Optional metadata */ public function __construct( public ?string $name = null, public ?string $description = null, + public ?array $meta = null, ) { } } diff --git a/src/Capability/Attribute/McpResource.php b/src/Capability/Attribute/McpResource.php index 873d485c..86b33078 100644 --- a/src/Capability/Attribute/McpResource.php +++ b/src/Capability/Attribute/McpResource.php @@ -23,12 +23,13 @@ final class McpResource { /** - * @param string $uri The specific URI identifying this resource instance. Must be unique within the server. - * @param ?string $name A human-readable name for this resource. If null, a default might be generated from the method name. - * @param ?string $description An optional description of the resource. Defaults to class DocBlock summary. - * @param ?string $mimeType the MIME type, if known and constant for this resource - * @param ?int $size the size in bytes, if known and constant - * @param Annotations|null $annotations optional annotations describing the resource + * @param string $uri The specific URI identifying this resource instance. Must be unique within the server. + * @param ?string $name A human-readable name for this resource. If null, a default might be generated from the method name. + * @param ?string $description An optional description of the resource. Defaults to class DocBlock summary. + * @param ?string $mimeType the MIME type, if known and constant for this resource + * @param ?int $size the size in bytes, if known and constant + * @param Annotations|null $annotations optional annotations describing the resource + * @param ?array $meta Optional metadata */ public function __construct( public string $uri, @@ -37,6 +38,7 @@ public function __construct( public ?string $mimeType = null, public ?int $size = null, public ?Annotations $annotations = null, + public ?array $meta = null, ) { } } diff --git a/src/Capability/Attribute/McpResourceTemplate.php b/src/Capability/Attribute/McpResourceTemplate.php index 9b8887f1..14e66c5f 100644 --- a/src/Capability/Attribute/McpResourceTemplate.php +++ b/src/Capability/Attribute/McpResourceTemplate.php @@ -23,11 +23,12 @@ final class McpResourceTemplate { /** - * @param string $uriTemplate the URI template string (RFC 6570) - * @param ?string $name A human-readable name for the template type. If null, a default might be generated from the method name. - * @param ?string $description Optional description. Defaults to class DocBlock summary. - * @param ?string $mimeType optional default MIME type for matching resources - * @param ?Annotations $annotations optional annotations describing the resource template + * @param string $uriTemplate the URI template string (RFC 6570) + * @param ?string $name A human-readable name for the template type. If null, a default might be generated from the method name. + * @param ?string $description Optional description. Defaults to class DocBlock summary. + * @param ?string $mimeType optional default MIME type for matching resources + * @param ?Annotations $annotations optional annotations describing the resource template + * @param ?array $meta Optional metadata */ public function __construct( public string $uriTemplate, @@ -35,6 +36,7 @@ public function __construct( public ?string $description = null, public ?string $mimeType = null, public ?Annotations $annotations = null, + public ?array $meta = null, ) { } } diff --git a/src/Capability/Attribute/McpTool.php b/src/Capability/Attribute/McpTool.php index d4af3e6c..e5cab26b 100644 --- a/src/Capability/Attribute/McpTool.php +++ b/src/Capability/Attribute/McpTool.php @@ -20,14 +20,16 @@ class McpTool { /** - * @param string|null $name The name of the tool (defaults to the method name) - * @param string|null $description The description of the tool (defaults to the DocBlock/inferred) - * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior + * @param string|null $name The name of the tool (defaults to the method name) + * @param string|null $description The description of the tool (defaults to the DocBlock/inferred) + * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior + * @param ?array $meta Optional metadata */ public function __construct( public ?string $name = null, public ?string $description = null, public ?ToolAnnotations $annotations = null, + public ?array $meta = null, ) { } } diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index e6cc328f..e8f32940 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -222,7 +222,8 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName); $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; $inputSchema = $this->schemaGenerator->generate($method); - $tool = new Tool($name, $inputSchema, $description, $instance->annotations); + $meta = $instance->meta ?? null; + $tool = new Tool($name, $inputSchema, $description, $instance->annotations, $meta); $tools[$name] = new ToolReference($tool, [$className, $methodName], false); ++$discoveredCount['tools']; break; @@ -234,8 +235,10 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $mimeType = $instance->mimeType; $size = $instance->size; $annotations = $instance->annotations; - $resource = new Resource($instance->uri, $name, $description, $mimeType, $annotations, $size); + $meta = $instance->meta; + $resource = new Resource($instance->uri, $name, $description, $mimeType, $annotations, $size, $meta); $resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName], false); + ++$discoveredCount['resources']; break; @@ -253,7 +256,8 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $paramTag = $paramTags['$'.$param->getName()] ?? null; $arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable()); } - $prompt = new Prompt($name, $description, $arguments); + $meta = $instance->meta ?? null; + $prompt = new Prompt($name, $description, $arguments, $meta); $completionProviders = $this->getCompletionProviders($method); $prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders); ++$discoveredCount['prompts']; @@ -265,7 +269,8 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; $mimeType = $instance->mimeType; $annotations = $instance->annotations; - $resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations); + $meta = $instance->meta ?? null; + $resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations, $meta); $completionProviders = $this->getCompletionProviders($method); $resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], false, $completionProviders); ++$discoveredCount['resourceTemplates']; diff --git a/src/Capability/Registry/Loader/ArrayLoader.php b/src/Capability/Registry/Loader/ArrayLoader.php index 7420bc75..a826bf6d 100644 --- a/src/Capability/Registry/Loader/ArrayLoader.php +++ b/src/Capability/Registry/Loader/ArrayLoader.php @@ -45,6 +45,7 @@ final class ArrayLoader implements LoaderInterface * name: ?string, * description: ?string, * annotations: ?ToolAnnotations, + * meta: ?array * }[] $tools * @param array{ * handler: Handler, @@ -54,6 +55,7 @@ final class ArrayLoader implements LoaderInterface * mimeType: ?string, * size: int|null, * annotations: ?Annotations, + * meta: ?array * }[] $resources * @param array{ * handler: Handler, @@ -62,11 +64,13 @@ final class ArrayLoader implements LoaderInterface * description: ?string, * mimeType: ?string, * annotations: ?Annotations, + * meta: ?array * }[] $resourceTemplates * @param array{ * handler: Handler, * name: ?string, * description: ?string, + * meta: ?array * }[] $prompts */ public function __construct( @@ -102,7 +106,7 @@ public function load(ReferenceRegistryInterface $registry): void $inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection); - $tool = new Tool($name, $inputSchema, $description, $data['annotations']); + $tool = new Tool($name, $inputSchema, $description, $data['annotations'], $data['meta'] ?? null); $registry->registerTool($tool, $data['handler'], true); $handlerDesc = $this->getHandlerDescription($data['handler']); @@ -137,8 +141,9 @@ public function load(ReferenceRegistryInterface $registry): void $mimeType = $data['mimeType']; $size = $data['size']; $annotations = $data['annotations']; + $meta = $data['meta']; - $resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size); + $resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size, $meta); $registry->registerResource($resource, $data['handler'], true); $handlerDesc = $this->getHandlerDescription($data['handler']); @@ -172,8 +177,9 @@ public function load(ReferenceRegistryInterface $registry): void $uriTemplate = $data['uriTemplate']; $mimeType = $data['mimeType']; $annotations = $data['annotations']; + $meta = $data['meta']; - $template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations); + $template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations, $meta); $completionProviders = $this->getCompletionProviders($reflection); $registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true); @@ -224,8 +230,8 @@ public function load(ReferenceRegistryInterface $registry): void !$param->isOptional() && !$param->isDefaultValueAvailable(), ); } - - $prompt = new Prompt($name, $description, $arguments); + $meta = $data['meta']; + $prompt = new Prompt($name, $description, $arguments, $meta); $completionProviders = $this->getCompletionProviders($reflection); $registry->registerPrompt($prompt, $data['handler'], $completionProviders, true); diff --git a/src/Capability/Registry/ResourceReference.php b/src/Capability/Registry/ResourceReference.php index c93d5a14..d9b6a7e4 100644 --- a/src/Capability/Registry/ResourceReference.php +++ b/src/Capability/Registry/ResourceReference.php @@ -68,9 +68,11 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = return [$readResult->resource]; } + $meta = $this->schema->meta; + if (\is_array($readResult)) { if (empty($readResult)) { - return [new TextResourceContents($uri, 'application/json', '[]')]; + return [new TextResourceContents($uri, 'application/json', '[]', $meta)]; } $allAreResourceContents = true; @@ -118,14 +120,15 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = if (\is_string($readResult)) { $mimeType = $mimeType ?? $this->guessMimeTypeFromString($readResult); - return [new TextResourceContents($uri, $mimeType, $readResult)]; + return [new TextResourceContents($uri, $mimeType, $readResult, $meta)]; } if (\is_resource($readResult) && 'stream' === get_resource_type($readResult)) { $result = BlobResourceContents::fromStream( $uri, $readResult, - $mimeType ?? 'application/octet-stream' + $mimeType ?? 'application/octet-stream', + $meta ); @fclose($readResult); @@ -136,21 +139,21 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = if (\is_array($readResult) && isset($readResult['blob']) && \is_string($readResult['blob'])) { $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'application/octet-stream'; - return [new BlobResourceContents($uri, $mimeType, $readResult['blob'])]; + return [new BlobResourceContents($uri, $mimeType, $readResult['blob'], $meta)]; } if (\is_array($readResult) && isset($readResult['text']) && \is_string($readResult['text'])) { $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'text/plain'; - return [new TextResourceContents($uri, $mimeType, $readResult['text'])]; + return [new TextResourceContents($uri, $mimeType, $readResult['text'], $meta)]; } if ($readResult instanceof \SplFileInfo && $readResult->isFile() && $readResult->isReadable()) { if ($mimeType && str_contains(strtolower($mimeType), 'text')) { - return [new TextResourceContents($uri, $mimeType, file_get_contents($readResult->getPathname()))]; + return [new TextResourceContents($uri, $mimeType, file_get_contents($readResult->getPathname()), $meta)]; } - return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType)]; + return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType, $meta)]; } if (\is_array($readResult)) { @@ -159,7 +162,7 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = try { $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); - return [new TextResourceContents($uri, $mimeType, $jsonString)]; + return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; } catch (\JsonException $e) { throw new RuntimeException(\sprintf('Failed to encode array as JSON for URI "%s": %s', $uri, $e->getMessage())); } @@ -169,7 +172,7 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); $mimeType = $mimeType ?? 'application/json'; - return [new TextResourceContents($uri, $mimeType, $jsonString)]; + return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; } catch (\JsonException $e) { throw new RuntimeException(\sprintf('Failed to encode array as JSON for URI "%s": %s', $uri, $e->getMessage())); } diff --git a/src/Capability/Registry/ResourceTemplateReference.php b/src/Capability/Registry/ResourceTemplateReference.php index b71a943a..88104c9d 100644 --- a/src/Capability/Registry/ResourceTemplateReference.php +++ b/src/Capability/Registry/ResourceTemplateReference.php @@ -101,9 +101,11 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = return [$readResult->resource]; } + $meta = $this->resourceTemplate->meta; + if (\is_array($readResult)) { if (empty($readResult)) { - return [new TextResourceContents($uri, 'application/json', '[]')]; + return [new TextResourceContents($uri, 'application/json', '[]', $meta)]; } $allAreResourceContents = true; @@ -151,14 +153,15 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = if (\is_string($readResult)) { $mimeType = $mimeType ?? $this->guessMimeTypeFromString($readResult); - return [new TextResourceContents($uri, $mimeType, $readResult)]; + return [new TextResourceContents($uri, $mimeType, $readResult, $meta)]; } if (\is_resource($readResult) && 'stream' === get_resource_type($readResult)) { $result = BlobResourceContents::fromStream( $uri, $readResult, - $mimeType ?? 'application/octet-stream' + $mimeType ?? 'application/octet-stream', + $meta ); @fclose($readResult); @@ -169,21 +172,21 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = if (\is_array($readResult) && isset($readResult['blob']) && \is_string($readResult['blob'])) { $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'application/octet-stream'; - return [new BlobResourceContents($uri, $mimeType, $readResult['blob'])]; + return [new BlobResourceContents($uri, $mimeType, $readResult['blob'], $meta)]; } if (\is_array($readResult) && isset($readResult['text']) && \is_string($readResult['text'])) { $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'text/plain'; - return [new TextResourceContents($uri, $mimeType, $readResult['text'])]; + return [new TextResourceContents($uri, $mimeType, $readResult['text'], $meta)]; } if ($readResult instanceof \SplFileInfo && $readResult->isFile() && $readResult->isReadable()) { if ($mimeType && str_contains(strtolower($mimeType), 'text')) { - return [new TextResourceContents($uri, $mimeType, file_get_contents($readResult->getPathname()))]; + return [new TextResourceContents($uri, $mimeType, file_get_contents($readResult->getPathname()), $meta)]; } - return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType)]; + return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType, $meta)]; } if (\is_array($readResult)) { @@ -192,7 +195,7 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = try { $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); - return [new TextResourceContents($uri, $mimeType, $jsonString)]; + return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; } catch (\JsonException $e) { throw new RuntimeException("Failed to encode array as JSON for URI '{$uri}': {$e->getMessage()}"); } @@ -202,7 +205,7 @@ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); $mimeType = $mimeType ?? 'application/json'; - return [new TextResourceContents($uri, $mimeType, $jsonString)]; + return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; } catch (\JsonException $e) { throw new RuntimeException("Failed to encode array as JSON for URI '{$uri}': {$e->getMessage()}"); } diff --git a/src/Schema/Content/BlobResourceContents.php b/src/Schema/Content/BlobResourceContents.php index c8b07b05..0d6016d5 100644 --- a/src/Schema/Content/BlobResourceContents.php +++ b/src/Schema/Content/BlobResourceContents.php @@ -19,7 +19,8 @@ * @phpstan-type BlobResourceContentsData array{ * uri: string, * mimeType?: string|null, - * blob: string + * blob: string, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -27,16 +28,18 @@ 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 + * @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 + * @param ?array $meta Optional metadata */ public function __construct( string $uri, ?string $mimeType, public readonly string $blob, + ?array $meta = null, ) { - parent::__construct($uri, $mimeType); + parent::__construct($uri, $mimeType, $meta); } /** @@ -51,25 +54,29 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('Missing or invalid "blob" for BlobResourceContents.'); } - return new self($data['uri'], $data['mimeType'] ?? null, $data['blob']); + return new self($data['uri'], $data['mimeType'] ?? null, $data['blob'], $data['_meta'] ?? null); } /** - * @param resource $stream - */ - public static function fromStream(string $uri, $stream, string $mimeType): self + * @param resource $stream + * @param ?array $meta Optional metadata + * */ + public static function fromStream(string $uri, $stream, string $mimeType, ?array $meta = null): self { $blob = stream_get_contents($stream); - return new self($uri, $mimeType, base64_encode($blob)); + return new self($uri, $mimeType, base64_encode($blob), $meta); } - public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null): self + /** + * @param ?array $meta Optional metadata + * */ + public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null, ?array $meta = null): self { $mimeType = $explicitMimeType ?? mime_content_type($file->getPathname()); $blob = file_get_contents($file->getPathname()); - return new self($uri, $mimeType, base64_encode($blob)); + return new self($uri, $mimeType, base64_encode($blob), $meta); } /** @@ -79,7 +86,7 @@ public function jsonSerialize(): array { return [ 'blob' => $this->blob, - ...$this->jsonSerialize(), + ...parent::jsonSerialize(), ]; } } diff --git a/src/Schema/Content/ResourceContents.php b/src/Schema/Content/ResourceContents.php index d4c7733b..ffd5599b 100644 --- a/src/Schema/Content/ResourceContents.php +++ b/src/Schema/Content/ResourceContents.php @@ -16,7 +16,8 @@ * * @phpstan-type ResourceContentsData = array{ * uri: string, - * mimeType?: string|null + * mimeType?: string|null, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -24,12 +25,14 @@ 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 + * @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 ?array $meta Optional metadata */ public function __construct( public readonly string $uri, public readonly ?string $mimeType = null, + public readonly ?array $meta = null, ) { } @@ -43,6 +46,10 @@ public function jsonSerialize(): array $data['mimeType'] = $this->mimeType; } + if (null !== $this->meta) { + $data['_meta'] = $this->meta; + } + return $data; } } diff --git a/src/Schema/Content/TextResourceContents.php b/src/Schema/Content/TextResourceContents.php index 04880f59..47ee31fd 100644 --- a/src/Schema/Content/TextResourceContents.php +++ b/src/Schema/Content/TextResourceContents.php @@ -19,7 +19,8 @@ * @phpstan-type TextResourceContentsData array{ * uri: string, * mimeType?: string|null, - * text: string + * text: string, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -27,16 +28,18 @@ 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). + * @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). + * @param ?array $meta Optional metadata */ public function __construct( string $uri, ?string $mimeType, public readonly string $text, + ?array $meta = null, ) { - parent::__construct($uri, $mimeType); + parent::__construct($uri, $mimeType, $meta); } /** @@ -51,7 +54,7 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('Missing or invalid "text" for TextResourceContents.'); } - return new self($data['uri'], $data['mimeType'] ?? null, $data['text']); + return new self($data['uri'], $data['mimeType'] ?? null, $data['text'], $data['_meta'] ?? null); } /** diff --git a/src/Schema/Prompt.php b/src/Schema/Prompt.php index 96cffcd9..c61efcd1 100644 --- a/src/Schema/Prompt.php +++ b/src/Schema/Prompt.php @@ -22,6 +22,7 @@ * name: string, * description?: string, * arguments?: PromptArgumentData[], + * _meta?: array * } * * @author Kyrian Obikwelu @@ -32,11 +33,13 @@ class Prompt implements \JsonSerializable * @param string $name the name of the prompt or prompt template * @param string|null $description an optional description of what this prompt provides * @param PromptArgument[]|null $arguments A list of arguments for templating. Null if not a template. + * @param ?array $meta Optional metadata */ public function __construct( public readonly string $name, public readonly ?string $description = null, public readonly ?array $arguments = null, + public readonly ?array $meta = null, ) { if (null !== $this->arguments) { foreach ($this->arguments as $arg) { @@ -60,10 +63,15 @@ public static function fromArray(array $data): self $arguments = array_map(fn (array $argData) => PromptArgument::fromArray($argData), $data['arguments']); } + if (!empty($data['_meta']) && !\is_array($data['_meta'])) { + throw new InvalidArgumentException('Invalid "_meta" in Prompt data.'); + } + return new self( name: $data['name'], description: $data['description'] ?? null, - arguments: $arguments + arguments: $arguments, + meta: isset($data['_meta']) ? $data['_meta'] : null ); } @@ -71,7 +79,8 @@ public static function fromArray(array $data): self * @return array{ * name: string, * description?: string, - * arguments?: array + * arguments?: array, + * _meta?: array * } */ public function jsonSerialize(): array @@ -83,6 +92,9 @@ public function jsonSerialize(): array if (null !== $this->arguments) { $data['arguments'] = $this->arguments; } + if (null !== $this->meta) { + $data['_meta'] = $this->meta; + } return $data; } diff --git a/src/Schema/Resource.php b/src/Schema/Resource.php index ca6fa11a..36ac5938 100644 --- a/src/Schema/Resource.php +++ b/src/Schema/Resource.php @@ -25,6 +25,7 @@ * mimeType?: string|null, * annotations?: AnnotationsData|null, * size?: int|null, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -43,14 +44,15 @@ class Resource implements \JsonSerializable private const URI_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*$/'; /** - * @param string $uri the URI of this resource - * @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements. - * @param string|null $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - * @param string|null $mimeType the MIME type of this resource, if known - * @param Annotations|null $annotations optional annotations for the client - * @param int|null $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * @param string $uri the URI of this resource + * @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements. + * @param string|null $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + * @param string|null $mimeType the MIME type of this resource, if known + * @param Annotations|null $annotations optional annotations for the client + * @param int|null $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * @param ?array $meta Optional metadata * - * This can be used by Hosts to display file sizes and estimate context window usage. + * This can be used by Hosts to display file sizes and estimate context window usage */ public function __construct( public readonly string $uri, @@ -59,6 +61,7 @@ public function __construct( public readonly ?string $mimeType = null, public readonly ?Annotations $annotations = null, public readonly ?int $size = null, + public readonly ?array $meta = null, ) { if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) { throw new InvalidArgumentException('Invalid resource name: must contain only alphanumeric characters, underscores, and hyphens.'); @@ -80,13 +83,18 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('Invalid or missing "name" in Resource data.'); } + if (!empty($data['_meta']) && !\is_array($data['_meta'])) { + throw new InvalidArgumentException('Invalid "_meta" in Resource data.'); + } + return new self( uri: $data['uri'], name: $data['name'], description: $data['description'] ?? null, mimeType: $data['mimeType'] ?? null, annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null, - size: isset($data['size']) ? (int) $data['size'] : null + size: isset($data['size']) ? (int) $data['size'] : null, + meta: isset($data['_meta']) ? $data['_meta'] : null ); } @@ -98,6 +106,7 @@ public static function fromArray(array $data): self * mimeType?: string, * annotations?: Annotations, * size?: int, + * _meta?: array * } */ public function jsonSerialize(): array @@ -118,6 +127,9 @@ public function jsonSerialize(): array if (null !== $this->size) { $data['size'] = $this->size; } + if (null !== $this->meta) { + $data['_meta'] = $this->meta; + } return $data; } diff --git a/src/Schema/ResourceTemplate.php b/src/Schema/ResourceTemplate.php index 796b3e22..136b8b6e 100644 --- a/src/Schema/ResourceTemplate.php +++ b/src/Schema/ResourceTemplate.php @@ -24,6 +24,7 @@ * description?: string|null, * mimeType?: string|null, * annotations?: AnnotationsData|null, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -42,11 +43,12 @@ class ResourceTemplate implements \JsonSerializable private const URI_TEMPLATE_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/.*{[^{}]+}.*/'; /** - * @param string $uriTemplate a URI template (according to RFC 6570) that can be used to construct resource URIs - * @param string $name A human-readable name for the type of resource this template refers to. This can be used by clients to populate UI elements. - * @param string|null $description This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - * @param string|null $mimeType The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - * @param Annotations|null $annotations optional annotations for the client + * @param string $uriTemplate a URI template (according to RFC 6570) that can be used to construct resource URIs + * @param string $name A human-readable name for the type of resource this template refers to. This can be used by clients to populate UI elements. + * @param string|null $description This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + * @param string|null $mimeType The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + * @param Annotations|null $annotations optional annotations for the client + * @param ?array $meta Optional metadata */ public function __construct( public readonly string $uriTemplate, @@ -54,6 +56,7 @@ public function __construct( public readonly ?string $description = null, public readonly ?string $mimeType = null, public readonly ?Annotations $annotations = null, + public readonly ?array $meta = null, ) { if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) { throw new InvalidArgumentException('Invalid resource name: must contain only alphanumeric characters, underscores, and hyphens.'); @@ -75,12 +78,17 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('Invalid or missing "name" in ResourceTemplate data.'); } + if (!empty($data['_meta']) && !\is_array($data['_meta'])) { + throw new InvalidArgumentException('Invalid "_meta" in ResourceTemplate data.'); + } + return new self( uriTemplate: $data['uriTemplate'], name: $data['name'], description: $data['description'] ?? null, mimeType: $data['mimeType'] ?? null, - annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null + annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null, + meta: isset($data['_meta']) ? $data['_meta'] : null ); } @@ -91,6 +99,7 @@ public static function fromArray(array $data): self * description?: string, * mimeType?: string, * annotations?: Annotations, + * _meta?: array * } */ public function jsonSerialize(): array @@ -108,6 +117,9 @@ public function jsonSerialize(): array if (null !== $this->annotations) { $data['annotations'] = $this->annotations; } + if (null !== $this->meta) { + $data['_meta'] = $this->meta; + } return $data; } diff --git a/src/Schema/Tool.php b/src/Schema/Tool.php index 29646efc..c3613074 100644 --- a/src/Schema/Tool.php +++ b/src/Schema/Tool.php @@ -28,6 +28,7 @@ * inputSchema: ToolInputSchema, * description?: string|null, * annotations?: ToolAnnotationsData, + * _meta?: array * } * * @author Kyrian Obikwelu @@ -35,18 +36,20 @@ class Tool implements \JsonSerializable { /** - * @param string $name the name of the tool - * @param string|null $description A human-readable description of the tool. - * This can be used by clients to improve the LLM's understanding of - * available tools. It can be thought of like a "hint" to the model. - * @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool - * @param ToolAnnotations|null $annotations optional additional tool information + * @param string $name the name of the tool + * @param string|null $description A human-readable description of the tool. + * This can be used by clients to improve the LLM's understanding of + * available tools. It can be thought of like a "hint" to the model. + * @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool + * @param ToolAnnotations|null $annotations optional additional tool information + * @param ?array $meta Optional metadata */ public function __construct( public readonly string $name, public readonly array $inputSchema, public readonly ?string $description, public readonly ?ToolAnnotations $annotations, + public readonly ?array $meta = null, ) { if (!isset($inputSchema['type']) || 'object' !== $inputSchema['type']) { throw new InvalidArgumentException('Tool inputSchema must be a JSON Schema of type "object".'); @@ -75,7 +78,8 @@ public static function fromArray(array $data): self $data['name'], $data['inputSchema'], isset($data['description']) && \is_string($data['description']) ? $data['description'] : null, - isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null + isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null, + isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null ); } @@ -85,6 +89,7 @@ public static function fromArray(array $data): self * inputSchema: ToolInputSchema, * description?: string, * annotations?: ToolAnnotations, + * _meta?: array * } */ public function jsonSerialize(): array @@ -99,6 +104,9 @@ public function jsonSerialize(): array if (null !== $this->annotations) { $data['annotations'] = $this->annotations; } + if (null !== $this->meta) { + $data['_meta'] = $this->meta; + } return $data; } diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 90bf53a4..bb550f93 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -78,6 +78,7 @@ final class Builder * name: ?string, * description: ?string, * annotations: ?ToolAnnotations, + * meta: ?array * }[] */ private array $tools = []; @@ -91,6 +92,7 @@ final class Builder * mimeType: ?string, * size: int|null, * annotations: ?Annotations, + * meta: ?array * }[] */ private array $resources = []; @@ -103,6 +105,7 @@ final class Builder * description: ?string, * mimeType: ?string, * annotations: ?Annotations, + * meta: ?array * }[] */ private array $resourceTemplates = []; @@ -112,6 +115,7 @@ final class Builder * handler: Handler, * name: ?string, * description: ?string, + * meta: ?array * }[] */ private array $prompts = [];