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 / Utils / McpValidator.php
commercebird / vendor / wordpress / mcp-adapter / includes / Domain / Utils Last commit date
AbilityArgumentNormalizer.php 2 months ago ContentBlockHelper.php 2 months ago McpAnnotationMapper.php 2 months ago McpNameSanitizer.php 2 months ago McpValidator.php 2 months ago SchemaTransformer.php 2 months ago
McpValidator.php
534 lines
1 <?php
2
3 /**
4 * MCP Validator utility class for validating MCP component data.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Domain\Utils;
12
13 use DateTime;
14
15 /**
16 * Utility class for validating MCP component data according to MCP specification.
17 *
18 * Provides shared validation implementations used across multiple MCP component
19 * validators and registration classes. Each method focuses on a specific validation concern.
20 */
21 class McpValidator {
22
23 /**
24 * Allowed MIME types for MCP icons per specification.
25 *
26 * MUST support: image/png, image/jpeg, image/jpg
27 * SHOULD support: image/svg+xml, image/webp
28 *
29 * @since 0.5.0
30 *
31 * @var array<string>
32 */
33 private static array $allowed_icon_mime_types = array(
34 'image/png',
35 'image/jpeg',
36 'image/jpg',
37 'image/svg+xml',
38 'image/webp',
39 );
40
41 /**
42 * Validate an MCP component name.
43 *
44 * Validates that a name follows MCP naming conventions per MCP 2025-11-25 spec:
45 * - Must not be empty
46 * - Must not exceed the maximum length
47 * - Must only contain letters, numbers, hyphens (-), underscores (_), and dots (.)
48 *
49 * @param string $name The name to validate.
50 * @param int $max_length Maximum allowed length. Default is 128 per MCP spec.
51 *
52 * @return bool True if valid, false otherwise.
53 * @since 0.5.0
54 *
55 */
56 public static function validate_name( string $name, int $max_length = 128 ): bool {
57 // Names should not be empty (but allow "0" since it matches the regex).
58 if ( '' === $name ) {
59 return false;
60 }
61
62 // Check length constraints.
63 if ( strlen( $name ) > $max_length ) {
64 return false;
65 }
66
67 // Only allow letters, numbers, hyphens, underscores, and dots per MCP spec.
68 return (bool) preg_match( '/^[a-zA-Z0-9_.-]+$/', $name );
69 }
70
71 /**
72 * Validate image MIME type.
73 *
74 * Checks if the MIME type is a valid image type according to MCP specification.
75 *
76 * @param string $mime_type The MIME type to validate.
77 *
78 * @return bool True if valid image MIME type, false otherwise.
79 */
80 public static function validate_image_mime_type( string $mime_type ): bool {
81 return str_starts_with( strtolower( $mime_type ), 'image/' );
82 }
83
84 /**
85 * Validate audio MIME type.
86 *
87 * Checks if the MIME type is a valid audio type according to MCP specification.
88 *
89 * @param string $mime_type The MIME type to validate.
90 *
91 * @return bool True if valid audio MIME type, false otherwise.
92 */
93 public static function validate_audio_mime_type( string $mime_type ): bool {
94 return str_starts_with( strtolower( $mime_type ), 'audio/' );
95 }
96
97 /**
98 * Validate base64 content.
99 *
100 * Checks if a string is valid base64-encoded content.
101 *
102 * @param string $content The content to validate as base64.
103 *
104 * @return bool True if valid base64, false otherwise.
105 */
106 public static function validate_base64( string $content ): bool {
107 // Base64 content should not be empty.
108 if ( empty( $content ) ) {
109 return false;
110 }
111
112 // Reject whitespace-only strings (they decode to empty string but aren't valid base64 content).
113 if ( trim( $content ) === '' ) {
114 return false;
115 }
116
117 // Check if it's valid base64 encoding.
118 return base64_decode( $content, true ) !== false; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
119 }
120
121 /**
122 * Validate an array of icons.
123 *
124 * Returns valid icons and logs warnings for invalid ones.
125 * Invalid icons are filtered out (graceful degradation).
126 *
127 * @param array $icons Array of icon data.
128 * @param bool $log_warnings Whether to log warnings for invalid icons. Default true.
129 *
130 * @return array{valid: array, errors: array} Array with 'valid' icons and 'errors' details.
131 * @since 0.5.0
132 *
133 */
134 public static function validate_icons_array( array $icons, bool $log_warnings = true ): array {
135 $valid_icons = array();
136 $all_errors = array();
137
138 foreach ( $icons as $index => $icon ) {
139 if ( ! is_array( $icon ) ) {
140 $all_errors[] = array(
141 'index' => $index,
142 'errors' => array( __( 'Icon must be an array', 'mcp-adapter' ) ),
143 );
144 continue;
145 }
146
147 $errors = self::get_icon_validation_errors( $icon );
148
149 if ( empty( $errors ) ) {
150 $valid_icons[] = $icon;
151 } else {
152 $all_errors[] = array(
153 'index' => $index,
154 'errors' => $errors,
155 );
156
157 if ( $log_warnings ) {
158 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
159 error_log(
160 sprintf(
161 'MCP Adapter: Invalid icon at index %d skipped: %s',
162 $index,
163 implode( '; ', $errors )
164 )
165 );
166 }
167 }
168 }
169
170 return array(
171 'valid' => $valid_icons,
172 'errors' => $all_errors,
173 );
174 }
175
176 /**
177 * Get validation errors for an MCP icon object.
178 *
179 * Validates icon fields per MCP 2025-11-25 specification:
180 * - src (required): Valid URL or data: URI
181 * - mimeType (optional): One of allowed image MIME types
182 * - sizes (optional): Array of size strings in WxH format or "any"
183 * - theme (optional): "light" or "dark"
184 *
185 * @param array $icon The icon data to validate.
186 *
187 * @return array Array of validation errors, empty if valid.
188 * @since 0.5.0
189 *
190 */
191 public static function get_icon_validation_errors( array $icon ): array {
192 $errors = array();
193
194 // src is required.
195 if ( ! isset( $icon['src'] ) ) {
196 $errors[] = __( 'Icon must have a src field', 'mcp-adapter' );
197 } elseif ( ! is_string( $icon['src'] ) ) {
198 $errors[] = __( 'Icon src must be a string', 'mcp-adapter' );
199 } elseif ( ! self::validate_icon_src( $icon['src'] ) ) {
200 $errors[] = __( 'Icon src must be a valid URL (http/https) or data: URI', 'mcp-adapter' );
201 }
202
203 // mimeType is optional but must be valid if present.
204 if ( isset( $icon['mimeType'] ) ) {
205 if ( ! is_string( $icon['mimeType'] ) ) {
206 $errors[] = __( 'Icon mimeType must be a string', 'mcp-adapter' );
207 } elseif ( ! self::validate_icon_mime_type( $icon['mimeType'] ) ) {
208 $errors[] = sprintf(
209 /* translators: %s: comma-separated list of allowed MIME types */
210 __( 'Icon mimeType must be one of: %s', 'mcp-adapter' ),
211 implode( ', ', self::$allowed_icon_mime_types )
212 );
213 }
214 }
215
216 // sizes is optional but must be valid if present.
217 if ( isset( $icon['sizes'] ) ) {
218 if ( ! is_array( $icon['sizes'] ) ) {
219 $errors[] = __( 'Icon sizes must be an array', 'mcp-adapter' );
220 } else {
221 foreach ( $icon['sizes'] as $index => $size ) {
222 if ( ! is_string( $size ) ) {
223 $errors[] = sprintf(
224 /* translators: %d: array index */
225 __( 'Icon size at index %d must be a string', 'mcp-adapter' ),
226 $index
227 );
228 } elseif ( ! self::validate_icon_size( $size ) ) {
229 $errors[] = sprintf(
230 /* translators: 1: size value, 2: array index */
231 __( 'Icon size "%1$s" at index %2$d must be in WxH format (e.g., "48x48") or "any"', 'mcp-adapter' ),
232 $size,
233 $index
234 );
235 }
236 }
237 }
238 }
239
240 // theme is optional but must be valid if present.
241 if ( isset( $icon['theme'] ) ) {
242 if ( ! is_string( $icon['theme'] ) ) {
243 $errors[] = __( 'Icon theme must be a string', 'mcp-adapter' );
244 } elseif ( ! self::validate_icon_theme( $icon['theme'] ) ) {
245 $errors[] = __( 'Icon theme must be "light" or "dark"', 'mcp-adapter' );
246 }
247 }
248
249 return $errors;
250 }
251
252 /**
253 * Validate an icon source (src) value.
254 *
255 * Icon src must be a valid URL (http/https) or a data: URI with base64-encoded image data.
256 *
257 * @param string $src The icon source to validate.
258 *
259 * @return bool True if valid, false otherwise.
260 * @since 0.5.0
261 *
262 */
263 public static function validate_icon_src( string $src ): bool {
264 $src = trim( $src );
265
266 if ( empty( $src ) ) {
267 return false;
268 }
269
270 // Check for data: URI.
271 if ( str_starts_with( $src, 'data:' ) ) {
272 // data:[<mediatype>][;base64],<data>
273 // Simplified validation: must have data: prefix and contain comma.
274 return str_contains( $src, ',' );
275 }
276
277 // Check for http/https URL.
278 if ( str_starts_with( $src, 'http://' ) || str_starts_with( $src, 'https://' ) ) {
279 return filter_var( $src, FILTER_VALIDATE_URL ) !== false;
280 }
281
282 return false;
283 }
284
285 /**
286 * Validate an icon MIME type.
287 *
288 * Per MCP spec, clients MUST support image/png, image/jpeg (and image/jpg).
289 * Clients SHOULD support image/svg+xml, image/webp.
290 *
291 * @param string $mime_type The MIME type to validate.
292 *
293 * @return bool True if valid icon MIME type, false otherwise.
294 * @since 0.5.0
295 *
296 */
297 public static function validate_icon_mime_type( string $mime_type ): bool {
298 return in_array( strtolower( trim( $mime_type ) ), self::$allowed_icon_mime_types, true );
299 }
300
301 /**
302 * Validate an icon size string.
303 *
304 * Icon sizes must be in WxH format (e.g., "48x48", "96x96") or "any" for scalable formats.
305 * Both width and height must be positive integers (no zero dimensions, no leading zeros).
306 *
307 * @param string $size The size string to validate.
308 *
309 * @return bool True if valid, false otherwise.
310 * @since 0.5.0
311 *
312 */
313 public static function validate_icon_size( string $size ): bool {
314 $size = trim( $size );
315
316 if ( empty( $size ) ) {
317 return false;
318 }
319
320 // "any" is valid for scalable formats like SVG.
321 if ( 'any' === strtolower( $size ) ) {
322 return true;
323 }
324
325 // Must match WxH format with positive integers (no zero dimensions, no leading zeros).
326 // [1-9]\d* matches: 1, 2, ..., 9, 10, 11, ..., 99, 100, etc.
327 return (bool) preg_match( '/^[1-9]\d*x[1-9]\d*$/', $size );
328 }
329
330 /**
331 * Validate an icon theme value.
332 *
333 * Valid themes are "light" or "dark".
334 *
335 * @param string $theme The theme to validate.
336 *
337 * @return bool True if valid, false otherwise.
338 * @since 0.5.0
339 *
340 */
341 public static function validate_icon_theme( string $theme ): bool {
342 return in_array( strtolower( trim( $theme ) ), array( 'light', 'dark' ), true );
343 }
344
345 /**
346 * Get validation errors for shared MCP annotations.
347 *
348 * Validates shared annotation fields per MCP 2025-11-25 specification:
349 * - audience must be an array of valid Role values ("user", "assistant")
350 * - lastModified must be a valid ISO 8601 formatted string
351 * - priority must be a number between 0.0 and 1.0
352 *
353 * Only validates known shared annotation fields. Unknown fields are ignored.
354 * Used by resources and content types (text, image, audio).
355 *
356 * Note: Tools use ToolAnnotations which is a separate type validated by McpToolValidator.
357 *
358 * @param array $annotations The annotations to validate.
359 *
360 * @return array Array of validation errors, empty if valid.
361 */
362 public static function get_annotation_validation_errors( array $annotations ): array {
363 $errors = array();
364
365 foreach ( $annotations as $field => $value ) {
366 switch ( $field ) {
367 case 'audience':
368 if ( ! is_array( $value ) ) {
369 $errors[] = __( 'Annotation field audience must be an array', 'mcp-adapter' );
370 break;
371 }
372 if ( ! self::validate_roles_array( $value ) ) {
373 $errors[] = __( 'Annotation field audience must contain only valid roles ("user" or "assistant")', 'mcp-adapter' );
374 }
375 break;
376
377 case 'lastModified':
378 if ( ! is_string( $value ) || empty( trim( $value ) ) ) {
379 $errors[] = __( 'Annotation field lastModified must be a non-empty string', 'mcp-adapter' );
380 break;
381 }
382 if ( ! self::validate_iso8601_timestamp( trim( $value ) ) ) {
383 $errors[] = __( 'Annotation field lastModified must be a valid ISO 8601 timestamp', 'mcp-adapter' );
384 }
385 break;
386
387 case 'priority':
388 if ( ! is_numeric( $value ) ) {
389 $errors[] = __( 'Annotation field priority must be a number', 'mcp-adapter' );
390 break;
391 }
392 if ( ! self::validate_priority( $value ) ) {
393 $errors[] = __( 'Annotation field priority must be between 0.0 and 1.0', 'mcp-adapter' );
394 }
395 break;
396
397 default:
398 // Unknown fields are ignored to allow forward compatibility.
399 break;
400 }
401 }
402
403 return $errors;
404 }
405
406 /**
407 * Validate an array of roles according to MCP specification.
408 *
409 * All roles must be strings and must be either "user" or "assistant".
410 *
411 * @param array $roles The roles array to validate.
412 *
413 * @return bool True if all roles are valid, false otherwise.
414 */
415 public static function validate_roles_array( array $roles ): bool {
416 foreach ( $roles as $role ) {
417 if ( ! is_string( $role ) || ! self::validate_role( $role ) ) {
418 return false;
419 }
420 }
421
422 return true;
423 }
424
425 /**
426 * Validate a role value according to MCP specification.
427 *
428 * Valid roles are "user" or "assistant".
429 *
430 * @param string $role The role to validate.
431 *
432 * @return bool True if valid, false otherwise.
433 */
434 public static function validate_role( string $role ): bool {
435 return in_array( $role, array( 'user', 'assistant' ), true );
436 }
437
438 /**
439 * Validate ISO 8601 timestamp format.
440 *
441 * Checks if a string is a valid ISO 8601 timestamp by attempting to parse
442 * it using multiple ISO 8601 format variations.
443 *
444 * @param string $timestamp The timestamp to validate.
445 *
446 * @return bool True if valid ISO 8601 timestamp, false otherwise.
447 */
448 public static function validate_iso8601_timestamp( string $timestamp ): bool {
449 // Try to parse as DateTime with ISO 8601 format.
450 $datetime = DateTime::createFromFormat( DateTime::ATOM, $timestamp );
451 if ( $datetime && $datetime->format( DateTime::ATOM ) === $timestamp ) {
452 return true;
453 }
454
455 // Try alternative ISO 8601 formats.
456 $formats = array(
457 'Y-m-d\TH:i:s\Z', // UTC format
458 'Y-m-d\TH:i:sP', // With timezone offset
459 'Y-m-d\TH:i:s.u\Z', // With microseconds UTC
460 'Y-m-d\TH:i:s.uP', // With microseconds and timezone
461 );
462
463 foreach ( $formats as $format ) {
464 $datetime = DateTime::createFromFormat( $format, $timestamp );
465 if ( $datetime && $datetime->format( $format ) === $timestamp ) {
466 return true;
467 }
468 }
469
470 return false;
471 }
472
473 /**
474 * Validate a priority value according to MCP specification.
475 *
476 * Priority must be a number between 0.0 and 1.0 (inclusive).
477 *
478 * @param mixed $priority The priority value to validate.
479 *
480 * @return bool True if valid, false otherwise.
481 */
482 public static function validate_priority( $priority ): bool {
483 if ( ! is_numeric( $priority ) ) {
484 return false;
485 }
486
487 $priority_float = (float) $priority;
488
489 return $priority_float >= 0.0 && $priority_float <= 1.0;
490 }
491
492 /**
493 * Validate a resource URI format.
494 *
495 * Per MCP spec: "The URI can use any protocol; it is up to the server how to interpret it."
496 * This validates basic URI structure per RFC 3986.
497 *
498 * @param string $uri The URI to validate.
499 *
500 * @return bool True if valid, false otherwise.
501 */
502 public static function validate_resource_uri( string $uri ): bool {
503 // URI should not be empty.
504 if ( empty( $uri ) ) {
505 return false;
506 }
507
508 // Check reasonable length constraints.
509 if ( strlen( $uri ) > 2048 ) {
510 return false;
511 }
512
513 // Basic URI validation: must have scheme followed by colon (RFC 3986).
514 // This accepts any protocol as per MCP specification.
515 return (bool) preg_match( '/^[a-zA-Z][a-zA-Z0-9+.-]*:.+/', $uri );
516 }
517
518 /**
519 * Validate general MIME type format.
520 *
521 * Validates that a MIME type follows the standard format: type/subtype
522 * where both type and subtype contain valid characters.
523 *
524 * @param string $mime_type The MIME type to validate.
525 *
526 * @return bool True if valid MIME type format, false otherwise.
527 */
528 public static function validate_mime_type( string $mime_type ): bool {
529 // RFC 2045 compliant: allows +, ., and other valid MIME type characters.
530 // Examples: image/svg+xml, application/vnd.api+json, text/plain.
531 return (bool) preg_match( '/^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*$/', $mime_type );
532 }
533 }
534