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.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 4.0.8 All 454 releases
elementor / vendor / wordpress / mcp-adapter / includes / Domain / Prompts / McpPrompt.php

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

481 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 $prompt_meta = McpValidator::normalize_meta( $config['meta'] ?? null );
165 if ( null !== $prompt_meta ) {
166 $prompt_data['_meta'] = $prompt_meta;
167 }
168
169 if ( null !== $valid_icons ) {
170 $prompt_data['icons'] = $valid_icons;
171 }
172
173 // Create the Prompt DTO - wrap in try-catch since PromptArgument::fromArray() and PromptDto::fromArray() can throw.
174 try {
175 // Process arguments inside try-catch since PromptArgument::fromArray() can throw.
176 if ( isset( $config['arguments'] ) && is_array( $config['arguments'] ) && ! empty( $config['arguments'] ) ) {
177 $prompt_data['arguments'] = array_map(
178 static function ( array $arg ): PromptArgument {
179 return PromptArgument::fromArray(
180 array(
181 'name' => $arg['name'],
182 'title' => $arg['title'] ?? null,
183 'description' => $arg['description'] ?? null,
184 'required' => $arg['required'] ?? null,
185 )
186 );
187 },
188 $config['arguments']
189 );
190 }
191
192 $prompt = PromptDto::fromArray( $prompt_data );
193 } catch ( \Throwable $e ) {
194 return new WP_Error(
195 'mcp_prompt_dto_creation_failed',
196 sprintf(
197 /* translators: %s: error message */
198 __( 'Failed to create Prompt DTO: %s', 'mcp-adapter' ),
199 $e->getMessage()
200 ),
201 array( 'exception' => $e )
202 );
203 }
204
205 // Optional deep validation if enabled.
206 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
207 if ( $mcp_validation_enabled ) {
208 $validation_result = McpPromptValidator::validate_prompt_dto( $prompt );
209 if ( is_wp_error( $validation_result ) ) {
210 return $validation_result;
211 }
212 }
213
214 $instance = new self( $prompt );
215 $instance->handler = $config['handler'];
216
217 if ( isset( $config['permission'] ) && is_callable( $config['permission'] ) ) {
218 $instance->permission_callback = $config['permission'];
219 }
220
221 $instance->observability_context = array(
222 'component_type' => 'prompt',
223 'prompt_name' => $config['name'],
224 'source' => 'array',
225 );
226
227 return $instance;
228 }
229
230 /**
231 * Create an ability-backed MCP prompt.
232 *
233 * @param \WP_Ability $ability WordPress ability.
234 *
235 * @return self|\WP_Error
236 */
237 public static function fromAbility( \WP_Ability $ability ) {
238 $prompt_data = RegisterAbilityAsMcpPrompt::build( $ability );
239 if ( $prompt_data instanceof WP_Error ) {
240 return $prompt_data;
241 }
242
243 $instance = new self( $prompt_data['prompt'] );
244 $instance->adapter_meta = $prompt_data['adapter_meta'];
245 $instance->ability = $ability;
246
247 $instance->observability_context = array(
248 'component_type' => 'prompt',
249 'prompt_name' => $prompt_data['prompt']->getName(),
250 'ability_name' => $ability->get_name(),
251 'source' => 'ability',
252 );
253
254 return $instance;
255 }
256
257 /**
258 * Create a builder-backed MCP prompt.
259 *
260 * @param \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface $builder Builder instance.
261 *
262 * @return self|\WP_Error
263 */
264 public static function fromBuilder( McpPromptBuilderInterface $builder ) {
265 try {
266 $prompt = $builder->build();
267 } catch ( \Throwable $throwable ) {
268 return new WP_Error(
269 'mcp_prompt_builder_failed',
270 $throwable->getMessage(),
271 array( 'error_type' => get_class( $throwable ) )
272 );
273 }
274
275 // Optional deep validation if enabled.
276 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
277 if ( $mcp_validation_enabled ) {
278 $validation_result = McpPromptValidator::validate_prompt_dto( $prompt );
279 if ( is_wp_error( $validation_result ) ) {
280 return $validation_result;
281 }
282 }
283
284 $instance = new self( $prompt );
285 $instance->builder = $builder;
286
287 $instance->adapter_meta = array(
288 'source' => 'builder',
289 'builder_class' => get_class( $builder ),
290 );
291
292 $instance->observability_context = array(
293 'component_type' => 'prompt',
294 'prompt_name' => $prompt->getName(),
295 'source' => 'builder',
296 );
297
298 return $instance;
299 }
300
301 // =========================================================================
302 // McpComponentInterface Implementation
303 // =========================================================================
304
305 /**
306 * Get the clean protocol DTO for MCP responses.
307 *
308 * @return \WP\McpSchema\Server\Prompts\DTO\Prompt
309 */
310 public function get_protocol_dto(): PromptDto {
311 return $this->prompt;
312 }
313
314 /**
315 * Execute the prompt.
316 *
317 * @param mixed $arguments Prompt arguments.
318 *
319 * @return mixed
320 */
321 public function execute( $arguments ) {
322 $args = $this->unwrap_input_if_needed( $arguments );
323 $args = is_array( $args ) ? $args : array();
324
325 if ( null !== $this->ability ) {
326 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
327
328 try {
329 $result = $this->ability->execute( $args );
330 } catch ( \Throwable $throwable ) {
331 return new WP_Error(
332 'mcp_execution_failed',
333 $throwable->getMessage(),
334 array( 'error_type' => get_class( $throwable ) )
335 );
336 }
337 } elseif ( null !== $this->builder ) {
338 try {
339 $result = $this->builder->handle( $args );
340 } catch ( \Throwable $throwable ) {
341 return new WP_Error(
342 'mcp_execution_failed',
343 $throwable->getMessage(),
344 array( 'error_type' => get_class( $throwable ) )
345 );
346 }
347 } elseif ( null !== $this->handler ) {
348 try {
349 $result = call_user_func( $this->handler, $args );
350 } catch ( \Throwable $throwable ) {
351 return new WP_Error(
352 'mcp_execution_failed',
353 $throwable->getMessage(),
354 array( 'error_type' => get_class( $throwable ) )
355 );
356 }
357 } else {
358 return new WP_Error( 'mcp_prompt_no_handler', 'No prompt execution strategy configured.' );
359 }
360
361 if ( $result instanceof WP_Error ) {
362 return $result;
363 }
364
365 if ( ! is_array( $result ) ) {
366 $result = array( 'result' => $result );
367 }
368
369 return $result;
370 }
371
372 /**
373 * Unwrap prompt input arguments when the input schema was transformed (flattened → object wrapper).
374 *
375 * @param mixed $arguments Raw prompt arguments.
376 *
377 * @return mixed
378 */
379 private function unwrap_input_if_needed( $arguments ) {
380 $is_transformed = true === ( $this->adapter_meta['input_schema_transformed'] ?? false );
381
382 if ( ! $is_transformed ) {
383 return $arguments;
384 }
385
386 $wrapper = $this->adapter_meta['input_schema_wrapper'] ?? 'input';
387 $wrapper = is_string( $wrapper ) && '' !== trim( $wrapper ) ? $wrapper : 'input';
388
389 return is_array( $arguments ) ? ( $arguments[ $wrapper ] ?? null ) : null;
390 }
391
392 /**
393 * Check whether the current request has permission to execute this prompt.
394 *
395 * @param mixed $arguments Prompt arguments.
396 *
397 * @return bool|\WP_Error
398 */
399 public function check_permission( $arguments ) {
400 $args = $this->unwrap_input_if_needed( $arguments );
401 $args = is_array( $args ) ? $args : array();
402
403 if ( null !== $this->ability ) {
404 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
405
406 try {
407 return $this->ability->check_permissions( $args );
408 } catch ( \Throwable $throwable ) {
409 return new WP_Error(
410 'mcp_permission_check_failed',
411 $throwable->getMessage(),
412 array( 'error_type' => get_class( $throwable ) )
413 );
414 }
415 }
416
417 if ( null !== $this->builder ) {
418 try {
419 return $this->builder->has_permission( $args );
420 } catch ( \Throwable $throwable ) {
421 return new WP_Error(
422 'mcp_permission_check_failed',
423 $throwable->getMessage(),
424 array( 'error_type' => get_class( $throwable ) )
425 );
426 }
427 }
428
429 if ( null !== $this->permission_callback ) {
430 try {
431 $result = call_user_func( $this->permission_callback, $args );
432
433 return $result instanceof WP_Error ? $result : (bool) $result;
434 } catch ( \Throwable $throwable ) {
435 return new WP_Error(
436 'mcp_permission_check_failed',
437 $throwable->getMessage(),
438 array( 'error_type' => get_class( $throwable ) )
439 );
440 }
441 }
442
443 return new WP_Error(
444 'mcp_permission_denied',
445 'Access denied.',
446 array( 'failure_reason' => FailureReason::NO_PERMISSION_STRATEGY )
447 );
448 }
449
450 /**
451 * Get internal adapter metadata for this prompt.
452 *
453 * @return array<string, mixed>
454 */
455 public function get_adapter_meta(): array {
456 return $this->adapter_meta;
457 }
458
459 /**
460 * Get observability context tags for logging/metrics.
461 *
462 * @return array<string, mixed>
463 */
464 public function get_observability_context(): array {
465 return $this->observability_context;
466 }
467
468 // =========================================================================
469 // Private Helper Methods
470 // =========================================================================
471
472 /**
473 * Get the underlying builder instance, when builder-backed.
474 *
475 * @return \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface|null
476 */
477 public function get_builder(): ?McpPromptBuilderInterface {
478 return $this->builder;
479 }
480 }
481