PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
4.3.2 4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 All 455 releases
elementor / vendor / wordpress / mcp-adapter / includes / Domain / Resources / McpResource.php

McpResource.php in Elementor Website Builder – more than just a page builder 4.3.0, at vendor/wordpress/mcp-adapter/includes/Domain/Resources/McpResource.php

360 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 when non-empty. The value itself is not checked.
153 if ( isset( $config['mimeType'] ) ) {
154 $mime_type = trim( $config['mimeType'] );
155 if ( '' !== $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 $resource_meta = McpValidator::normalize_meta( $config['meta'] ?? null );
174 if ( null !== $resource_meta ) {
175 $resource_data['_meta'] = $resource_meta;
176 }
177
178 // Create the Resource DTO - wrap in try-catch since Annotations::fromArray() and ResourceDto::fromArray() can throw.
179 try {
180 // Process annotations inside try-catch since Annotations::fromArray() can throw.
181 if ( isset( $config['annotations'] ) && is_array( $config['annotations'] ) && ! empty( $config['annotations'] ) ) {
182 $resource_data['annotations'] = Annotations::fromArray( $config['annotations'] );
183 }
184
185 $resource = ResourceDto::fromArray( $resource_data );
186 } catch ( \Throwable $e ) {
187 return new WP_Error(
188 'mcp_resource_dto_creation_failed',
189 sprintf(
190 /* translators: %s: error message */
191 __( 'Failed to create Resource DTO: %s', 'mcp-adapter' ),
192 $e->getMessage()
193 ),
194 array( 'exception' => $e )
195 );
196 }
197
198 // Optional deep validation if enabled.
199 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
200 if ( $mcp_validation_enabled ) {
201 $validation_result = McpResourceValidator::validate_resource_dto( $resource );
202 if ( is_wp_error( $validation_result ) ) {
203 return $validation_result;
204 }
205 }
206
207 $instance = new self( $resource );
208 $instance->handler = $config['handler'];
209
210 if ( isset( $config['permission'] ) && is_callable( $config['permission'] ) ) {
211 $instance->permission_callback = $config['permission'];
212 }
213
214 $instance->observability_context = array(
215 'component_type' => 'resource',
216 'resource_uri' => $uri,
217 'source' => 'array',
218 );
219
220 return $instance;
221 }
222
223 /**
224 * Create an ability-backed MCP resource.
225 *
226 * @param \WP_Ability $ability WordPress ability.
227 * @param \WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null $error_handler Optional error handler.
228 *
229 * @return self|\WP_Error
230 */
231 public static function fromAbility( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null ) {
232 $resource_data = RegisterAbilityAsMcpResource::build( $ability, $error_handler );
233 if ( $resource_data instanceof WP_Error ) {
234 return $resource_data;
235 }
236
237 $instance = new self( $resource_data['resource'] );
238 $instance->adapter_meta = $resource_data['adapter_meta'];
239 $instance->ability = $ability;
240
241 $instance->observability_context = array(
242 'component_type' => 'resource',
243 'resource_uri' => $resource_data['resource']->getUri(),
244 'ability_name' => $ability->get_name(),
245 'source' => 'ability',
246 );
247
248 return $instance;
249 }
250
251 // =========================================================================
252 // McpComponentInterface Implementation
253 // =========================================================================
254
255 /**
256 * Get the clean protocol DTO for MCP responses.
257 *
258 * @return \WP\McpSchema\Server\Resources\DTO\Resource
259 */
260 public function get_protocol_dto(): ResourceDto {
261 return $this->mcp_resource_dto;
262 }
263
264 /**
265 * Execute the resource read.
266 *
267 * @param mixed $arguments Read arguments (may be empty).
268 *
269 * @return mixed
270 */
271 public function execute( $arguments ) {
272 // Ability-backed resources match existing behavior: no args passed to abilities.
273 if ( null !== $this->ability ) {
274 try {
275 return $this->ability->execute();
276 } catch ( \Throwable $throwable ) {
277 return new WP_Error(
278 'mcp_execution_failed',
279 $throwable->getMessage(),
280 array( 'error_type' => get_class( $throwable ) )
281 );
282 }
283 }
284
285 if ( null !== $this->handler ) {
286 try {
287 return call_user_func( $this->handler, $arguments );
288 } catch ( \Throwable $throwable ) {
289 return new WP_Error(
290 'mcp_execution_failed',
291 $throwable->getMessage(),
292 array( 'error_type' => get_class( $throwable ) )
293 );
294 }
295 }
296
297 return new WP_Error( 'mcp_resource_no_handler', 'No resource execution strategy configured.' );
298 }
299
300 /**
301 * Check whether the current request has permission to read this resource.
302 *
303 * @param mixed $arguments Read arguments (may be empty).
304 *
305 * @return bool|\WP_Error
306 */
307 public function check_permission( $arguments ) {
308 // Ability-backed resources match existing behavior: no args passed to abilities.
309 if ( null !== $this->ability ) {
310 try {
311 return $this->ability->check_permissions();
312 } catch ( \Throwable $throwable ) {
313 return new WP_Error(
314 'mcp_permission_check_failed',
315 $throwable->getMessage(),
316 array( 'error_type' => get_class( $throwable ) )
317 );
318 }
319 }
320
321 if ( null !== $this->permission_callback ) {
322 try {
323 $result = call_user_func( $this->permission_callback, $arguments );
324
325 return $result instanceof WP_Error ? $result : (bool) $result;
326 } catch ( \Throwable $throwable ) {
327 return new WP_Error(
328 'mcp_permission_check_failed',
329 $throwable->getMessage(),
330 array( 'error_type' => get_class( $throwable ) )
331 );
332 }
333 }
334
335 return new WP_Error(
336 'mcp_permission_denied',
337 'Access denied.',
338 array( 'failure_reason' => FailureReason::NO_PERMISSION_STRATEGY )
339 );
340 }
341
342 /**
343 * Get internal adapter metadata for this resource.
344 *
345 * @return array<string, mixed>
346 */
347 public function get_adapter_meta(): array {
348 return $this->adapter_meta;
349 }
350
351 /**
352 * Get observability context tags for logging/metrics.
353 *
354 * @return array<string, mixed>
355 */
356 public function get_observability_context(): array {
357 return $this->observability_context;
358 }
359 }
360