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

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

613 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Prompts method handlers for MCP requests.
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Handlers\Prompts;
11
12 use WP\MCP\Core\McpServer;
13 use WP\MCP\Handlers\HandlerHelperTrait;
14 use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
15 use WP\McpSchema\Server\Prompts\DTO\GetPromptResult;
16 use WP\McpSchema\Server\Prompts\DTO\ListPromptsResult;
17 use WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
18 use WP\McpSchema\Server\Prompts\DTO\PromptMessage;
19
20 /**
21 * Handles prompts-related MCP methods.
22 *
23 * @since 0.5.0
24 */
25 class PromptsHandler {
26 use HandlerHelperTrait;
27
28 /**
29 * Valid content types from ContentBlockFactory.
30 *
31 * @var list<string>
32 */
33 private static array $valid_content_types = array( 'text', 'image', 'audio', 'resource_link', 'resource' );
34
35 /**
36 * Valid role values for PromptMessage.
37 *
38 * @var list<string>
39 */
40 private static array $valid_roles = array( 'user', 'assistant' );
41
42 /**
43 * Default role for messages when not specified.
44 *
45 * @var string
46 */
47 private static string $default_role = 'user';
48
49 /**
50 * The WordPress MCP instance.
51 *
52 * @var \WP\MCP\Core\McpServer
53 */
54 private McpServer $mcp;
55
56 /**
57 * Constructor.
58 *
59 * @param \WP\MCP\Core\McpServer $mcp The WordPress MCP instance.
60 */
61 public function __construct( McpServer $mcp ) {
62 $this->mcp = $mcp;
63 }
64
65 /**
66 * Handles the prompts/list request.
67 *
68 * @return \WP\McpSchema\Server\Prompts\DTO\ListPromptsResult Response with prompts list DTO.
69 */
70 public function list_prompts(): ListPromptsResult {
71 $prompts = array_values( $this->mcp->get_prompts() );
72
73 /**
74 * Filters the list of prompts before returning to the client.
75 *
76 * Use this filter to filter prompts by context, add dynamic prompts,
77 * or reorder the prompts list.
78 *
79 * @since 0.5.0
80 *
81 * @param array<\WP\McpSchema\Server\Prompts\DTO\Prompt> $prompts Array of Prompt DTOs.
82 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
83 */
84 $prompts = $this->validate_filtered_list(
85 apply_filters( 'mcp_adapter_prompts_list', $prompts, $this->mcp ),
86 $prompts,
87 'mcp_adapter_prompts_list',
88 $this->mcp->get_error_handler()
89 );
90
91 return ListPromptsResult::fromArray(
92 array(
93 'prompts' => $prompts,
94 )
95 );
96 }
97
98 /**
99 * Handles the prompts/get request.
100 *
101 * @param array $params Request parameters.
102 * @param string|int|null $request_id Optional. The request ID for JSON-RPC. Default 0.
103 *
104 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult|\WP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse Response with prompt execution results or error.
105 */
106 public function get_prompt( array $params, $request_id = 0 ) {
107 // Extract parameters using helper method.
108 $request_params = $this->extract_params( $params );
109
110 if ( ! isset( $request_params['name'] ) ) {
111 return McpErrorFactory::missing_parameter( $request_id, 'name' );
112 }
113
114 $prompt_name = (string) $request_params['name'];
115 $prompt_name = trim( $prompt_name );
116
117 if ( isset( $request_params['arguments'] ) && ! is_array( $request_params['arguments'] ) ) {
118 return McpErrorFactory::invalid_params( $request_id, 'arguments must be an object' );
119 }
120
121 $mcp_prompt = $this->mcp->get_mcp_prompt( $prompt_name );
122
123 if ( ! $mcp_prompt ) {
124 return McpErrorFactory::prompt_not_found( $request_id, $prompt_name );
125 }
126
127 /** @var \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt */
128 $prompt = $mcp_prompt->get_protocol_dto();
129
130 // Get the arguments for the prompt.
131 $arguments = $request_params['arguments'] ?? array();
132
133 try {
134 $permission = $mcp_prompt->check_permission( $arguments );
135 if ( true !== $permission ) {
136 $error_message = 'Access denied for prompt: ' . $prompt_name;
137 if ( is_wp_error( $permission ) ) {
138 $error_message = $permission->get_error_message();
139 }
140
141 return McpErrorFactory::permission_denied( $request_id, $error_message );
142 }
143
144 /**
145 * Filters prompt arguments before execution, or short-circuits execution entirely.
146 *
147 * Return the (optionally modified) arguments array to proceed with execution,
148 * or return a WP_Error to block execution and return an error to the client.
149 *
150 * @since 0.5.0
151 *
152 * @param array $arguments The prompt arguments.
153 * @param string $prompt_name The prompt name being retrieved.
154 * @param \WP\MCP\Domain\Prompts\McpPrompt $mcp_prompt The MCP prompt instance.
155 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
156 */
157 $arguments = apply_filters( 'mcp_adapter_pre_prompt_get', $arguments, $prompt_name, $mcp_prompt, $this->mcp );
158
159 // Allow pre-filter to short-circuit execution by returning WP_Error.
160 if ( is_wp_error( $arguments ) ) {
161 return McpErrorFactory::internal_error( $request_id, $arguments->get_error_message() );
162 }
163
164 $result = $mcp_prompt->execute( $arguments );
165
166 /**
167 * Filters the prompt execution result before normalization.
168 *
169 * Use this filter for message transformation, context injection,
170 * content enrichment, or audit logging.
171 *
172 * @since 0.5.0
173 *
174 * @param mixed|\WP_Error $result The raw execution result (may be WP_Error).
175 * @param array $arguments The prompt arguments used.
176 * @param string $prompt_name The prompt name.
177 * @param \WP\MCP\Domain\Prompts\McpPrompt $mcp_prompt The MCP prompt instance.
178 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
179 */
180 $result = apply_filters( 'mcp_adapter_prompt_get_result', $result, $arguments, $prompt_name, $mcp_prompt, $this->mcp );
181
182 if ( is_wp_error( $result ) ) {
183 $this->mcp->get_error_handler()->log(
184 'Prompt execution returned WP_Error',
185 array(
186 'prompt_name' => $prompt_name,
187 'error_code' => $result->get_error_code(),
188 'error_message' => $result->get_error_message(),
189 )
190 );
191
192 return McpErrorFactory::internal_error( $request_id, $result->get_error_message() );
193 }
194
195 return $this->normalize_result_to_dto( $result, $prompt, $prompt_name );
196 } catch ( \Throwable $e ) {
197 $this->mcp->get_error_handler()->log(
198 'Prompt execution failed',
199 array(
200 'prompt_name' => $prompt_name,
201 'arguments' => $arguments,
202 'error' => $e->getMessage(),
203 )
204 );
205
206 return McpErrorFactory::internal_error( $request_id, 'Prompt execution failed' );
207 }
208 }
209
210 // =========================================================================
211 // Result Normalization (Tiered Convenience Shortcuts)
212 // =========================================================================
213
214 /**
215 * Normalize and convert prompt execution result to GetPromptResult DTO.
216 *
217 * Supports tiered return formats:
218 * - Tier 1: Full MCP format with 'messages' array
219 * - Tier 2: Simple 'text' shorthand
220 * - Tier 3: Single message with 'role' and 'content'
221 * - Tier 4: Multi-text with 'texts' array
222 * - Tier 5: Fallback JSON encoding for arbitrary data
223 *
224 * @since 0.5.0
225 *
226 * @param array $result Raw result from prompt execution.
227 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO for description fallback.
228 * @param string $prompt_name Prompt name for logging.
229 *
230 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
231 */
232 private function normalize_result_to_dto(
233 array $result,
234 PromptDto $prompt,
235 string $prompt_name
236 ): GetPromptResult {
237 // Tier 1: Full MCP format with 'messages' array.
238 if ( isset( $result['messages'] ) && is_array( $result['messages'] ) ) {
239 return $this->normalize_tier1_messages( $result, $prompt, $prompt_name );
240 }
241
242 // Tier 2: Simple 'text' shorthand.
243 if ( isset( $result['text'] ) && is_string( $result['text'] ) ) {
244 return $this->normalize_tier2_text( $result, $prompt );
245 }
246
247 // Tier 3: Single message with 'role' key.
248 if ( isset( $result['role'] ) && isset( $result['content'] ) ) {
249 return $this->normalize_tier3_single_message( $result, $prompt, $prompt_name );
250 }
251
252 // Tier 4: Multi-text with 'texts' array.
253 if ( isset( $result['texts'] ) && is_array( $result['texts'] ) ) {
254 return $this->normalize_tier4_texts( $result, $prompt );
255 }
256
257 // Tier 5: Fallback - JSON encode arbitrary data.
258 return $this->normalize_tier5_fallback( $result, $prompt, $prompt_name );
259 }
260
261 /**
262 * Tier 1: Full MCP-compliant format with 'messages' array.
263 *
264 * @since 0.5.0
265 *
266 * @param array $result Raw result with 'messages' key.
267 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO.
268 * @param string $prompt_name Prompt name for logging.
269 *
270 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
271 */
272 private function normalize_tier1_messages(
273 array $result,
274 PromptDto $prompt,
275 string $prompt_name
276 ): GetPromptResult {
277 $message_dtos = array();
278
279 foreach ( $result['messages'] as $index => $message ) {
280 if ( ! is_array( $message ) ) {
281 $this->mcp->get_error_handler()->log(
282 'Invalid message structure in prompt result, skipping',
283 array(
284 'prompt_name' => $prompt_name,
285 'message_index' => $index,
286 'message_type' => gettype( $message ),
287 ),
288 'warning'
289 );
290 continue;
291 }
292
293 $message_dtos[] = $this->validate_and_create_message( $message, $prompt_name );
294 }
295
296 // Ensure we have at least one message.
297 if ( empty( $message_dtos ) ) {
298 $message_dtos[] = PromptMessage::fromArray(
299 array(
300 'role' => self::$default_role,
301 'content' => array(
302 'type' => 'text',
303 'text' => '(No messages returned)',
304 ),
305 )
306 );
307 }
308
309 return GetPromptResult::fromArray(
310 array(
311 'messages' => $message_dtos,
312 'description' => $result['description'] ?? $prompt->getDescription(),
313 )
314 );
315 }
316
317 /**
318 * Tier 2: Simple 'text' shorthand.
319 *
320 * Creates a single user message with text content.
321 *
322 * @since 0.5.0
323 *
324 * @param array $result Raw result with 'text' key.
325 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO.
326 *
327 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
328 */
329 private function normalize_tier2_text( array $result, PromptDto $prompt ): GetPromptResult {
330 $content = array(
331 'type' => 'text',
332 'text' => (string) $result['text'],
333 );
334
335 // Support optional annotations on the text.
336 if ( isset( $result['annotations'] ) && is_array( $result['annotations'] ) ) {
337 $content['annotations'] = $result['annotations'];
338 }
339
340 $message_dto = PromptMessage::fromArray(
341 array(
342 'role' => self::$default_role,
343 'content' => $content,
344 )
345 );
346
347 return GetPromptResult::fromArray(
348 array(
349 'messages' => array( $message_dto ),
350 'description' => $result['description'] ?? $prompt->getDescription(),
351 )
352 );
353 }
354
355 /**
356 * Tier 3: Single message with 'role' and 'content'.
357 *
358 * @since 0.5.0
359 *
360 * @param array $result Raw result with 'role' and 'content' keys.
361 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO.
362 * @param string $prompt_name Prompt name for logging.
363 *
364 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
365 */
366 private function normalize_tier3_single_message(
367 array $result,
368 PromptDto $prompt,
369 string $prompt_name
370 ): GetPromptResult {
371 $message_dto = $this->validate_and_create_message( $result, $prompt_name );
372
373 return GetPromptResult::fromArray(
374 array(
375 'messages' => array( $message_dto ),
376 'description' => $result['description'] ?? $prompt->getDescription(),
377 )
378 );
379 }
380
381 /**
382 * Tier 4: Multi-text with 'texts' array.
383 *
384 * Creates multiple messages with the same role.
385 *
386 * @since 0.5.0
387 *
388 * @param array $result Raw result with 'texts' key.
389 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO.
390 *
391 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
392 */
393 private function normalize_tier4_texts( array $result, PromptDto $prompt ): GetPromptResult {
394 $role = $this->validate_role( $result['role'] ?? self::$default_role, '' );
395 $message_dtos = array();
396
397 foreach ( $result['texts'] as $text ) {
398 if ( ! is_string( $text ) ) {
399 continue;
400 }
401
402 $message_dtos[] = PromptMessage::fromArray(
403 array(
404 'role' => $role,
405 'content' => array(
406 'type' => 'text',
407 'text' => $text,
408 ),
409 )
410 );
411 }
412
413 // Ensure we have at least one message.
414 if ( empty( $message_dtos ) ) {
415 $message_dtos[] = PromptMessage::fromArray(
416 array(
417 'role' => $role,
418 'content' => array(
419 'type' => 'text',
420 'text' => '(No texts provided)',
421 ),
422 )
423 );
424 }
425
426 return GetPromptResult::fromArray(
427 array(
428 'messages' => $message_dtos,
429 'description' => $result['description'] ?? $prompt->getDescription(),
430 )
431 );
432 }
433
434 /**
435 * Tier 5: Fallback - JSON encode arbitrary data.
436 *
437 * Used when no other tier matches. Logs an observability event.
438 *
439 * @since 0.5.0
440 *
441 * @param array $result Raw result (arbitrary structure).
442 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO.
443 * @param string $prompt_name Prompt name for logging.
444 *
445 * @return \WP\McpSchema\Server\Prompts\DTO\GetPromptResult
446 */
447 private function normalize_tier5_fallback(
448 array $result,
449 PromptDto $prompt,
450 string $prompt_name
451 ): GetPromptResult {
452 // Log observability event for fallback normalization.
453 $this->mcp->get_observability_handler()->record_event(
454 'prompt_result_fallback_normalization',
455 array(
456 'prompt_name' => $prompt_name,
457 'result_keys' => array_keys( $result ),
458 )
459 );
460
461 $json_content = wp_json_encode( $result, JSON_PRETTY_PRINT );
462 if ( false === $json_content ) {
463 $json_content = '{}';
464 }
465
466 $message_dto = PromptMessage::fromArray(
467 array(
468 'role' => self::$default_role,
469 'content' => array(
470 'type' => 'text',
471 'text' => $json_content,
472 ),
473 )
474 );
475
476 return GetPromptResult::fromArray(
477 array(
478 'messages' => array( $message_dto ),
479 'description' => $prompt->getDescription(),
480 )
481 );
482 }
483
484 // =========================================================================
485 // Validation Helpers
486 // =========================================================================
487
488 /**
489 * Validate message structure and create PromptMessage DTO.
490 *
491 * Validates role and content type, applying defaults where needed.
492 *
493 * @since 0.5.0
494 *
495 * @param array $message Raw message array.
496 * @param string $prompt_name Prompt name for logging.
497 *
498 * @return \WP\McpSchema\Server\Prompts\DTO\PromptMessage
499 */
500 private function validate_and_create_message( array $message, string $prompt_name ): PromptMessage {
501 // Validate and normalize role.
502 $role = $this->validate_role( $message['role'] ?? self::$default_role, $prompt_name );
503
504 // Validate and normalize content.
505 $content = $message['content'] ?? array();
506 if ( ! is_array( $content ) ) {
507 // If content is a string, wrap it as text.
508 $content = array(
509 'type' => 'text',
510 'text' => is_string( $content ) ? $content : (string) $content,
511 );
512 }
513
514 $content = $this->validate_content_type( $content, $prompt_name );
515
516 return PromptMessage::fromArray(
517 array(
518 'role' => $role,
519 'content' => $content,
520 )
521 );
522 }
523
524 /**
525 * Validate content type against ContentBlockFactory registry.
526 *
527 * @since 0.5.0
528 *
529 * @param array $content Content array with 'type' key.
530 * @param string $prompt_name Prompt name for logging.
531 *
532 * @return array Validated content array (may be modified if invalid type).
533 */
534 private function validate_content_type( array $content, string $prompt_name ): array {
535 $type = $content['type'] ?? null;
536
537 // Check if type is missing.
538 if ( null === $type || '' === $type ) {
539 $this->mcp->get_error_handler()->log(
540 'Missing content type in prompt result, defaulting to text',
541 array(
542 'prompt_name' => $prompt_name,
543 ),
544 'warning'
545 );
546
547 $text = isset( $content['text'] ) ? (string) $content['text'] : wp_json_encode( $content, JSON_PRETTY_PRINT );
548
549 return array(
550 'type' => 'text',
551 'text' => false === $text ? '{}' : $text,
552 );
553 }
554
555 // Check if type is valid.
556 if ( ! in_array( $type, self::$valid_content_types, true ) ) {
557 $this->mcp->get_error_handler()->log(
558 'Invalid content type in prompt result, converting to text',
559 array(
560 'prompt_name' => $prompt_name,
561 'invalid_type' => $type,
562 'valid_types' => self::$valid_content_types,
563 ),
564 'warning'
565 );
566
567 // Convert the entire content to a text representation.
568 $json_content = wp_json_encode( $content, JSON_PRETTY_PRINT );
569 if ( false === $json_content ) {
570 $json_content = '{}';
571 }
572
573 return array(
574 'type' => 'text',
575 'text' => $json_content,
576 );
577 }
578
579 // Type is valid, return content as-is (preserves annotations).
580 return $content;
581 }
582
583 /**
584 * Validate role value and apply default if invalid.
585 *
586 * @since 0.5.0
587 *
588 * @param string $role Role value to validate.
589 * @param string $prompt_name Prompt name for logging (empty to skip logging).
590 *
591 * @return string Valid role value.
592 */
593 private function validate_role( string $role, string $prompt_name ): string {
594 if ( in_array( $role, self::$valid_roles, true ) ) {
595 return $role;
596 }
597
598 if ( '' !== $prompt_name ) {
599 $this->mcp->get_error_handler()->log(
600 'Invalid role in prompt message, defaulting to user',
601 array(
602 'prompt_name' => $prompt_name,
603 'invalid_role' => $role,
604 'valid_roles' => self::$valid_roles,
605 ),
606 'warning'
607 );
608 }
609
610 return self::$default_role;
611 }
612 }
613