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 / McpAnnotationMapper.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
McpAnnotationMapper.php
233 lines
1 <?php
2 /**
3 * MCP Annotation Mapper utility class for mapping WordPress ability annotations to MCP format.
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Domain\Utils;
11
12 /**
13 * Utility class for mapping WordPress ability annotations to MCP Annotations format.
14 *
15 * Provides shared annotation mapping and transformation logic used across multiple
16 * MCP component registration classes. Handles conversion of WordPress-format annotations
17 * to MCP-compliant annotation structures.
18 */
19 class McpAnnotationMapper {
20
21 /**
22 * Comprehensive mapping of MCP annotations.
23 *
24 * Maps MCP annotation fields to their type, which features they apply to,
25 * and their WordPress Ability API equivalent property names.
26 *
27 * Structure:
28 * - type: The data type (boolean, string, array, number)
29 * - features: Array of MCP features where this annotation is used (tool, resource)
30 * - ability_property: The WordPress Ability API property name (may differ from MCP field name), or null if mapping 1:1
31 *
32 * Note: Per MCP 2025-11-25 spec:
33 * - Tools use ToolAnnotations (title, *Hint fields only)
34 * - Resources use shared Annotations (audience, priority, lastModified)
35 * - Prompts do NOT support annotations at template level (only on message content blocks)
36 *
37 * @var array<string, array{type: string, features: array<string>, ability_property: string|null}>
38 */
39 private static array $mcp_annotations = array(
40 // Shared annotations - Resources only (NOT Tools or Prompt templates per MCP spec).
41 // ToolAnnotations is a separate type that does not include these fields.
42 // Prompt templates do not support annotations; only content blocks inside messages do.
43 'audience' => array(
44 'type' => 'array',
45 'features' => array( 'resource' ),
46 'ability_property' => null,
47 ),
48 'lastModified' => array(
49 'type' => 'string',
50 'features' => array( 'resource' ),
51 'ability_property' => null,
52 ),
53 'priority' => array(
54 'type' => 'number',
55 'features' => array( 'resource' ),
56 'ability_property' => null,
57 ),
58 // Tool-specific annotations (ToolAnnotations type per MCP 2025-11-25 spec).
59 'readOnlyHint' => array(
60 'type' => 'boolean',
61 'features' => array( 'tool' ),
62 'ability_property' => 'readonly',
63 ),
64 'destructiveHint' => array(
65 'type' => 'boolean',
66 'features' => array( 'tool' ),
67 'ability_property' => 'destructive',
68 ),
69 'idempotentHint' => array(
70 'type' => 'boolean',
71 'features' => array( 'tool' ),
72 'ability_property' => 'idempotent',
73 ),
74 'openWorldHint' => array(
75 'type' => 'boolean',
76 'features' => array( 'tool' ),
77 'ability_property' => null,
78 ),
79 'title' => array(
80 'type' => 'string',
81 'features' => array( 'tool' ),
82 'ability_property' => null,
83 ),
84 );
85
86 /**
87 * Map WordPress ability annotation property names to MCP field names.
88 *
89 * Maps WordPress-format field names to MCP equivalents (e.g., readonly → readOnlyHint).
90 * Only includes annotations applicable to the specified feature type.
91 * Null values are excluded from the result.
92 *
93 * @param array $ability_annotations WordPress ability annotations.
94 * @param string $feature_type The MCP feature type ('tool', 'resource', or 'prompt').
95 *
96 * @return array Mapped annotations for the specified feature type.
97 */
98 public static function map( array $ability_annotations, string $feature_type ): array {
99 $result = array();
100
101 foreach ( self::$mcp_annotations as $mcp_field => $config ) {
102 if ( ! in_array( $feature_type, $config['features'], true ) ) {
103 continue;
104 }
105
106 $value = self::resolve_annotation_value(
107 $ability_annotations,
108 $mcp_field,
109 $config['ability_property']
110 );
111
112 if ( null === $value ) {
113 continue;
114 }
115
116 $normalized = self::normalize_annotation_value( $config['type'], $value );
117 if ( null === $normalized ) {
118 continue;
119 }
120
121 $result[ $mcp_field ] = $normalized;
122 }
123
124 return $result;
125 }
126
127 /**
128 * Resolve the annotation value, preferring WordPress-format overrides when available.
129 *
130 * @param array $annotations Raw annotations from the ability.
131 * @param string $mcp_field The MCP field name.
132 * @param string|null $ability_property Optional WordPress-format field name, or null if mapping 1:1.
133 *
134 * @return mixed The annotation value, or null if not found.
135 */
136 private static function resolve_annotation_value( array $annotations, string $mcp_field, ?string $ability_property ) {
137 // WordPress-format overrides take precedence when present.
138 if ( null !== $ability_property && array_key_exists( $ability_property, $annotations ) && ! is_null( $annotations[ $ability_property ] ) ) {
139 return $annotations[ $ability_property ];
140 }
141
142 if ( array_key_exists( $mcp_field, $annotations ) && ! is_null( $annotations[ $mcp_field ] ) ) {
143 return $annotations[ $mcp_field ];
144 }
145
146 return null;
147 }
148
149 /**
150 * Normalize annotation values to the types expected by MCP.
151 *
152 * @param string $field_type Expected MCP type (boolean, string, array, number).
153 * @param mixed $value Raw annotation value.
154 *
155 * @return mixed|null Normalized value or null if invalid.
156 */
157 private static function normalize_annotation_value( string $field_type, $value ) {
158 switch ( $field_type ) {
159 case 'boolean':
160 return self::normalize_boolean( $value );
161
162 case 'string':
163 if ( ! is_scalar( $value ) ) {
164 return null;
165 }
166 $trimmed = trim( (string) $value );
167
168 return '' === $trimmed ? null : $trimmed;
169
170 case 'array':
171 return is_array( $value ) && ! empty( $value ) ? $value : null;
172
173 case 'number':
174 return is_numeric( $value ) ? (float) $value : null;
175
176 default:
177 return $value;
178 }
179 }
180
181 /**
182 * Normalize a value to a strict boolean.
183 *
184 * Accepts only well-defined boolean representations to avoid ambiguous conversions.
185 * PHP's default (bool) cast incorrectly converts 'false' string to true.
186 *
187 * Accepted values:
188 * - true, false (PHP booleans)
189 * - 1, 0 (integers)
190 * - '1', '0', 'true', 'false' (case-insensitive strings)
191 *
192 * @param mixed $value The value to normalize.
193 *
194 * @return bool|null The normalized boolean, or null if value cannot be safely converted.
195 */
196 private static function normalize_boolean( $value ): ?bool {
197 // Already a boolean - return as-is.
198 if ( is_bool( $value ) ) {
199 return $value;
200 }
201
202 // Integer 1 or 0.
203 if ( is_int( $value ) ) {
204 if ( 1 === $value ) {
205 return true;
206 }
207 if ( 0 === $value ) {
208 return false;
209 }
210
211 // Other integers are invalid (e.g., 2, -1).
212 return null;
213 }
214
215 // String representations (case-insensitive).
216 if ( is_string( $value ) ) {
217 $lower = strtolower( trim( $value ) );
218 if ( 'true' === $lower || '1' === $lower ) {
219 return true;
220 }
221 if ( 'false' === $lower || '0' === $lower ) {
222 return false;
223 }
224
225 // Other strings are invalid (e.g., 'yes', 'no', empty string).
226 return null;
227 }
228
229 // All other types (arrays, objects, floats, null) are invalid.
230 return null;
231 }
232 }
233