PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.1
Elementor Website Builder – more than just a page builder v4.3.1
4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 All 454 releases
elementor / vendor / wordpress / mcp-adapter / includes / Domain / Tools / RegisterAbilityAsMcpTool.php

RegisterAbilityAsMcpTool.php in Elementor Website Builder – more than just a page builder 4.3.1, at vendor/wordpress/mcp-adapter/includes/Domain/Tools/RegisterAbilityAsMcpTool.php

239 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 = McpValidator::normalize_meta( $mcp_meta['_meta'] ?? null );
187 if ( null !== $tool_meta ) {
188 $tool_data['_meta'] = $tool_meta;
189 }
190
191 return array(
192 'tool_data' => $tool_data,
193 'adapter_meta' => $adapter_meta,
194 );
195 }
196
197 /**
198 * Resolve the MCP tool name from ability.
199 *
200 * Sanitizes the ability name to MCP-valid format, applies filter, and validates result.
201 *
202 * @return string|\WP_Error Valid tool name or error.
203 * @since 0.5.0
204 *
205 */
206 private function resolve_tool_name() {
207 // Sanitize ability name to MCP-valid format.
208 $sanitized_name = McpNameSanitizer::sanitize_name( $this->ability->get_name() );
209
210 if ( is_wp_error( $sanitized_name ) ) {
211 return $sanitized_name;
212 }
213
214 /**
215 * Filters the MCP tool name derived from an ability.
216 *
217 * @since 0.5.0
218 *
219 * @param string $name The sanitized tool name.
220 * @param \WP_Ability $ability The source ability instance.
221 */
222 $filtered_name = apply_filters( 'mcp_adapter_tool_name', $sanitized_name, $this->ability );
223
224 // Validate post-filter (in case filter broke it).
225 if ( ! is_string( $filtered_name ) || ! McpValidator::validate_name( $filtered_name ) ) {
226 return new WP_Error(
227 'mcp_tool_name_filter_invalid',
228 sprintf(
229 /* translators: %s: invalid tool name returned by filter */
230 __( 'Filter returned invalid MCP tool name: %s', 'mcp-adapter' ),
231 is_string( $filtered_name ) ? $filtered_name : gettype( $filtered_name )
232 )
233 );
234 }
235
236 return $filtered_name;
237 }
238 }
239