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 / Prompts / McpPrompt.php

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

480 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * MCP Prompt component.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Domain\Prompts;
12
13 use WP\MCP\Domain\Contracts\McpComponentInterface;
14 use WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface;
15 use WP\MCP\Domain\Utils\AbilityArgumentNormalizer;
16 use WP\MCP\Domain\Utils\McpValidator;
17 use WP\MCP\Infrastructure\Observability\FailureReason;
18 use WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
19 use WP\McpSchema\Server\Prompts\DTO\PromptArgument;
20 use WP_Error;
21
22 /**
23 * Prompt component providing unified execution and permission checks.
24 *
25 * This class supports multiple ways to register prompts:
26 *
27 * 1. Array configuration:
28 * ```php
29 * $prompt = McpPrompt::fromArray([
30 * 'name' => 'code-review',
31 * 'title' => 'Code Review',
32 * 'description' => 'Generate a comprehensive code review',
33 * 'arguments' => [
34 * ['name' => 'code', 'description' => 'The code to review', 'required' => true],
35 * ],
36 * 'handler' => fn($args) => ['messages' => [...]],
37 * 'permission' => fn() => true,
38 * ]);
39 * ```
40 *
41 * 2. From WordPress Ability (ability-backed):
42 * ```php
43 * $prompt = McpPrompt::fromAbility($ability);
44 * ```
45 *
46 * 3. From prompt builder (builder-backed compatibility):
47 * ```php
48 * $prompt = McpPrompt::fromBuilder($builder);
49 * ```
50 *
51 * McpPrompt wraps a protocol-only PromptDto for MCP serialization. Internal
52 * adapter metadata and execution wiring live on this class and are never
53 * exposed to MCP clients. Use get_protocol_dto() for protocol responses.
54 *
55 * @since 0.5.0
56 */
57 final class McpPrompt implements McpComponentInterface {
58
59
60 // =========================================================================
61 // Runtime Properties
62 // =========================================================================
63
64 /**
65 * Clean Prompt DTO (protocol-only).
66 *
67 * @var \WP\McpSchema\Server\Prompts\DTO\Prompt
68 */
69 private PromptDto $prompt;
70
71 /**
72 * Ability used for execution/permission checks (ability-backed prompts).
73 *
74 * @var \WP_Ability|null
75 */
76 private ?\WP_Ability $ability = null;
77
78 /**
79 * Builder instance (builder-backed prompts).
80 *
81 * @var \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface|null
82 */
83 private ?McpPromptBuilderInterface $builder = null;
84
85 /**
86 * Direct execution handler (callable-backed prompts).
87 *
88 * @var callable|null
89 */
90 private $handler = null;
91
92 /**
93 * Direct permission callback (callable-backed prompts).
94 *
95 * @var callable|null
96 */
97 private $permission_callback = null;
98
99 /**
100 * Internal adapter metadata (never exposed to clients).
101 *
102 * @var array<string, mixed>
103 */
104 private array $adapter_meta = array();
105
106 /**
107 * Observability context tags for logging/metrics.
108 *
109 * @var array<string, mixed>
110 */
111 private array $observability_context = array();
112
113 // =========================================================================
114 // Constructor
115 // =========================================================================
116
117 /**
118 * Private constructor - use factory methods.
119 *
120 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The Prompt DTO.
121 */
122 private function __construct( PromptDto $prompt ) {
123 $this->prompt = $prompt;
124 }
125
126 // =========================================================================
127 // Factory Methods
128 // =========================================================================
129
130 /**
131 * Create a prompt definition from an array configuration.
132 *
133 * @param array $config The prompt configuration array.
134 *
135 * @return self|\WP_Error
136 */
137 public static function fromArray( array $config ) {
138 if ( empty( $config['name'] ) ) {
139 return new WP_Error( 'mcp_prompt_missing_name', 'Prompt configuration must include a "name" field.' );
140 }
141
142 if ( ! isset( $config['handler'] ) || ! is_callable( $config['handler'] ) ) {
143 return new WP_Error( 'mcp_prompt_missing_handler', 'Prompt configuration must include a callable "handler" field.' );
144 }
145
146 // Validate and prepare icons if set.
147 $valid_icons = null;
148 if ( isset( $config['icons'] ) && is_array( $config['icons'] ) && ! empty( $config['icons'] ) ) {
149 $icons_result = McpValidator::validate_icons_array( $config['icons'] );
150 if ( ! empty( $icons_result['valid'] ) ) {
151 $valid_icons = $icons_result['valid'];
152 }
153 }
154
155 $prompt_data = array(
156 'name' => $config['name'],
157 'description' => $config['description'] ?? null,
158 );
159
160 if ( isset( $config['title'] ) ) {
161 $prompt_data['title'] = $config['title'];
162 }
163
164 if ( isset( $config['meta'] ) && is_array( $config['meta'] ) && ! empty( $config['meta'] ) ) {
165 $prompt_data['_meta'] = $config['meta'];
166 }
167
168 if ( null !== $valid_icons ) {
169 $prompt_data['icons'] = $valid_icons;
170 }
171
172 // Create the Prompt DTO - wrap in try-catch since PromptArgument::fromArray() and PromptDto::fromArray() can throw.
173 try {
174 // Process arguments inside try-catch since PromptArgument::fromArray() can throw.
175 if ( isset( $config['arguments'] ) && is_array( $config['arguments'] ) && ! empty( $config['arguments'] ) ) {
176 $prompt_data['arguments'] = array_map(
177 static function ( array $arg ): PromptArgument {
178 return PromptArgument::fromArray(
179 array(
180 'name' => $arg['name'],
181 'title' => $arg['title'] ?? null,
182 'description' => $arg['description'] ?? null,
183 'required' => $arg['required'] ?? null,
184 )
185 );
186 },
187 $config['arguments']
188 );
189 }
190
191 $prompt = PromptDto::fromArray( $prompt_data );
192 } catch ( \Throwable $e ) {
193 return new WP_Error(
194 'mcp_prompt_dto_creation_failed',
195 sprintf(
196 /* translators: %s: error message */
197 __( 'Failed to create Prompt DTO: %s', 'mcp-adapter' ),
198 $e->getMessage()
199 ),
200 array( 'exception' => $e )
201 );
202 }
203
204 // Optional deep validation if enabled.
205 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
206 if ( $mcp_validation_enabled ) {
207 $validation_result = McpPromptValidator::validate_prompt_dto( $prompt );
208 if ( is_wp_error( $validation_result ) ) {
209 return $validation_result;
210 }
211 }
212
213 $instance = new self( $prompt );
214 $instance->handler = $config['handler'];
215
216 if ( isset( $config['permission'] ) && is_callable( $config['permission'] ) ) {
217 $instance->permission_callback = $config['permission'];
218 }
219
220 $instance->observability_context = array(
221 'component_type' => 'prompt',
222 'prompt_name' => $config['name'],
223 'source' => 'array',
224 );
225
226 return $instance;
227 }
228
229 /**
230 * Create an ability-backed MCP prompt.
231 *
232 * @param \WP_Ability $ability WordPress ability.
233 *
234 * @return self|\WP_Error
235 */
236 public static function fromAbility( \WP_Ability $ability ) {
237 $prompt_data = RegisterAbilityAsMcpPrompt::build( $ability );
238 if ( $prompt_data instanceof WP_Error ) {
239 return $prompt_data;
240 }
241
242 $instance = new self( $prompt_data['prompt'] );
243 $instance->adapter_meta = $prompt_data['adapter_meta'];
244 $instance->ability = $ability;
245
246 $instance->observability_context = array(
247 'component_type' => 'prompt',
248 'prompt_name' => $prompt_data['prompt']->getName(),
249 'ability_name' => $ability->get_name(),
250 'source' => 'ability',
251 );
252
253 return $instance;
254 }
255
256 /**
257 * Create a builder-backed MCP prompt.
258 *
259 * @param \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface $builder Builder instance.
260 *
261 * @return self|\WP_Error
262 */
263 public static function fromBuilder( McpPromptBuilderInterface $builder ) {
264 try {
265 $prompt = $builder->build();
266 } catch ( \Throwable $throwable ) {
267 return new WP_Error(
268 'mcp_prompt_builder_failed',
269 $throwable->getMessage(),
270 array( 'error_type' => get_class( $throwable ) )
271 );
272 }
273
274 // Optional deep validation if enabled.
275 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
276 if ( $mcp_validation_enabled ) {
277 $validation_result = McpPromptValidator::validate_prompt_dto( $prompt );
278 if ( is_wp_error( $validation_result ) ) {
279 return $validation_result;
280 }
281 }
282
283 $instance = new self( $prompt );
284 $instance->builder = $builder;
285
286 $instance->adapter_meta = array(
287 'source' => 'builder',
288 'builder_class' => get_class( $builder ),
289 );
290
291 $instance->observability_context = array(
292 'component_type' => 'prompt',
293 'prompt_name' => $prompt->getName(),
294 'source' => 'builder',
295 );
296
297 return $instance;
298 }
299
300 // =========================================================================
301 // McpComponentInterface Implementation
302 // =========================================================================
303
304 /**
305 * Get the clean protocol DTO for MCP responses.
306 *
307 * @return \WP\McpSchema\Server\Prompts\DTO\Prompt
308 */
309 public function get_protocol_dto(): PromptDto {
310 return $this->prompt;
311 }
312
313 /**
314 * Execute the prompt.
315 *
316 * @param mixed $arguments Prompt arguments.
317 *
318 * @return mixed
319 */
320 public function execute( $arguments ) {
321 $args = $this->unwrap_input_if_needed( $arguments );
322 $args = is_array( $args ) ? $args : array();
323
324 if ( null !== $this->ability ) {
325 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
326
327 try {
328 $result = $this->ability->execute( $args );
329 } catch ( \Throwable $throwable ) {
330 return new WP_Error(
331 'mcp_execution_failed',
332 $throwable->getMessage(),
333 array( 'error_type' => get_class( $throwable ) )
334 );
335 }
336 } elseif ( null !== $this->builder ) {
337 try {
338 $result = $this->builder->handle( $args );
339 } catch ( \Throwable $throwable ) {
340 return new WP_Error(
341 'mcp_execution_failed',
342 $throwable->getMessage(),
343 array( 'error_type' => get_class( $throwable ) )
344 );
345 }
346 } elseif ( null !== $this->handler ) {
347 try {
348 $result = call_user_func( $this->handler, $args );
349 } catch ( \Throwable $throwable ) {
350 return new WP_Error(
351 'mcp_execution_failed',
352 $throwable->getMessage(),
353 array( 'error_type' => get_class( $throwable ) )
354 );
355 }
356 } else {
357 return new WP_Error( 'mcp_prompt_no_handler', 'No prompt execution strategy configured.' );
358 }
359
360 if ( $result instanceof WP_Error ) {
361 return $result;
362 }
363
364 if ( ! is_array( $result ) ) {
365 $result = array( 'result' => $result );
366 }
367
368 return $result;
369 }
370
371 /**
372 * Unwrap prompt input arguments when the input schema was transformed (flattened → object wrapper).
373 *
374 * @param mixed $arguments Raw prompt arguments.
375 *
376 * @return mixed
377 */
378 private function unwrap_input_if_needed( $arguments ) {
379 $is_transformed = true === ( $this->adapter_meta['input_schema_transformed'] ?? false );
380
381 if ( ! $is_transformed ) {
382 return $arguments;
383 }
384
385 $wrapper = $this->adapter_meta['input_schema_wrapper'] ?? 'input';
386 $wrapper = is_string( $wrapper ) && '' !== trim( $wrapper ) ? $wrapper : 'input';
387
388 return is_array( $arguments ) ? ( $arguments[ $wrapper ] ?? null ) : null;
389 }
390
391 /**
392 * Check whether the current request has permission to execute this prompt.
393 *
394 * @param mixed $arguments Prompt arguments.
395 *
396 * @return bool|\WP_Error
397 */
398 public function check_permission( $arguments ) {
399 $args = $this->unwrap_input_if_needed( $arguments );
400 $args = is_array( $args ) ? $args : array();
401
402 if ( null !== $this->ability ) {
403 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
404
405 try {
406 return $this->ability->check_permissions( $args );
407 } catch ( \Throwable $throwable ) {
408 return new WP_Error(
409 'mcp_permission_check_failed',
410 $throwable->getMessage(),
411 array( 'error_type' => get_class( $throwable ) )
412 );
413 }
414 }
415
416 if ( null !== $this->builder ) {
417 try {
418 return $this->builder->has_permission( $args );
419 } catch ( \Throwable $throwable ) {
420 return new WP_Error(
421 'mcp_permission_check_failed',
422 $throwable->getMessage(),
423 array( 'error_type' => get_class( $throwable ) )
424 );
425 }
426 }
427
428 if ( null !== $this->permission_callback ) {
429 try {
430 $result = call_user_func( $this->permission_callback, $args );
431
432 return $result instanceof WP_Error ? $result : (bool) $result;
433 } catch ( \Throwable $throwable ) {
434 return new WP_Error(
435 'mcp_permission_check_failed',
436 $throwable->getMessage(),
437 array( 'error_type' => get_class( $throwable ) )
438 );
439 }
440 }
441
442 return new WP_Error(
443 'mcp_permission_denied',
444 'Access denied.',
445 array( 'failure_reason' => FailureReason::NO_PERMISSION_STRATEGY )
446 );
447 }
448
449 /**
450 * Get internal adapter metadata for this prompt.
451 *
452 * @return array<string, mixed>
453 */
454 public function get_adapter_meta(): array {
455 return $this->adapter_meta;
456 }
457
458 /**
459 * Get observability context tags for logging/metrics.
460 *
461 * @return array<string, mixed>
462 */
463 public function get_observability_context(): array {
464 return $this->observability_context;
465 }
466
467 // =========================================================================
468 // Private Helper Methods
469 // =========================================================================
470
471 /**
472 * Get the underlying builder instance, when builder-backed.
473 *
474 * @return \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface|null
475 */
476 public function get_builder(): ?McpPromptBuilderInterface {
477 return $this->builder;
478 }
479 }
480