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 / McpTool.php

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

418 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * MCP Tool component.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Domain\Tools;
12
13 use WP\MCP\Domain\Contracts\McpComponentInterface;
14 use WP\MCP\Domain\Utils\AbilityArgumentNormalizer;
15 use WP\MCP\Domain\Utils\McpValidator;
16 use WP\MCP\Infrastructure\Observability\FailureReason;
17 use WP\McpSchema\Server\Tools\DTO\Tool as ToolDto;
18 use WP\McpSchema\Server\Tools\DTO\ToolAnnotations;
19 use WP_Error;
20
21 /**
22 * Tool component providing unified execution and permission checks.
23 *
24 * This class provides multiple flexible ways to create MCP tools:
25 *
26 * 1. Array configuration:
27 * ```php
28 * $tool = McpTool::fromArray([
29 * 'name' => 'uppercase-text',
30 * 'title' => 'Uppercase Text',
31 * 'description' => 'Converts text to uppercase',
32 * 'inputSchema' => ['type' => 'object', 'properties' => [...]],
33 * 'handler' => fn($args) => ['result' => strtoupper($args['text'])],
34 * 'permission' => fn() => true,
35 * 'annotations' => ['readOnlyHint' => true],
36 * ]);
37 * ```
38 *
39 * 2. From WordPress Ability (ability-backed):
40 * ```php
41 * $tool = McpTool::fromAbility($ability);
42 * ```
43 *
44 * McpTool wraps a protocol-only ToolDto for MCP serialization. Internal
45 * adapter metadata and execution wiring live on this class and are never
46 * exposed to MCP clients. Use get_protocol_dto() for protocol responses.
47 *
48 * @since 0.5.0
49 */
50 final class McpTool implements McpComponentInterface {
51
52
53 // =========================================================================
54 // Runtime Properties
55 // =========================================================================
56
57 /**
58 * Clean Tool DTO (protocol-only).
59 *
60 * @var \WP\McpSchema\Server\Tools\DTO\Tool
61 */
62 private ToolDto $tool;
63
64 /**
65 * Ability used for execution/permission checks (ability-backed tools).
66 *
67 * @var \WP_Ability|null
68 */
69 private ?\WP_Ability $ability = null;
70
71 /**
72 * Direct execution handler (callable-backed tools).
73 *
74 * @var callable|null
75 */
76 private $handler = null;
77
78 /**
79 * Direct permission callback (callable-backed tools).
80 *
81 * @var callable|null
82 */
83 private $permission_callback = null;
84
85 /**
86 * Internal adapter metadata (never exposed to clients).
87 *
88 * @var array<string, mixed>
89 */
90 private array $adapter_meta = array();
91
92 /**
93 * Observability context tags for logging/metrics.
94 *
95 * @var array<string, mixed>
96 */
97 private array $observability_context = array();
98
99 // =========================================================================
100 // Constructor
101 // =========================================================================
102
103 /**
104 * Private constructor - use factory methods.
105 *
106 * @param \WP\McpSchema\Server\Tools\DTO\Tool $tool The Tool DTO.
107 */
108 private function __construct( ToolDto $tool ) {
109 $this->tool = $tool;
110 }
111
112 // =========================================================================
113 // Factory Methods
114 // =========================================================================
115
116 /**
117 * Create a tool definition from an array configuration.
118 *
119 * @param array $config The tool configuration array.
120 *
121 * @return self|\WP_Error
122 */
123 public static function fromArray( array $config ) {
124 if ( empty( $config['name'] ) ) {
125 return new WP_Error( 'mcp_tool_missing_name', 'Tool configuration must include a "name" field.' );
126 }
127
128 if ( ! isset( $config['handler'] ) || ! is_callable( $config['handler'] ) ) {
129 return new WP_Error( 'mcp_tool_missing_handler', 'Tool configuration must include a callable "handler" field.' );
130 }
131
132 // Prepare input schema - ensure it's an object type for MCP compliance.
133 $input_schema = $config['inputSchema'] ?? array( 'type' => 'object' );
134 if ( ! isset( $input_schema['type'] ) ) {
135 $input_schema['type'] = 'object';
136 }
137
138 // Build tool data array.
139 $tool_data = array(
140 'name' => $config['name'],
141 'inputSchema' => $input_schema,
142 );
143
144 // Optional fields.
145 if ( isset( $config['title'] ) ) {
146 $tool_data['title'] = $config['title'];
147 }
148
149 if ( isset( $config['description'] ) ) {
150 $tool_data['description'] = $config['description'];
151 }
152
153 if ( isset( $config['outputSchema'] ) && is_array( $config['outputSchema'] ) ) {
154 $tool_data['outputSchema'] = $config['outputSchema'];
155 }
156
157 // Validate and prepare icons if set.
158 if ( isset( $config['icons'] ) && is_array( $config['icons'] ) && ! empty( $config['icons'] ) ) {
159 $icons_result = McpValidator::validate_icons_array( $config['icons'] );
160 if ( ! empty( $icons_result['valid'] ) ) {
161 $tool_data['icons'] = $icons_result['valid'];
162 }
163 }
164
165 // Preserve user-provided _meta.
166 $tool_meta = McpValidator::normalize_meta( $config['meta'] ?? null );
167 if ( null !== $tool_meta ) {
168 $tool_data['_meta'] = $tool_meta;
169 }
170
171 // Create the Tool DTO - wrap in try-catch since ToolAnnotations::fromArray() and ToolDto::fromArray() can throw.
172 try {
173 // Process annotations inside try-catch since ToolAnnotations::fromArray() can throw.
174 if ( isset( $config['annotations'] ) && is_array( $config['annotations'] ) && ! empty( $config['annotations'] ) ) {
175 $tool_data['annotations'] = ToolAnnotations::fromArray( $config['annotations'] );
176 }
177
178 $tool = ToolDto::fromArray( $tool_data );
179 } catch ( \Throwable $e ) {
180 return new WP_Error(
181 'mcp_tool_dto_creation_failed',
182 sprintf(
183 /* translators: %s: error message */
184 __( 'Failed to create Tool DTO: %s', 'mcp-adapter' ),
185 $e->getMessage()
186 ),
187 array( 'exception' => $e )
188 );
189 }
190
191 // Optional deep validation if enabled.
192 $mcp_validation_enabled = apply_filters( 'mcp_adapter_validation_enabled', false );
193 if ( $mcp_validation_enabled ) {
194 $validation_result = McpToolValidator::validate_tool_dto( $tool );
195 if ( is_wp_error( $validation_result ) ) {
196 return $validation_result;
197 }
198 }
199
200 $instance = new self( $tool );
201 $instance->handler = $config['handler'];
202
203 if ( isset( $config['permission'] ) && is_callable( $config['permission'] ) ) {
204 $instance->permission_callback = $config['permission'];
205 }
206
207 $instance->observability_context = array(
208 'component_type' => 'tool',
209 'tool_name' => $config['name'],
210 'source' => 'array',
211 );
212
213 return $instance;
214 }
215
216 /**
217 * Create an ability-backed MCP tool.
218 *
219 * @param \WP_Ability $ability WordPress ability.
220 *
221 * @return self|\WP_Error
222 */
223 public static function fromAbility( \WP_Ability $ability ) {
224 $tool_data = RegisterAbilityAsMcpTool::build( $ability );
225 if ( $tool_data instanceof WP_Error ) {
226 return $tool_data;
227 }
228
229 $instance = new self( $tool_data['tool'] );
230 $instance->adapter_meta = $tool_data['adapter_meta'];
231 $instance->ability = $ability;
232
233 $instance->observability_context = array(
234 'component_type' => 'tool',
235 'tool_name' => $tool_data['tool']->getName(),
236 'ability_name' => $ability->get_name(),
237 'source' => 'ability',
238 );
239
240 return $instance;
241 }
242
243 // =========================================================================
244 // McpComponentInterface Implementation
245 // =========================================================================
246
247 /**
248 * Get the clean protocol DTO for MCP responses.
249 *
250 * @return \WP\McpSchema\Server\Tools\DTO\Tool
251 */
252 public function get_protocol_dto(): ToolDto {
253 return $this->tool;
254 }
255
256 /**
257 * Execute the tool.
258 *
259 * @param mixed $arguments Tool arguments.
260 *
261 * @return mixed
262 */
263 public function execute( $arguments ) {
264 $args = $this->unwrap_input_if_needed( $arguments );
265
266 if ( null !== $this->ability ) {
267 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
268
269 try {
270 $result = $this->ability->execute( $args );
271 } catch ( \Throwable $throwable ) {
272 return new WP_Error(
273 'mcp_execution_failed',
274 $throwable->getMessage(),
275 array( 'error_type' => get_class( $throwable ) )
276 );
277 }
278 } elseif ( null !== $this->handler ) {
279 try {
280 $result = call_user_func( $this->handler, $args );
281 } catch ( \Throwable $throwable ) {
282 return new WP_Error(
283 'mcp_execution_failed',
284 $throwable->getMessage(),
285 array( 'error_type' => get_class( $throwable ) )
286 );
287 }
288 } else {
289 return new WP_Error( 'mcp_tool_no_handler', 'No tool execution strategy configured.' );
290 }
291
292 if ( $result instanceof WP_Error ) {
293 return $result;
294 }
295
296 $result = $this->wrap_output_if_needed( $result );
297
298 if ( ! is_array( $result ) ) {
299 $result = array( 'result' => $result );
300 }
301
302 return $result;
303 }
304
305 /**
306 * Unwrap tool input arguments when the input schema was transformed (flattened → object wrapper).
307 *
308 * @param mixed $arguments Raw tool arguments.
309 *
310 * @return mixed
311 */
312 private function unwrap_input_if_needed( $arguments ) {
313 $is_transformed = true === ( $this->adapter_meta['input_schema_transformed'] ?? false );
314
315 if ( ! $is_transformed ) {
316 return $arguments;
317 }
318
319 $wrapper = $this->adapter_meta['input_schema_wrapper'] ?? 'input';
320 $wrapper = is_string( $wrapper ) && '' !== trim( $wrapper ) ? $wrapper : 'input';
321
322 return is_array( $arguments ) ? ( $arguments[ $wrapper ] ?? null ) : null;
323 }
324
325 /**
326 * Wrap tool results when the output schema was transformed (flattened → object wrapper).
327 *
328 * @param mixed $result Raw result.
329 *
330 * @return mixed
331 */
332 private function wrap_output_if_needed( $result ) {
333 $is_transformed = true === ( $this->adapter_meta['output_schema_transformed'] ?? false );
334
335 if ( ! $is_transformed ) {
336 return $result;
337 }
338
339 $wrapper = $this->adapter_meta['output_schema_wrapper'] ?? 'result';
340 $wrapper = is_string( $wrapper ) && '' !== trim( $wrapper ) ? $wrapper : 'result';
341
342 return array( $wrapper => $result );
343 }
344
345 /**
346 * Check whether the current request has permission to execute this tool.
347 *
348 * @param mixed $arguments Tool arguments.
349 *
350 * @return bool|\WP_Error
351 */
352 public function check_permission( $arguments ) {
353 $args = $this->unwrap_input_if_needed( $arguments );
354
355 // Ability-backed tools delegate to the ability's permission system.
356 if ( null !== $this->ability ) {
357 $args = AbilityArgumentNormalizer::normalize( $this->ability, $args );
358
359 try {
360 return $this->ability->check_permissions( $args );
361 } catch ( \Throwable $throwable ) {
362 return new WP_Error(
363 'mcp_permission_check_failed',
364 $throwable->getMessage(),
365 array( 'error_type' => get_class( $throwable ) )
366 );
367 }
368 }
369
370 // Callable-backed tools use their required permission callback.
371 if ( null !== $this->permission_callback ) {
372 try {
373 $result = call_user_func( $this->permission_callback, $args );
374
375 return $result instanceof WP_Error ? $result : (bool) $result;
376 } catch ( \Throwable $throwable ) {
377 return new WP_Error(
378 'mcp_permission_check_failed',
379 $throwable->getMessage(),
380 array( 'error_type' => get_class( $throwable ) )
381 );
382 }
383 }
384
385 // Defensive fallback: should never reach here if factories are used correctly.
386 return new WP_Error(
387 'mcp_permission_denied',
388 'Access denied.',
389 array(
390 'failure_reason' => FailureReason::NO_PERMISSION_STRATEGY,
391 'tool_name' => $this->tool->getName(),
392 )
393 );
394 }
395
396 // =========================================================================
397 // Private Helper Methods
398 // =========================================================================
399
400 /**
401 * Get internal adapter metadata for this tool.
402 *
403 * @return array<string, mixed>
404 */
405 public function get_adapter_meta(): array {
406 return $this->adapter_meta;
407 }
408
409 /**
410 * Get observability context tags for logging/metrics.
411 *
412 * @return array<string, mixed>
413 */
414 public function get_observability_context(): array {
415 return $this->observability_context;
416 }
417 }
418