PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / vendor / wordpress / mcp-adapter / includes / Domain / Prompts / McpPromptValidator.php
commercebird / vendor / wordpress / mcp-adapter / includes / Domain / Prompts Last commit date
Contracts 2 months ago McpPrompt.php 2 months ago McpPromptBuilder.php 2 months ago McpPromptValidator.php 2 months ago RegisterAbilityAsMcpPrompt.php 2 months ago
McpPromptValidator.php
521 lines
1 <?php
2 /**
3 * MCP Prompt Validator class for validating MCP prompts according to the specification.
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Domain\Prompts;
11
12 use WP\MCP\Domain\Resources\McpResourceValidator;
13 use WP\MCP\Domain\Utils\McpValidator;
14 use WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
15 use WP_Error;
16
17 /**
18 * Validates MCP prompts against the Model Context Protocol specification.
19 *
20 * Provides minimal, resource-efficient validation to ensure prompts conform
21 * to the MCP schema requirements without heavy processing overhead.
22 *
23 * @link https://modelcontextprotocol.io/specification/2025-11-25/server/prompts
24 */
25 class McpPromptValidator {
26
27 /**
28 * Validate an MCP prompt data array against the MCP schema.
29 *
30 * @param array $prompt_data The prompt data to validate.
31 * @param string $context Optional context for error messages.
32 *
33 * @return bool|\WP_Error True if valid, WP_Error if validation fails.
34 */
35 public static function validate_prompt_data( array $prompt_data, string $context = '' ) {
36 $validation_errors = self::get_validation_errors( $prompt_data );
37
38 if ( ! empty( $validation_errors ) ) {
39 $error_message = $context ? "[{$context}] " : '';
40 $error_message .= sprintf(
41 /* translators: %s: comma-separated list of validation errors */
42 __( 'Prompt validation failed: %s', 'mcp-adapter' ),
43 implode( ', ', $validation_errors )
44 );
45 return new WP_Error( 'mcp_prompt_validation_failed', esc_html( $error_message ) );
46 }
47
48 return true;
49 }
50
51 /**
52 * Validate a Prompt DTO against the MCP schema.
53 *
54 * @param \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt The prompt DTO to validate.
55 *
56 * @return bool|\WP_Error True if valid, WP_Error otherwise.
57 */
58 public static function validate_prompt_dto( PromptDto $prompt ) {
59 $errors = array();
60
61 // Validate name.
62 if ( ! McpValidator::validate_name( $prompt->getName() ) ) {
63 $errors[] = __( 'Prompt name must be 1-128 characters and contain only [A-Za-z0-9_.-]', 'mcp-adapter' );
64 }
65
66 // Validate icons if present.
67 $icons = $prompt->getIcons();
68 if ( ! empty( $icons ) ) {
69 $icons_array = array_map( static fn( $icon ) => $icon->toArray(), $icons );
70 $icons_result = McpValidator::validate_icons_array( $icons_array );
71 $icons_errors = self::format_icon_validation_errors( $icons_result );
72 $errors = array_merge( $errors, $icons_errors );
73 }
74
75 // Validate annotations if present (shared annotations).
76 // Currently Prompt DTO doesn't have annotations field in spec, but if it did, we'd validate here.
77 // BaseMetadata has title and name, handled separately.
78
79 if ( ! empty( $errors ) ) {
80 return new WP_Error(
81 'mcp_prompt_validation_failed',
82 sprintf(
83 /* translators: %s: list of validation errors */
84 __( 'Prompt validation failed: %s', 'mcp-adapter' ),
85 implode( '; ', $errors )
86 )
87 );
88 }
89
90 return true;
91 }
92
93 /**
94 * Validate an McpPrompt instance against the MCP schema.
95 *
96 * @param \WP\MCP\Domain\Prompts\McpPrompt $prompt The prompt instance to validate.
97 *
98 * @return bool|\WP_Error True if valid, WP_Error if validation fails.
99 */
100 public static function validate_prompt_instance( McpPrompt $prompt ) {
101 return self::validate_prompt_dto( $prompt->get_protocol_dto() );
102 }
103
104
105 /**
106 * Get validation error details for debugging purposes.
107 * This is the core validation method - all other validation methods use this.
108 *
109 * @param array $prompt_data The prompt data to validate.
110 *
111 * @return array Array of validation errors, empty if valid.
112 */
113 public static function get_validation_errors( array $prompt_data ): array {
114 $errors = array();
115
116 // Check required fields
117 if ( empty( $prompt_data['name'] ) || ! is_string( $prompt_data['name'] ) || ! McpValidator::validate_name( $prompt_data['name'] ) ) {
118 $errors[] = __( 'Prompt name is required and must be 1-128 characters and contain only [A-Za-z0-9_.-]', 'mcp-adapter' );
119 }
120
121 // Check optional fields if present
122 if ( isset( $prompt_data['title'] ) && ! is_string( $prompt_data['title'] ) ) {
123 $errors[] = __( 'Prompt title must be a string if provided', 'mcp-adapter' );
124 }
125
126 if ( isset( $prompt_data['description'] ) && ! is_string( $prompt_data['description'] ) ) {
127 $errors[] = __( 'Prompt description must be a string if provided', 'mcp-adapter' );
128 }
129
130 // Validate arguments (optional field)
131 if ( isset( $prompt_data['arguments'] ) ) {
132 $arguments_errors = self::get_arguments_validation_errors( $prompt_data['arguments'] );
133 if ( ! empty( $arguments_errors ) ) {
134 $errors = array_merge( $errors, $arguments_errors );
135 }
136 }
137
138 return $errors;
139 }
140
141 /**
142 * Get detailed validation errors for prompt arguments.
143 *
144 * @param array|mixed $arguments The arguments to validate.
145 *
146 * @return array Array of validation errors, empty if valid.
147 */
148 private static function get_arguments_validation_errors( $arguments ): array {
149 $errors = array();
150
151 // Arguments must be an array
152 if ( ! is_array( $arguments ) ) {
153 return array( __( 'Prompt arguments must be an array if provided', 'mcp-adapter' ) );
154 }
155
156 // Validate each argument
157 foreach ( $arguments as $index => $argument ) {
158 if ( ! is_array( $argument ) ) {
159 $errors[] = sprintf(
160 /* translators: %d: argument index */
161 __( 'Prompt argument at index %d must be an object', 'mcp-adapter' ),
162 $index
163 );
164 continue;
165 }
166
167 // Check required name field
168 if ( empty( $argument['name'] ) || ! is_string( $argument['name'] ) ) {
169 $errors[] = sprintf(
170 /* translators: %d: argument index */
171 __( 'Prompt argument at index %d must have a non-empty name string', 'mcp-adapter' ),
172 $index
173 );
174 continue;
175 }
176
177 // Validate argument name format (uses standard 128-char limit per MCP spec).
178 if ( ! McpValidator::validate_name( $argument['name'] ) ) {
179 $errors[] = sprintf(
180 /* translators: %s: argument name */
181 __( 'Prompt argument \'%s\' name must only contain letters, numbers, hyphens (-), underscores (_), and dots (.), and be 128 characters or less', 'mcp-adapter' ),
182 $argument['name']
183 );
184 }
185
186 // Check optional description field
187 if ( isset( $argument['description'] ) && ! is_string( $argument['description'] ) ) {
188 $errors[] = sprintf(
189 /* translators: %s: argument name */
190 __( 'Prompt argument \'%s\' description must be a string if provided', 'mcp-adapter' ),
191 $argument['name']
192 );
193 }
194
195 // Check optional required field
196 if ( ! isset( $argument['required'] ) || is_bool( $argument['required'] ) ) {
197 continue;
198 }
199
200 $errors[] = sprintf(
201 /* translators: %s: argument name */
202 __( 'Prompt argument \'%s\' required field must be a boolean if provided', 'mcp-adapter' ),
203 $argument['name']
204 );
205 }
206
207 return $errors;
208 }
209
210 /**
211 * Validate prompt messages array (used when getting a prompt with messages).
212 *
213 * @param array $messages The messages to validate.
214 *
215 * @return array Array of validation errors, empty if valid.
216 */
217 public static function validate_prompt_messages( array $messages ): array {
218 $errors = array();
219
220 foreach ( $messages as $index => $message ) {
221 if ( ! is_array( $message ) ) {
222 $errors[] = sprintf(
223 /* translators: %d: message index */
224 __( 'Message at index %d must be an object', 'mcp-adapter' ),
225 $index
226 );
227 continue;
228 }
229
230 // Check the required role field
231 if ( empty( $message['role'] ) || ! is_string( $message['role'] ) ) {
232 $errors[] = sprintf(
233 /* translators: %d: message index */
234 __( 'Message at index %d must have a role field', 'mcp-adapter' ),
235 $index
236 );
237 continue;
238 }
239
240 // Validate role value
241 if ( ! in_array( $message['role'], array( 'user', 'assistant' ), true ) ) {
242 $errors[] = sprintf(
243 /* translators: %d: message index */
244 __( 'Message at index %d role must be either \'user\' or \'assistant\'', 'mcp-adapter' ),
245 $index
246 );
247 }
248
249 // Check the required content field
250 if ( empty( $message['content'] ) || ! is_array( $message['content'] ) ) {
251 $errors[] = sprintf(
252 /* translators: %d: message index */
253 __( 'Message at index %d must have a content object', 'mcp-adapter' ),
254 $index
255 );
256 continue;
257 }
258
259 // Validate content
260 $content_errors = self::get_content_validation_errors( $message['content'], $index );
261 if ( empty( $content_errors ) ) {
262 continue;
263 }
264
265 $errors = array_merge( $errors, $content_errors );
266 }
267
268 return $errors;
269 }
270
271 /**
272 * Format icon validation errors from the validation result.
273 *
274 * @param array{valid: array, errors: array} $icons_result The result from validate_icons_array.
275 *
276 * @return array Array of formatted error messages.
277 */
278 private static function format_icon_validation_errors( array $icons_result ): array {
279 $errors = array();
280
281 if ( ! empty( $icons_result['errors'] ) ) {
282 foreach ( $icons_result['errors'] as $error_group ) {
283 foreach ( $error_group['errors'] as $error ) {
284 $errors[] = sprintf(
285 /* translators: 1: icon index, 2: error message */
286 __( 'Icon at index %1$d: %2$s', 'mcp-adapter' ),
287 $error_group['index'],
288 $error
289 );
290 }
291 }
292 }
293
294 return $errors;
295 }
296
297 /**
298 * Get validation errors for message content.
299 *
300 * @param array $content The content to validate.
301 * @param int $message_index The message index for error reporting.
302 *
303 * @return array Array of validation errors, empty if valid.
304 */
305 private static function get_content_validation_errors( array $content, int $message_index ): array {
306 $errors = array();
307
308 // Check the required type field
309 if ( empty( $content['type'] ) || ! is_string( $content['type'] ) ) {
310 return array(
311 sprintf(
312 /* translators: %d: message index */
313 __( 'Message %d content must have a type field', 'mcp-adapter' ),
314 $message_index
315 ),
316 );
317 }
318
319 $type = $content['type'];
320
321 switch ( $type ) {
322 case 'text':
323 if ( ! isset( $content['text'] ) || ! is_string( $content['text'] ) ) {
324 $errors[] = sprintf(
325 /* translators: %d: message index */
326 __( 'Message %d text content must have a text field', 'mcp-adapter' ),
327 $message_index
328 );
329 }
330 break;
331
332 case 'image':
333 if ( empty( $content['data'] ) || ! is_string( $content['data'] ) ) {
334 $errors[] = sprintf(
335 /* translators: %d: message index */
336 __( 'Message %d image content must have a data field with base64-encoded image', 'mcp-adapter' ),
337 $message_index
338 );
339 } elseif ( ! McpValidator::validate_base64( $content['data'] ) ) {
340 $errors[] = sprintf(
341 /* translators: %d: message index */
342 __( 'Message %d image content data must be valid base64', 'mcp-adapter' ),
343 $message_index
344 );
345 }
346
347 if ( empty( $content['mimeType'] ) || ! is_string( $content['mimeType'] ) ) {
348 $errors[] = sprintf(
349 /* translators: %d: message index */
350 __( 'Message %d image content must have a mimeType field', 'mcp-adapter' ),
351 $message_index
352 );
353 } elseif ( ! McpValidator::validate_image_mime_type( $content['mimeType'] ) ) {
354 $errors[] = sprintf(
355 /* translators: %d: message index */
356 __( 'Message %d image content must have a valid image MIME type', 'mcp-adapter' ),
357 $message_index
358 );
359 }
360 break;
361
362 case 'audio':
363 if ( empty( $content['data'] ) || ! is_string( $content['data'] ) ) {
364 $errors[] = sprintf(
365 /* translators: %d: message index */
366 __( 'Message %d audio content must have a data field with base64-encoded audio', 'mcp-adapter' ),
367 $message_index
368 );
369 } elseif ( ! McpValidator::validate_base64( $content['data'] ) ) {
370 $errors[] = sprintf(
371 /* translators: %d: message index */
372 __( 'Message %d audio content data must be valid base64', 'mcp-adapter' ),
373 $message_index
374 );
375 }
376
377 if ( empty( $content['mimeType'] ) || ! is_string( $content['mimeType'] ) ) {
378 $errors[] = sprintf(
379 /* translators: %d: message index */
380 __( 'Message %d audio content must have a mimeType field', 'mcp-adapter' ),
381 $message_index
382 );
383 } elseif ( ! McpValidator::validate_audio_mime_type( $content['mimeType'] ) ) {
384 $errors[] = sprintf(
385 /* translators: %d: message index */
386 __( 'Message %d audio content must have a valid audio MIME type', 'mcp-adapter' ),
387 $message_index
388 );
389 }
390 break;
391
392 case 'resource_link':
393 // ResourceLink is a metadata-only reference (same shape as Resource + type discriminator).
394 if ( empty( $content['name'] ) || ! is_string( $content['name'] ) ) {
395 $errors[] = sprintf(
396 /* translators: %d: message index */
397 __( 'Message %d resource_link content must have a name field', 'mcp-adapter' ),
398 $message_index
399 );
400 }
401
402 if ( empty( $content['uri'] ) || ! is_string( $content['uri'] ) ) {
403 $errors[] = sprintf(
404 /* translators: %d: message index */
405 __( 'Message %d resource_link content must have a uri field', 'mcp-adapter' ),
406 $message_index
407 );
408 } elseif ( ! McpValidator::validate_resource_uri( $content['uri'] ) ) {
409 $errors[] = sprintf(
410 /* translators: %d: message index */
411 __( 'Message %d resource_link content uri must be a valid URI format', 'mcp-adapter' ),
412 $message_index
413 );
414 }
415
416 if ( isset( $content['mimeType'] ) ) {
417 if ( ! is_string( $content['mimeType'] ) ) {
418 $errors[] = sprintf(
419 /* translators: %d: message index */
420 __( 'Message %d resource_link content mimeType must be a string if provided', 'mcp-adapter' ),
421 $message_index
422 );
423 } elseif ( ! McpValidator::validate_mime_type( $content['mimeType'] ) ) {
424 $errors[] = sprintf(
425 /* translators: %d: message index */
426 __( 'Message %d resource_link content mimeType must be a valid MIME type format', 'mcp-adapter' ),
427 $message_index
428 );
429 }
430 }
431
432 if ( isset( $content['size'] ) && ! is_int( $content['size'] ) ) {
433 $errors[] = sprintf(
434 /* translators: %d: message index */
435 __( 'Message %d resource_link content size must be an integer if provided', 'mcp-adapter' ),
436 $message_index
437 );
438 }
439
440 if ( isset( $content['title'] ) && ! is_string( $content['title'] ) ) {
441 $errors[] = sprintf(
442 /* translators: %d: message index */
443 __( 'Message %d resource_link content title must be a string if provided', 'mcp-adapter' ),
444 $message_index
445 );
446 }
447
448 if ( isset( $content['description'] ) && ! is_string( $content['description'] ) ) {
449 $errors[] = sprintf(
450 /* translators: %d: message index */
451 __( 'Message %d resource_link content description must be a string if provided', 'mcp-adapter' ),
452 $message_index
453 );
454 }
455
456 if ( isset( $content['icons'] ) ) {
457 if ( ! is_array( $content['icons'] ) ) {
458 $errors[] = sprintf(
459 /* translators: %d: message index */
460 __( 'Message %d resource_link content icons must be an array if provided', 'mcp-adapter' ),
461 $message_index
462 );
463 } else {
464 $icons_result = McpValidator::validate_icons_array( $content['icons'], false );
465 $errors = array_merge( $errors, self::format_icon_validation_errors( $icons_result ) );
466 }
467 }
468
469 break;
470
471 case 'resource':
472 if ( empty( $content['resource'] ) || ! is_array( $content['resource'] ) ) {
473 $errors[] = sprintf(
474 /* translators: %d: message index */
475 __( 'Message %d resource content must have a resource object', 'mcp-adapter' ),
476 $message_index
477 );
478 } else {
479 // Validate embedded resource using the resource validator for strict MCP compliance.
480 $resource_errors = McpResourceValidator::get_validation_errors( $content['resource'] );
481 foreach ( $resource_errors as $resource_error ) {
482 $errors[] = sprintf(
483 /* translators: %1$d: message index, %2$s: resource error */
484 __( 'Message %1$d embedded resource: %2$s', 'mcp-adapter' ),
485 $message_index,
486 $resource_error
487 );
488 }
489 }
490 break;
491
492 default:
493 $errors[] = sprintf(
494 /* translators: %1$d: message index, %2$s: content type */
495 __( 'Message %1$d content type \'%2$s\' is not supported. Must be \'text\', \'image\', \'audio\', \'resource\', or \'resource_link\'', 'mcp-adapter' ),
496 $message_index,
497 $type
498 );
499 break;
500 }
501
502 // Check optional annotations
503 if ( isset( $content['annotations'] ) ) {
504 if ( ! is_array( $content['annotations'] ) ) {
505 $errors[] = sprintf(
506 /* translators: %d: message index */
507 __( 'Message %d content annotations must be an array if provided', 'mcp-adapter' ),
508 $message_index
509 );
510 } else {
511 $annotation_errors = McpValidator::get_annotation_validation_errors( $content['annotations'] );
512 if ( ! empty( $annotation_errors ) ) {
513 $errors = array_merge( $errors, $annotation_errors );
514 }
515 }
516 }
517
518 return $errors;
519 }
520 }
521