feat: Add output schema support to MCP tools (#153)
This commit is contained in:
parent
4c3e5e61c7
commit
c0938a19bd
39 changed files with 605 additions and 55 deletions
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
All notable changes to `mcp/sdk` will be documented in this file.
|
||||
|
||||
0.3
|
||||
-----
|
||||
|
||||
* Add output schema support to MCP tools
|
||||
|
||||
0.2.2
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,31 @@ final class EnvToolHandler
|
|||
*
|
||||
* @return array<string, string|int> the result, varying by APP_MODE
|
||||
*/
|
||||
#[McpTool(name: 'process_data_by_mode')]
|
||||
#[McpTool(
|
||||
name: 'process_data_by_mode',
|
||||
outputSchema: [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'mode' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The processing mode used',
|
||||
],
|
||||
'processed_input' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The processed input data',
|
||||
],
|
||||
'original_input' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The original input data (only in default mode)',
|
||||
],
|
||||
'message' => [
|
||||
'type' => 'string',
|
||||
'description' => 'A descriptive message about the processing',
|
||||
],
|
||||
],
|
||||
'required' => ['mode', 'message'],
|
||||
]
|
||||
)]
|
||||
public function processData(string $input): array
|
||||
{
|
||||
$appMode = getenv('APP_MODE'); // Read from environment
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ use Mcp\Schema\ToolAnnotations;
|
|||
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 ?Icon[] $icons Optional list of icon URLs representing the tool
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
* @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 ?Icon[] $icons Optional list of icon URLs representing the tool
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
* @param array<string, mixed> $outputSchema Optional JSON Schema object for defining the expected output structure
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $name = null,
|
||||
|
|
@ -33,6 +34,7 @@ class McpTool
|
|||
public ?ToolAnnotations $annotations = null,
|
||||
public ?array $icons = null,
|
||||
public ?array $meta = null,
|
||||
public ?array $outputSchema = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class Discoverer
|
|||
$name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||
$description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null;
|
||||
$inputSchema = $this->schemaGenerator->generate($method);
|
||||
$outputSchema = $this->schemaGenerator->generateOutputSchema($method);
|
||||
$tool = new Tool(
|
||||
$name,
|
||||
$inputSchema,
|
||||
|
|
@ -229,6 +230,7 @@ class Discoverer
|
|||
$instance->annotations,
|
||||
$instance->icons,
|
||||
$instance->meta,
|
||||
$outputSchema,
|
||||
);
|
||||
$tools[$name] = new ToolReference($tool, [$className, $methodName], false);
|
||||
++$discoveredCount['tools'];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace Mcp\Capability\Discovery;
|
||||
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Mcp\Capability\Attribute\Schema;
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Server\RequestContext;
|
||||
|
|
@ -81,6 +82,27 @@ class SchemaGenerator
|
|||
return $this->buildSchemaFromParameters($parametersInfo, $methodSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JSON Schema object (as a PHP array) for a method's or function's return type.
|
||||
*
|
||||
* Only returns an outputSchema if explicitly provided in the McpTool attribute.
|
||||
* Per MCP spec, outputSchema should only be present when explicitly provided.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array
|
||||
{
|
||||
// Only return outputSchema if explicitly provided in McpTool attribute
|
||||
$mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||
if ($mcpToolAttrs) {
|
||||
$mcpToolInstance = $mcpToolAttrs[0]->newInstance();
|
||||
|
||||
return $mcpToolInstance->outputSchema;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts method-level or function-level Schema attribute.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ final class ArrayLoader implements LoaderInterface
|
|||
* description: ?string,
|
||||
* annotations: ?ToolAnnotations,
|
||||
* icons: ?Icon[],
|
||||
* meta: ?array<string, mixed>
|
||||
* meta: ?array<string, mixed>,
|
||||
* outputSchema: ?array<string, mixed>
|
||||
* }[] $tools
|
||||
* @param array{
|
||||
* handler: Handler,
|
||||
|
|
@ -117,6 +118,7 @@ final class ArrayLoader implements LoaderInterface
|
|||
annotations: $data['annotations'] ?? null,
|
||||
icons: $data['icons'] ?? null,
|
||||
meta: $data['meta'] ?? null,
|
||||
outputSchema: $data['outputSchema'] ?? null,
|
||||
);
|
||||
$registry->registerTool($tool, $data['handler'], true);
|
||||
|
||||
|
|
|
|||
|
|
@ -111,4 +111,33 @@ class ToolReference extends ElementReference
|
|||
|
||||
return [new TextContent($jsonResult)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts structured content from a tool result using the output schema.
|
||||
*
|
||||
* @param mixed $toolExecutionResult the raw value returned by the tool's PHP method
|
||||
*
|
||||
* @return array<string, mixed>|null the structured content, or null if not extractable
|
||||
*
|
||||
* @throws \JsonException if JSON encoding fails for non-Content array/object results
|
||||
*/
|
||||
public function extractStructuredContent(mixed $toolExecutionResult): ?array
|
||||
{
|
||||
if (\is_array($toolExecutionResult)) {
|
||||
return $toolExecutionResult;
|
||||
}
|
||||
|
||||
if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) {
|
||||
$jsonResult = json_encode(
|
||||
$toolExecutionResult,
|
||||
\JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | \JSON_INVALID_UTF8_SUBSTITUTE
|
||||
);
|
||||
|
||||
return json_decode(
|
||||
$jsonResult, true, 512, \JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class CallToolResult implements ResultInterface
|
|||
* content: array<mixed>,
|
||||
* isError?: bool,
|
||||
* _meta?: array<string, mixed>,
|
||||
* structuredContent?: array<string, mixed>
|
||||
* } $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
|
|
|
|||
|
|
@ -24,13 +24,21 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
* properties: array<string, mixed>,
|
||||
* required: string[]|null
|
||||
* }
|
||||
* @phpstan-type ToolOutputSchema array{
|
||||
* type: 'object',
|
||||
* properties?: array<string, mixed>,
|
||||
* required?: string[]|null,
|
||||
* additionalProperties?: bool|array<string, mixed>,
|
||||
* description?: string
|
||||
* }
|
||||
* @phpstan-type ToolData array{
|
||||
* name: string,
|
||||
* inputSchema: ToolInputSchema,
|
||||
* description?: string|null,
|
||||
* annotations?: ToolAnnotationsData,
|
||||
* icons?: IconData[],
|
||||
* _meta?: array<string, mixed>
|
||||
* _meta?: array<string, mixed>,
|
||||
* outputSchema?: ToolOutputSchema
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
|
|
@ -38,14 +46,15 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
class Tool implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param string $name the name of the tool
|
||||
* @param ?string $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 $annotations optional additional tool information
|
||||
* @param ?Icon[] $icons optional icons representing the tool
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
* @param string $name the name of the tool
|
||||
* @param ?string $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 $annotations optional additional tool information
|
||||
* @param ?Icon[] $icons optional icons representing the tool
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
* @param ToolOutputSchema|null $outputSchema optional JSON Schema object (as a PHP array) defining the expected output structure
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
|
|
@ -54,6 +63,7 @@ class Tool implements \JsonSerializable
|
|||
public readonly ?ToolAnnotations $annotations,
|
||||
public readonly ?array $icons = null,
|
||||
public readonly ?array $meta = null,
|
||||
public readonly ?array $outputSchema = null,
|
||||
) {
|
||||
if (!isset($inputSchema['type']) || 'object' !== $inputSchema['type']) {
|
||||
throw new InvalidArgumentException('Tool inputSchema must be a JSON Schema of type "object".');
|
||||
|
|
@ -78,13 +88,23 @@ class Tool implements \JsonSerializable
|
|||
$data['inputSchema']['properties'] = new \stdClass();
|
||||
}
|
||||
|
||||
if (isset($data['outputSchema']) && \is_array($data['outputSchema'])) {
|
||||
if (!isset($data['outputSchema']['type']) || 'object' !== $data['outputSchema']['type']) {
|
||||
throw new InvalidArgumentException('Tool outputSchema must be of type "object".');
|
||||
}
|
||||
if (isset($data['outputSchema']['properties']) && \is_array($data['outputSchema']['properties']) && empty($data['outputSchema']['properties'])) {
|
||||
$data['outputSchema']['properties'] = new \stdClass();
|
||||
}
|
||||
}
|
||||
|
||||
return new 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['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
|
||||
isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null
|
||||
isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null,
|
||||
isset($data['outputSchema']) && \is_array($data['outputSchema']) ? $data['outputSchema'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +115,8 @@ class Tool implements \JsonSerializable
|
|||
* description?: string,
|
||||
* annotations?: ToolAnnotations,
|
||||
* icons?: Icon[],
|
||||
* _meta?: array<string, mixed>
|
||||
* _meta?: array<string, mixed>,
|
||||
* outputSchema?: ToolOutputSchema
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
|
|
@ -116,6 +137,9 @@ class Tool implements \JsonSerializable
|
|||
if (null !== $this->meta) {
|
||||
$data['_meta'] = $this->meta;
|
||||
}
|
||||
if (null !== $this->outputSchema) {
|
||||
$data['outputSchema'] = $this->outputSchema;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ final class Builder
|
|||
* description: ?string,
|
||||
* annotations: ?ToolAnnotations,
|
||||
* icons: ?Icon[],
|
||||
* meta: ?array<string, mixed>
|
||||
* meta: ?array<string, mixed>,
|
||||
* outputSchema: ?array<string, mixed>,
|
||||
* }[]
|
||||
*/
|
||||
private array $tools = [];
|
||||
|
|
@ -330,6 +331,7 @@ final class Builder
|
|||
* @param array<string, mixed>|null $inputSchema
|
||||
* @param ?Icon[] $icons
|
||||
* @param array<string, mixed>|null $meta
|
||||
* @param array<string, mixed>|null $outputSchema
|
||||
*/
|
||||
public function addTool(
|
||||
callable|array|string $handler,
|
||||
|
|
@ -339,6 +341,7 @@ final class Builder
|
|||
?array $inputSchema = null,
|
||||
?array $icons = null,
|
||||
?array $meta = null,
|
||||
?array $outputSchema = null,
|
||||
): self {
|
||||
$this->tools[] = compact(
|
||||
'handler',
|
||||
|
|
@ -348,6 +351,7 @@ final class Builder
|
|||
'inputSchema',
|
||||
'icons',
|
||||
'meta',
|
||||
'outputSchema',
|
||||
);
|
||||
|
||||
return $this;
|
||||
|
|
|
|||
|
|
@ -65,13 +65,16 @@ final class CallToolHandler implements RequestHandlerInterface
|
|||
|
||||
$result = $this->referenceHandler->handle($reference, $arguments);
|
||||
|
||||
$structuredContent = null;
|
||||
if (!$result instanceof CallToolResult) {
|
||||
$result = new CallToolResult($reference->formatResult($result));
|
||||
$structuredContent = $reference->extractStructuredContent($result);
|
||||
$result = new CallToolResult($reference->formatResult($result), structuredContent: $structuredContent);
|
||||
}
|
||||
|
||||
$this->logger->debug('Tool executed successfully', [
|
||||
'name' => $toolName,
|
||||
'result_type' => \gettype($result),
|
||||
'structured_content' => $structuredContent,
|
||||
]);
|
||||
|
||||
return new Response($request->getId(), $result);
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
|
||||
namespace Mcp\Tests\Conformance;
|
||||
|
||||
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;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Server\Protocol;
|
||||
use Mcp\Server\RequestContext;
|
||||
|
||||
|
|
@ -26,12 +26,9 @@ final class Elements
|
|||
public const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
|
||||
public const TEST_AUDIO_BASE64 = 'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=';
|
||||
|
||||
/**
|
||||
* @return Content[]
|
||||
*/
|
||||
public function toolMultipleTypes(): array
|
||||
public function toolMultipleTypes(): CallToolResult
|
||||
{
|
||||
return [
|
||||
return new CallToolResult([
|
||||
new TextContent('Multiple content types test:'),
|
||||
new ImageContent(self::TEST_IMAGE_BASE64, 'image/png'),
|
||||
EmbeddedResource::fromText(
|
||||
|
|
@ -39,7 +36,7 @@ final class Elements
|
|||
'{ "test" = "data", "value" = 123 }',
|
||||
'application/json',
|
||||
),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function toolWithLogging(RequestContext $context): string
|
||||
|
|
|
|||
|
|
@ -5,5 +5,18 @@
|
|||
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Project Deadline\\\" scheduled successfully for \\\"2024-12-15\\\".\",\n \"event_details\": {\n \"title\": \"Project Deadline\",\n \"date\": \"2024-12-15\",\n \"type\": \"reminder\",\n \"time\": \"All day\",\n \"priority\": \"Normal\",\n \"attendees\": [],\n \"invites_will_be_sent\": false\n }\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message": "Event \"Project Deadline\" scheduled successfully for \"2024-12-15\".",
|
||||
"event_details": {
|
||||
"title": "Project Deadline",
|
||||
"date": "2024-12-15",
|
||||
"type": "reminder",
|
||||
"time": "All day",
|
||||
"priority": "Normal",
|
||||
"attendees": [],
|
||||
"invites_will_be_sent": false
|
||||
}
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,20 @@
|
|||
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Client Call\\\" scheduled successfully for \\\"2024-12-02\\\".\",\n \"event_details\": {\n \"title\": \"Client Call\",\n \"date\": \"2024-12-02\",\n \"type\": \"call\",\n \"time\": \"14:30\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"client@example.com\"\n ],\n \"invites_will_be_sent\": false\n }\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message": "Event \"Client Call\" scheduled successfully for \"2024-12-02\".",
|
||||
"event_details": {
|
||||
"title": "Client Call",
|
||||
"date": "2024-12-02",
|
||||
"type": "call",
|
||||
"time": "14:30",
|
||||
"priority": "Normal",
|
||||
"attendees": [
|
||||
"client@example.com"
|
||||
],
|
||||
"invites_will_be_sent": false
|
||||
}
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,20 @@
|
|||
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Office Party\\\" scheduled successfully for \\\"2024-12-20\\\".\",\n \"event_details\": {\n \"title\": \"Office Party\",\n \"date\": \"2024-12-20\",\n \"type\": \"other\",\n \"time\": \"18:00\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"team@company.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message": "Event \"Office Party\" scheduled successfully for \"2024-12-20\".",
|
||||
"event_details": {
|
||||
"title": "Office Party",
|
||||
"date": "2024-12-20",
|
||||
"type": "other",
|
||||
"time": "18:00",
|
||||
"priority": "Normal",
|
||||
"attendees": [
|
||||
"team@company.com"
|
||||
],
|
||||
"invites_will_be_sent": true
|
||||
}
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,21 @@
|
|||
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Team Standup\\\" scheduled successfully for \\\"2024-12-01\\\".\",\n \"event_details\": {\n \"title\": \"Team Standup\",\n \"date\": \"2024-12-01\",\n \"type\": \"meeting\",\n \"time\": \"09:00\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"alice@example.com\",\n \"bob@example.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message": "Event \"Team Standup\" scheduled successfully for \"2024-12-01\".",
|
||||
"event_details": {
|
||||
"title": "Team Standup",
|
||||
"date": "2024-12-01",
|
||||
"type": "meeting",
|
||||
"time": "09:00",
|
||||
"priority": "Normal",
|
||||
"attendees": [
|
||||
"alice@example.com",
|
||||
"bob@example.com"
|
||||
],
|
||||
"invites_will_be_sent": true
|
||||
}
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@
|
|||
"text": "{\n \"success\": true,\n \"message_sent\": \"Welcome, Alice! Welcome to our platform!\"\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message_sent": "Welcome, Alice! Welcome to our platform!"
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@
|
|||
"text": "{\n \"success\": true,\n \"message\": \"Test tool without params\"\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"success": true,
|
||||
"message": "Test tool without params"
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,11 @@
|
|||
"text": "{\n \"result\": 50,\n \"operation\": \"10 multiply 5\",\n \"precision\": 2,\n \"within_bounds\": true\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"result": 50,
|
||||
"operation": "10 multiply 5",
|
||||
"precision": 2,
|
||||
"within_bounds": true
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,11 @@
|
|||
"text": "{\n \"original\": \"Hello World Test\",\n \"formatted\": \"HELLO WORLD TEST\",\n \"length\": 16,\n \"format_applied\": \"uppercase\"\n}"
|
||||
}
|
||||
],
|
||||
"structuredContent": {
|
||||
"original": "Hello World Test",
|
||||
"formatted": "HELLO WORLD TEST",
|
||||
"length": 16,
|
||||
"format_applied": "uppercase"
|
||||
},
|
||||
"isError": false
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue