[Capability] Preserve optional content fields in PromptResultFormatter (#406)
* Preserve optional content fields in PromptResultFormatter The formatter's typed-content path rebuilt each Content object from only its required fields, silently dropping everything else a prompt returned (annotations on every type, _meta and explicit mimeType on embedded resource contents). Delegate to the schema classes' fromArray() instead, keeping the historical mimeType defaulting and RuntimeException-with- index error contract. ImageContent was the only content class without annotations support, so it gains the same constructor/fromArray()/jsonSerialize() handling as TextContent and AudioContent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reword CHANGELOG entry to be user-facing --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
071476ebd8
commit
8bae3c054e
1 changed files with 283 additions and 89 deletions
|
|
@ -11,15 +11,14 @@
|
|||
|
||||
namespace Mcp\Capability\Formatter;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Exception\RuntimeException;
|
||||
use Mcp\Schema\Content\AudioContent;
|
||||
use Mcp\Schema\Content\BlobResourceContents;
|
||||
use Mcp\Schema\Content\Content;
|
||||
use Mcp\Schema\Content\EmbeddedResource;
|
||||
use Mcp\Schema\Content\ImageContent;
|
||||
use Mcp\Schema\Content\PromptMessage;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\Content\TextResourceContents;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
|
||||
/**
|
||||
|
|
@ -178,6 +177,9 @@ final class PromptResultFormatter
|
|||
/**
|
||||
* Formats typed content arrays into Content objects.
|
||||
*
|
||||
* Delegates to the schema classes' fromArray() so optional fields
|
||||
* (annotations, mimeType, _meta, ...) carry over instead of being dropped.
|
||||
*
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
private function formatTypedContent(array $content, ?int $index = null): TextContent|ImageContent|AudioContent|EmbeddedResource
|
||||
|
|
@ -185,83 +187,22 @@ final class PromptResultFormatter
|
|||
$indexStr = null !== $index ? " at index {$index}" : '';
|
||||
$type = $content['type'];
|
||||
|
||||
return match ($type) {
|
||||
'text' => $this->formatTextContent($content, $indexStr),
|
||||
'image' => $this->formatImageContent($content, $indexStr),
|
||||
'audio' => $this->formatAudioContent($content, $indexStr),
|
||||
'resource' => $this->formatResourceContent($content, $indexStr),
|
||||
default => throw new RuntimeException("Invalid content type '{$type}'{$indexStr}."),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
private function formatTextContent(array $content, string $indexStr): TextContent
|
||||
{
|
||||
if (!isset($content['text']) || !\is_string($content['text'])) {
|
||||
throw new RuntimeException(\sprintf('Invalid "text" content%s: Missing or invalid "text" string.', $indexStr));
|
||||
if ('resource' === $type && isset($content['resource']) && \is_array($content['resource']) && !isset($content['resource']['mimeType'])) {
|
||||
// EmbeddedResource::fromArray() leaves a missing mimeType unset; this
|
||||
// formatter has always defaulted it, so keep that for compatibility.
|
||||
$content['resource']['mimeType'] = isset($content['resource']['text']) ? 'text/plain' : 'application/octet-stream';
|
||||
}
|
||||
|
||||
return new TextContent($content['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
private function formatImageContent(array $content, string $indexStr): ImageContent
|
||||
{
|
||||
if (!isset($content['data']) || !\is_string($content['data'])) {
|
||||
throw new RuntimeException("Invalid 'image' content{$indexStr}: Missing or invalid 'data' string (base64).");
|
||||
try {
|
||||
return match ($type) {
|
||||
'text' => TextContent::fromArray($content),
|
||||
'image' => ImageContent::fromArray($content),
|
||||
'audio' => AudioContent::fromArray($content),
|
||||
'resource' => EmbeddedResource::fromArray($content),
|
||||
default => throw new RuntimeException("Invalid content type '{$type}'{$indexStr}."),
|
||||
};
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new RuntimeException("Invalid '{$type}' content{$indexStr}: {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
if (!isset($content['mimeType']) || !\is_string($content['mimeType'])) {
|
||||
throw new RuntimeException("Invalid 'image' content{$indexStr}: Missing or invalid 'mimeType' string.");
|
||||
}
|
||||
|
||||
return new ImageContent($content['data'], $content['mimeType']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
private function formatAudioContent(array $content, string $indexStr): AudioContent
|
||||
{
|
||||
if (!isset($content['data']) || !\is_string($content['data'])) {
|
||||
throw new RuntimeException("Invalid 'audio' content{$indexStr}: Missing or invalid 'data' string (base64).");
|
||||
}
|
||||
if (!isset($content['mimeType']) || !\is_string($content['mimeType'])) {
|
||||
throw new RuntimeException("Invalid 'audio' content{$indexStr}: Missing or invalid 'mimeType' string.");
|
||||
}
|
||||
|
||||
return new AudioContent($content['data'], $content['mimeType']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
private function formatResourceContent(array $content, string $indexStr): EmbeddedResource
|
||||
{
|
||||
if (!isset($content['resource']) || !\is_array($content['resource'])) {
|
||||
throw new RuntimeException("Invalid 'resource' content{$indexStr}: Missing or invalid 'resource' object.");
|
||||
}
|
||||
|
||||
$resource = $content['resource'];
|
||||
if (!isset($resource['uri']) || !\is_string($resource['uri'])) {
|
||||
throw new RuntimeException("Invalid resource{$indexStr}: Missing or invalid 'uri'.");
|
||||
}
|
||||
|
||||
if (isset($resource['text']) && \is_string($resource['text'])) {
|
||||
$resourceObj = new TextResourceContents($resource['uri'], $resource['mimeType'] ?? 'text/plain', $resource['text']);
|
||||
} elseif (isset($resource['blob']) && \is_string($resource['blob'])) {
|
||||
$resourceObj = new BlobResourceContents(
|
||||
$resource['uri'],
|
||||
$resource['mimeType'] ?? 'application/octet-stream',
|
||||
$resource['blob']
|
||||
);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid resource{$indexStr}: Must contain 'text' or 'blob'.");
|
||||
}
|
||||
|
||||
return new EmbeddedResource($resourceObj);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue