• File: McpValidator.php
  • Full Path: /home/baristalay/public_html/Utils/Utils/Results/McpValidator.php
  • Date Modified: 06/03/2026 11:18 AM
  • File size: 10.59 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php
/**
 * MCP Validator utility class for validating MCP component data.
 *
 * @package McpAdapter
 */

declare( strict_types=1 );

namespace WP\MCP\Domain\Utils;

/**
 * Utility class for validating MCP component data according to MCP specification.
 *
 * Provides shared validation implementations used across multiple MCP component
 * validators and registration classes. Each method focuses on a specific validation concern.
 */
class McpValidator {

	/**
	 * Validate ISO 8601 timestamp format.
	 *
	 * Checks if a string is a valid ISO 8601 timestamp by attempting to parse
	 * it using multiple ISO 8601 format variations.
	 *
	 * @param string $timestamp The timestamp to validate.
	 *
	 * @return bool True if valid ISO 8601 timestamp, false otherwise.
	 */
	public static function validate_iso8601_timestamp( string $timestamp ): bool {
		// Try to parse as DateTime with ISO 8601 format.
		$datetime = \DateTime::createFromFormat( \DateTime::ATOM, $timestamp );
		if ( $datetime && $datetime->format( \DateTime::ATOM ) === $timestamp ) {
			return true;
		}

		// Try alternative ISO 8601 formats.
		$formats = array(
			'Y-m-d\TH:i:s\Z',           // UTC format
			'Y-m-d\TH:i:sP',            // With timezone offset
			'Y-m-d\TH:i:s.u\Z',         // With microseconds UTC
			'Y-m-d\TH:i:s.uP',          // With microseconds and timezone
		);

		foreach ( $formats as $format ) {
			$datetime = \DateTime::createFromFormat( $format, $timestamp );
			if ( $datetime && $datetime->format( $format ) === $timestamp ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Validate an MCP component name.
	 *
	 * Validates that a name follows MCP naming conventions:
	 * - Must not be empty
	 * - Must not exceed the maximum length
	 * - Must only contain letters, numbers, hyphens (-), and underscores (_)
	 *
	 * @param string $name The name to validate.
	 * @param int    $max_length Maximum allowed length. Default is 255.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_name( string $name, int $max_length = 255 ): bool {
		// Names should not be empty.
		if ( empty( $name ) ) {
			return false;
		}

		// Check length constraints.
		if ( strlen( $name ) > $max_length ) {
			return false;
		}

		// Only allow letters, numbers, hyphens, and underscores.
		return (bool) preg_match( '/^[a-zA-Z0-9_-]+$/', $name );
	}

	/**
	 * Validate a tool or prompt name (max 255 characters).
	 *
	 * @param string $name The name to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_tool_or_prompt_name( string $name ): bool {
		return self::validate_name( $name, 255 );
	}

	/**
	 * Validate an argument name (max 64 characters).
	 *
	 * @param string $name The name to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_argument_name( string $name ): bool {
		return self::validate_name( $name, 64 );
	}

	/**
	 * Validate general MIME type format.
	 *
	 * Validates that a MIME type follows the standard format: type/subtype
	 * where both type and subtype contain valid characters.
	 *
	 * @param string $mime_type The MIME type to validate.
	 *
	 * @return bool True if valid MIME type format, false otherwise.
	 */
	public static function validate_mime_type( string $mime_type ): bool {
		return (bool) preg_match( '/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*$/', $mime_type );
	}

	/**
	 * Validate image MIME type.
	 *
	 * Checks if the MIME type is a valid image type according to MCP specification.
	 *
	 * @param string $mime_type The MIME type to validate.
	 *
	 * @return bool True if valid image MIME type, false otherwise.
	 */
	public static function validate_image_mime_type( string $mime_type ): bool {
		return str_starts_with( strtolower( $mime_type ), 'image/' );
	}

	/**
	 * Validate audio MIME type.
	 *
	 * Checks if the MIME type is a valid audio type according to MCP specification.
	 *
	 * @param string $mime_type The MIME type to validate.
	 *
	 * @return bool True if valid audio MIME type, false otherwise.
	 */
	public static function validate_audio_mime_type( string $mime_type ): bool {
		return str_starts_with( strtolower( $mime_type ), 'audio/' );
	}

	/**
	 * Validate base64 content.
	 *
	 * Checks if a string is valid base64-encoded content.
	 *
	 * @param string $content The content to validate as base64.
	 *
	 * @return bool True if valid base64, false otherwise.
	 */
	public static function validate_base64( string $content ): bool {
		// Base64 content should not be empty.
		if ( empty( $content ) ) {
			return false;
		}

		// Reject whitespace-only strings (they decode to empty string but aren't valid base64 content).
		if ( trim( $content ) === '' ) {
			return false;
		}

		// Check if it's valid base64 encoding.
		return base64_decode( $content, true ) !== false; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
	}

	/**
	 * Validate a resource URI format.
	 *
	 * Per MCP spec: "The URI can use any protocol; it is up to the server how to interpret it."
	 * This validates basic URI structure per RFC 3986.
	 *
	 * @param string $uri The URI to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_resource_uri( string $uri ): bool {
		// URI should not be empty.
		if ( empty( $uri ) ) {
			return false;
		}

		// Check reasonable length constraints.
		if ( strlen( $uri ) > 2048 ) {
			return false;
		}

		// Basic URI validation: must have scheme followed by colon (RFC 3986).
		// This accepts any protocol as per MCP specification.
		return (bool) preg_match( '/^[a-zA-Z][a-zA-Z0-9+.-]*:.+/', $uri );
	}

	/**
	 * Validate a role value according to MCP specification.
	 *
	 * Valid roles are "user" or "assistant".
	 *
	 * @param string $role The role to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_role( string $role ): bool {
		return in_array( $role, array( 'user', 'assistant' ), true );
	}

	/**
	 * Validate an array of roles according to MCP specification.
	 *
	 * All roles must be strings and must be either "user" or "assistant".
	 *
	 * @param array $roles The roles array to validate.
	 *
	 * @return bool True if all roles are valid, false otherwise.
	 */
	public static function validate_roles_array( array $roles ): bool {
		if ( empty( $roles ) ) {
			return false;
		}

		foreach ( $roles as $role ) {
			if ( ! is_string( $role ) || ! self::validate_role( $role ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Validate a priority value according to MCP specification.
	 *
	 * Priority must be a number between 0.0 and 1.0 (inclusive).
	 *
	 * @param mixed $priority The priority value to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public static function validate_priority( $priority ): bool {
		if ( ! is_numeric( $priority ) ) {
			return false;
		}

		$priority_float = (float) $priority;
		return $priority_float >= 0.0 && $priority_float <= 1.0;
	}

	/**
	 * Get validation errors for tool-specific MCP annotations.
	 *
	 * Validates tool annotation fields per MCP 2025-06-18 specification:
	 * - readOnlyHint, destructiveHint, idempotentHint, openWorldHint must be booleans
	 * - title must be a non-empty string
	 *
	 * Only validates known tool annotation fields. Unknown fields are ignored.
	 *
	 * @param array $annotations The annotations to validate.
	 *
	 * @return array Array of validation errors, empty if valid.
	 */
	public static function get_tool_annotation_validation_errors( array $annotations ): array {
		$errors = array();

		foreach ( $annotations as $field => $value ) {
			switch ( $field ) {
				case 'readOnlyHint':
				case 'destructiveHint':
				case 'idempotentHint':
				case 'openWorldHint':
					if ( ! is_bool( $value ) ) {
						$errors[] = sprintf(
							/* translators: %s: annotation field name */
							__( 'Tool annotation field %s must be a boolean', 'mcp-adapter' ),
							$field
						);
					}
					break;

				case 'title':
					if ( ! is_string( $value ) ) {
						$errors[] = sprintf(
							/* translators: %s: annotation field name */
							__( 'Tool annotation field %s must be a string', 'mcp-adapter' ),
							$field
						);
						break;
					}
					if ( empty( trim( $value ) ) ) {
						$errors[] = sprintf(
							/* translators: %s: annotation field name */
							__( 'Tool annotation field %s must be a non-empty string', 'mcp-adapter' ),
							$field
						);
					}
					break;

				default:
					// Unknown fields are ignored to allow forward compatibility.
					break;
			}
		}

		return $errors;
	}

	/**
	 * Get validation errors for shared MCP annotations.
	 *
	 * Validates shared annotation fields per MCP 2025-06-18 specification:
	 * - audience must be a non-empty array of valid Role values ("user", "assistant")
	 * - lastModified must be a valid ISO 8601 formatted string
	 * - priority must be a number between 0.0 and 1.0
	 *
	 * Only validates known shared annotation fields. Unknown fields are ignored.
	 * Used by resources, prompts, and tools.
	 *
	 * @param array $annotations The annotations to validate.
	 *
	 * @return array Array of validation errors, empty if valid.
	 */
	public static function get_annotation_validation_errors( array $annotations ): array {
		$errors = array();

		foreach ( $annotations as $field => $value ) {
			switch ( $field ) {
				case 'audience':
					if ( ! is_array( $value ) ) {
						$errors[] = __( 'Annotation field audience must be an array', 'mcp-adapter' );
						break;
					}
					if ( empty( $value ) ) {
						$errors[] = __( 'Annotation field audience must be a non-empty array', 'mcp-adapter' );
						break;
					}
					if ( ! self::validate_roles_array( $value ) ) {
						$errors[] = __( 'Annotation field audience must contain only valid roles ("user" or "assistant")', 'mcp-adapter' );
					}
					break;

				case 'lastModified':
					if ( ! is_string( $value ) || empty( trim( $value ) ) ) {
						$errors[] = __( 'Annotation field lastModified must be a non-empty string', 'mcp-adapter' );
						break;
					}
					if ( ! self::validate_iso8601_timestamp( trim( $value ) ) ) {
						$errors[] = __( 'Annotation field lastModified must be a valid ISO 8601 timestamp', 'mcp-adapter' );
					}
					break;

				case 'priority':
					if ( ! is_numeric( $value ) ) {
						$errors[] = __( 'Annotation field priority must be a number', 'mcp-adapter' );
						break;
					}
					if ( ! self::validate_priority( $value ) ) {
						$errors[] = __( 'Annotation field priority must be between 0.0 and 1.0', 'mcp-adapter' );
					}
					break;

				default:
					// Unknown fields are ignored to allow forward compatibility.
					break;
			}
		}

		return $errors;
	}
}