PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.3
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 2.3.1 All 196 releases
convertkit / vendor / wordpress / mcp-adapter / includes / Domain / Prompts / McpPrompt.php

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