PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wordpress / mcp-adapter / includes / Domain / Resources / McpResource.php

McpResource.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at vendor/wordpress/mcp-adapter/includes/Domain/Resources/McpResource.php

359 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * MCP Resource component.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Domain\Resources;
12
13 use WP\MCP\Domain\Contracts\McpComponentInterface;
14 use WP\MCP\Domain\Utils\McpValidator;
15 use WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface;
16 use WP\MCP\Infrastructure\Observability\FailureReason;
17 use WP\McpSchema\Common\Protocol\DTO\Annotations;
18 use WP\McpSchema\Server\Resources\DTO\Resource as ResourceDto;
19 use WP_Error;
20
21 /**
22 * Resource component providing unified execution and permission checks.
23 *
24 * This class supports multiple ways to register resources:
25 *
26 * 1. Array configuration:
27 * ```php
28 * $resource = McpResource::fromArray([
29 * 'uri' => 'WordPress://local/readme',
30 * 'title' => 'README',
31 * 'description' => 'Example resource',
32 * 'handler' => fn() => 'Hello',
33 * 'permission' => fn() => true,
34 * ]);
35 * ```
36 *
37 * 2. From WordPress Ability (ability-backed):
38 * ```php
39 * $resource = McpResource::fromAbility($ability);
40 * ```
41 *
42 * McpResource wraps a protocol-only ResourceDto for MCP serialization. Internal
43 * adapter metadata and execution wiring live on this class and are never
44 * exposed to MCP clients. Use get_protocol_dto() for protocol responses.
45 *
46 * @since 0.5.0
47 */
48 final class McpResource implements McpComponentInterface {
49
50
51 // =========================================================================
52 // Runtime Properties
53 // =========================================================================
54
55 /**
56 * Clean Resource DTO (protocol-only).
57 *
58 * @var \WP\McpSchema\Server\Resources\DTO\Resource
59 */
60 private ResourceDto $mcp_resource_dto;
61
62 /**
63 * Ability used for execution/permission checks (ability-backed resources).
64 *
65 * @var \WP_Ability|null
66 */
67 private ?\WP_Ability $ability = null;
68
69 /**
70 * Direct execution handler (callable-backed resources).
71 *
72 * @var callable|null
73 */
74 private $handler = null;
75
76 /**
77 * Direct permission callback (callable-backed resources).
78 *
79 * @var callable|null
80 */
81 private $permission_callback = null;
82
83 /**
84 * Internal adapter metadata (never exposed to clients).
85 *
86 * @var array<string, mixed>
87 */
88 private array $adapter_meta = array();
89
90 /**
91 * Observability context tags for logging/metrics.
92 *
93 * @var array<string, mixed>
94 */
95 private array $observability_context = array();
96
97 // =========================================================================
98 // Constructor
99 // =========================================================================
100
101 /**
102 * Private constructor - use factory methods.
103 *
104 * @param \WP\McpSchema\Server\Resources\DTO\Resource $resource_dto The Resource DTO.
105 */
106 private function __construct( ResourceDto $resource_dto ) {
107 $this->mcp_resource_dto = $resource_dto;
108 }
109
110 // =========================================================================
111 // Factory Methods
112 // =========================================================================
113
114 /**
115 * @param array $config The resource configuration array.
116 *
117 * @return self|\WP_Error
118 */
119 public static function fromArray( array $config ) {
120 if ( empty( $config['uri'] ) ) {
121 return new WP_Error( 'mcp_resource_missing_uri', 'Resource configuration must include a "uri" field.' );
122 }
123
124 if ( ! isset( $config['handler'] ) || ! is_callable( $config['handler'] ) ) {
125 return new WP_Error( 'mcp_resource_missing_handler', 'Resource configuration must include a callable "handler" field.' );
126 }
127
128 $uri = trim( $config['uri'] );
129
130 if ( ! McpValidator::validate_resource_uri( $uri ) ) {
131 return new WP_Error( 'mcp_resource_invalid_uri', 'Resource "uri" must be a valid RFC 3986 URI with a scheme.' );
132 }
133
134 $name = isset( $config['name'] ) ? trim( $config['name'] ) : $uri;
135 if ( '' === $name ) {
136 return new WP_Error( 'mcp_resource_missing_name', 'Resource "name" cannot be empty.' );
137 }
138
139 $resource_data = array(
140 'name' => $name,
141 'uri' => $uri,
142 );
143
144 if ( isset( $config['title'] ) ) {
145 $resource_data['title'] = $config['title'];
146 }
147
148 if ( isset( $config['description'] ) ) {
149 $resource_data['description'] = $config['description'];
150 }
151
152 // Include mimeType only when valid.
153 if ( isset( $config['mimeType'] ) ) {
154 $mime_type = trim( $config['mimeType'] );
155 if ( '' !== $mime_type && McpValidator::validate_mime_type( $mime_type ) ) {
156 $resource_data['mimeType'] = $mime_type;
157 }
158 }
159
160 // Include size only when > 0.
161 if ( isset( $config['size'] ) && $config['size'] > 0 ) {
162 $resource_data['size'] = $config['size'];
163 }
164
165 // Validate and include icons if set.
166 if ( isset( $config['icons'] ) && is_array( $config['icons'] ) && ! empty( $config['icons'] ) ) {
167 $icons_result = McpValidator::validate_icons_array( $config['icons'] );
168 if ( ! empty( $icons_result['valid'] ) ) {
169 $resource_data['icons'] = $icons_result['valid'];
170 }
171 }
172
173 if ( isset( $config['meta'] ) && is_array( $config['meta'] ) && ! empty( $config['meta'] ) ) {
174 $resource_data['_meta'] = $config['meta'];
175 }
176
177 // Create the Resource DTO - wrap in try-catch since Annotations::fromArray() and ResourceDto::fromArray() can throw.
178 try {
179 // Process annotations inside try-catch since Annotations::fromArray() can throw.
180 if ( isset( $config['annotations'] ) && is_array( $config['annotations'] ) && ! empty( $config['annotations'] ) ) {
181 $resource_data['annotations'] = Annotations::fromArray( $config['annotations'] );
182 }
183
184 $resource = ResourceDto::fromArray( $resource_data );
185 } catch ( \Throwable $e ) {
186 return new WP_Error(
187 'mcp_resource_dto_creation_failed',
188 sprintf(
189 /* translators: %s: error message */
190 __( 'Failed to create Resource DTO: %s', 'mcp-adapter' ),
191 $e->getMessage()
192 ),
193 array( 'exception' => $e )
194 );
195 }
196
197 // Optional deep validation if enabled.
198 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
199 if ( $mcp_validation_enabled ) {
200 $validation_result = McpResourceValidator::validate_resource_dto( $resource );
201 if ( is_wp_error( $validation_result ) ) {
202 return $validation_result;
203 }
204 }
205
206 $instance = new self( $resource );
207 $instance->handler = $config['handler'];
208
209 if ( isset( $config['permission'] ) && is_callable( $config['permission'] ) ) {
210 $instance->permission_callback = $config['permission'];
211 }
212
213 $instance->observability_context = array(
214 'component_type' => 'resource',
215 'resource_uri' => $uri,
216 'source' => 'array',
217 );
218
219 return $instance;
220 }
221
222 /**
223 * Create an ability-backed MCP resource.
224 *
225 * @param \WP_Ability $ability WordPress ability.
226 * @param \WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null $error_handler Optional error handler.
227 *
228 * @return self|\WP_Error
229 */
230 public static function fromAbility( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null ) {
231 $resource_data = RegisterAbilityAsMcpResource::build( $ability, $error_handler );
232 if ( $resource_data instanceof WP_Error ) {
233 return $resource_data;
234 }
235
236 $instance = new self( $resource_data['resource'] );
237 $instance->adapter_meta = $resource_data['adapter_meta'];
238 $instance->ability = $ability;
239
240 $instance->observability_context = array(
241 'component_type' => 'resource',
242 'resource_uri' => $resource_data['resource']->getUri(),
243 'ability_name' => $ability->get_name(),
244 'source' => 'ability',
245 );
246
247 return $instance;
248 }
249
250 // =========================================================================
251 // McpComponentInterface Implementation
252 // =========================================================================
253
254 /**
255 * Get the clean protocol DTO for MCP responses.
256 *
257 * @return \WP\McpSchema\Server\Resources\DTO\Resource
258 */
259 public function get_protocol_dto(): ResourceDto {
260 return $this->mcp_resource_dto;
261 }
262
263 /**
264 * Execute the resource read.
265 *
266 * @param mixed $arguments Read arguments (may be empty).
267 *
268 * @return mixed
269 */
270 public function execute( $arguments ) {
271 // Ability-backed resources match existing behavior: no args passed to abilities.
272 if ( null !== $this->ability ) {
273 try {
274 return $this->ability->execute();
275 } catch ( \Throwable $throwable ) {
276 return new WP_Error(
277 'mcp_execution_failed',
278 $throwable->getMessage(),
279 array( 'error_type' => get_class( $throwable ) )
280 );
281 }
282 }
283
284 if ( null !== $this->handler ) {
285 try {
286 return call_user_func( $this->handler, $arguments );
287 } catch ( \Throwable $throwable ) {
288 return new WP_Error(
289 'mcp_execution_failed',
290 $throwable->getMessage(),
291 array( 'error_type' => get_class( $throwable ) )
292 );
293 }
294 }
295
296 return new WP_Error( 'mcp_resource_no_handler', 'No resource execution strategy configured.' );
297 }
298
299 /**
300 * Check whether the current request has permission to read this resource.
301 *
302 * @param mixed $arguments Read arguments (may be empty).
303 *
304 * @return bool|\WP_Error
305 */
306 public function check_permission( $arguments ) {
307 // Ability-backed resources match existing behavior: no args passed to abilities.
308 if ( null !== $this->ability ) {
309 try {
310 return $this->ability->check_permissions();
311 } catch ( \Throwable $throwable ) {
312 return new WP_Error(
313 'mcp_permission_check_failed',
314 $throwable->getMessage(),
315 array( 'error_type' => get_class( $throwable ) )
316 );
317 }
318 }
319
320 if ( null !== $this->permission_callback ) {
321 try {
322 $result = call_user_func( $this->permission_callback, $arguments );
323
324 return $result instanceof WP_Error ? $result : (bool) $result;
325 } catch ( \Throwable $throwable ) {
326 return new WP_Error(
327 'mcp_permission_check_failed',
328 $throwable->getMessage(),
329 array( 'error_type' => get_class( $throwable ) )
330 );
331 }
332 }
333
334 return new WP_Error(
335 'mcp_permission_denied',
336 'Access denied.',
337 array( 'failure_reason' => FailureReason::NO_PERMISSION_STRATEGY )
338 );
339 }
340
341 /**
342 * Get internal adapter metadata for this resource.
343 *
344 * @return array<string, mixed>
345 */
346 public function get_adapter_meta(): array {
347 return $this->adapter_meta;
348 }
349
350 /**
351 * Get observability context tags for logging/metrics.
352 *
353 * @return array<string, mixed>
354 */
355 public function get_observability_context(): array {
356 return $this->observability_context;
357 }
358 }
359