PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / lib / mcp-adapter / includes / Domain / Utils / McpAnnotationMapper.php

McpAnnotationMapper.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at lib/mcp-adapter/includes/Domain/Utils/McpAnnotationMapper.php

233 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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