PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.4
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.4
3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 All 197 releases
convertkit / vendor / wordpress / mcp-adapter / includes / Domain / Resources / McpResource.php

McpResource.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.4, 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