PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / wordpress / mcp-adapter / includes / Domain / Tools / RegisterAbilityAsMcpTool.php
ameliabooking / vendor / wordpress / mcp-adapter / includes / Domain / Tools Last commit date
McpTool.php 3 months ago McpToolValidator.php 3 months ago RegisterAbilityAsMcpTool.php 3 months ago
RegisterAbilityAsMcpTool.php
242 lines
1 <?php
2
3 /**
4 * RegisterAbilityAsMcpTool class for converting WordPress abilities to MCP tools.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Domain\Tools;
12
13 use WP\MCP\Domain\Utils\McpAnnotationMapper;
14 use WP\MCP\Domain\Utils\McpNameSanitizer;
15 use WP\MCP\Domain\Utils\McpValidator;
16 use WP\MCP\Domain\Utils\SchemaTransformer;
17 use WP\McpSchema\Server\Tools\DTO\Tool as ToolDto;
18 use WP_Error;
19
20 /**
21 * RegisterAbilityAsMcpTool class.
22 *
23 * This class registers a WordPress ability as an MCP tool.
24 *
25 * @internal
26 *
27 * @package McpAdapter
28 */
29 class RegisterAbilityAsMcpTool {
30
31 /**
32 * The WordPress ability instance.
33 *
34 * @var \WP_Ability
35 */
36 private \WP_Ability $ability;
37
38 /**
39 * Constructor.
40 *
41 * @param \WP_Ability $ability The ability.
42 */
43 private function __construct( \WP_Ability $ability ) {
44 $this->ability = $ability;
45 }
46
47 /**
48 * Build a clean Tool DTO and adapter metadata for internal wiring.
49 *
50 * This method returns a protocol-only Tool DTO and provides the adapter metadata
51 * separately. This keeps the DTO stable across MCP spec changes and avoids coupling internal execution
52 * wiring to protocol surfaces.
53 *
54 * @param \WP_Ability $ability The ability.
55 *
56 * @return array{tool: \WP\McpSchema\Server\Tools\DTO\Tool, adapter_meta: array<string, mixed>}|\WP_Error
57 * @since 0.5.0
58 *
59 */
60 public static function build( \WP_Ability $ability ) {
61 $tool = new self( $ability );
62 $data = $tool->build_tool_data();
63
64 if ( is_wp_error( $data ) ) {
65 return $data;
66 }
67
68 try {
69 $tool_dto = ToolDto::fromArray( $data['tool_data'] );
70 } catch ( \Throwable $e ) {
71 return new WP_Error(
72 'mcp_tool_dto_creation_failed',
73 sprintf(
74 /* translators: %s: error message */
75 __( 'Failed to create Tool DTO for ability %1$s: %2$s', 'mcp-adapter' ),
76 $ability->get_name(),
77 $e->getMessage()
78 ),
79 array( 'exception' => $e )
80 );
81 }
82
83 // Optional deep validation if enabled.
84 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
85 if ( $mcp_validation_enabled ) {
86 $validation_result = McpToolValidator::validate_tool_dto( $tool_dto );
87 if ( is_wp_error( $validation_result ) ) {
88 return $validation_result;
89 }
90 }
91
92 return array(
93 'tool' => $tool_dto,
94 'adapter_meta' => $data['adapter_meta'],
95 );
96 }
97
98 /**
99 * Build Tool DTO data and adapter metadata.
100 *
101 * @return array{tool_data: array<string, mixed>, adapter_meta: array<string, mixed>}|\WP_Error
102 * @since 0.5.0
103 *
104 */
105 private function build_tool_data() {
106 // Resolve tool name first (can fail).
107 $tool_name = $this->resolve_tool_name();
108 if ( is_wp_error( $tool_name ) ) {
109 return $tool_name;
110 }
111
112 // Transform input schema to MCP-compatible object format.
113 $input_transform = SchemaTransformer::transform_to_object_schema(
114 $this->ability->get_input_schema()
115 );
116
117 $tool_data = array(
118 'name' => $tool_name,
119 'description' => trim( $this->ability->get_description() ),
120 'inputSchema' => $input_transform['schema'],
121 );
122
123 // Add optional title from ability label.
124 $label = $this->ability->get_label();
125 $label = trim( $label );
126 if ( ! empty( $label ) ) {
127 $tool_data['title'] = $label;
128 }
129
130 // Add optional output schema, transformed to object format if needed.
131 $output_schema = $this->ability->get_output_schema();
132 $output_transform = null;
133 if ( ! empty( $output_schema ) ) {
134 $output_transform = SchemaTransformer::transform_to_object_schema(
135 $output_schema,
136 'result'
137 );
138 $tool_data['outputSchema'] = $output_transform['schema'];
139 }
140
141 // Map annotations from ability meta to MCP format using unified mapper.
142 $ability_meta = $this->ability->get_meta();
143 if ( ! empty( $ability_meta['annotations'] ) && is_array( $ability_meta['annotations'] ) ) {
144 $mcp_annotations = McpAnnotationMapper::map( $ability_meta['annotations'], 'tool' );
145 if ( ! empty( $mcp_annotations ) ) {
146 $tool_data['annotations'] = $mcp_annotations;
147 }
148 }
149
150 // Set annotations.title from label if annotations exist but don't have a title.
151 if ( ! empty( $label ) && isset( $tool_data['annotations'] ) && ! isset( $tool_data['annotations']['title'] ) ) {
152 $tool_data['annotations']['title'] = $label;
153 }
154
155 // Store transformation metadata as internal metadata (stripped before responding to clients).
156 // Only record keys when semantically meaningful to keep metadata minimal and accurate.
157 $adapter_meta = array(
158 'ability' => $this->ability->get_name(),
159 );
160
161 // Only record input transformation metadata when a wrapper was actually applied.
162 if ( ! empty( $input_transform['was_transformed'] ) ) {
163 $adapter_meta['input_schema_transformed'] = true;
164 $adapter_meta['input_schema_wrapper'] = $input_transform['wrapper_property'];
165 }
166
167 // Only record output transformation metadata when outputSchema exists.
168 // Record wrapper only when transformation actually occurred.
169 if ( null !== $output_transform && ! empty( $output_transform['was_transformed'] ) ) {
170 $adapter_meta['output_schema_transformed'] = true;
171 $adapter_meta['output_schema_wrapper'] = $output_transform['wrapper_property'];
172 }
173
174 // Map icons from ability.meta.mcp.icons if present.
175 $mcp_meta = $ability_meta['mcp'] ?? array();
176 if ( ! empty( $mcp_meta['icons'] ) && is_array( $mcp_meta['icons'] ) ) {
177 $icons_result = McpValidator::validate_icons_array( $mcp_meta['icons'] );
178 if ( ! empty( $icons_result['valid'] ) ) {
179 $tool_data['icons'] = $icons_result['valid'];
180 }
181 }
182
183 // Build Tool `_meta`:
184 // - Preserve user-provided `_meta` from ability.meta.mcp._meta.
185 // - Adapter metadata is NEVER included in protocol DTO meta; it is returned separately in adapter_meta.
186 $tool_meta = array();
187 if ( ! empty( $mcp_meta['_meta'] ) && is_array( $mcp_meta['_meta'] ) ) {
188 $tool_meta = $mcp_meta['_meta'];
189 }
190 if ( ! empty( $tool_meta ) ) {
191 $tool_data['_meta'] = $tool_meta;
192 }
193
194 return array(
195 'tool_data' => $tool_data,
196 'adapter_meta' => $adapter_meta,
197 );
198 }
199
200 /**
201 * Resolve the MCP tool name from ability.
202 *
203 * Sanitizes the ability name to MCP-valid format, applies filter, and validates result.
204 *
205 * @return string|\WP_Error Valid tool name or error.
206 * @since 0.5.0
207 *
208 */
209 private function resolve_tool_name() {
210 // Sanitize ability name to MCP-valid format.
211 $sanitized_name = McpNameSanitizer::sanitize_name( $this->ability->get_name() );
212
213 if ( is_wp_error( $sanitized_name ) ) {
214 return $sanitized_name;
215 }
216
217 /**
218 * Filters the MCP tool name derived from an ability.
219 *
220 * @since 0.5.0
221 *
222 * @param string $name The sanitized tool name.
223 * @param \WP_Ability $ability The source ability instance.
224 */
225 $filtered_name = apply_filters( 'mcp_adapter_tool_name', $sanitized_name, $this->ability );
226
227 // Validate post-filter (in case filter broke it).
228 if ( ! is_string( $filtered_name ) || ! McpValidator::validate_name( $filtered_name ) ) {
229 return new WP_Error(
230 'mcp_tool_name_filter_invalid',
231 sprintf(
232 /* translators: %s: invalid tool name returned by filter */
233 __( 'Filter returned invalid MCP tool name: %s', 'mcp-adapter' ),
234 is_string( $filtered_name ) ? $filtered_name : gettype( $filtered_name )
235 )
236 );
237 }
238
239 return $filtered_name;
240 }
241 }
242