PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
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 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / vendor / wordpress / mcp-adapter / includes / Handlers / Tools / ToolsHandler.php

ToolsHandler.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at vendor/wordpress/mcp-adapter/includes/Handlers/Tools/ToolsHandler.php

384 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Tools method handlers for MCP requests.
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Handlers\Tools;
11
12 use WP\MCP\Core\McpServer;
13 use WP\MCP\Domain\Utils\ContentBlockHelper;
14 use WP\MCP\Domain\Utils\McpValidator;
15 use WP\MCP\Handlers\HandlerHelperTrait;
16 use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
17 use WP\MCP\Infrastructure\Observability\FailureReason;
18 use WP\McpSchema\Server\Tools\DTO\CallToolResult;
19 use WP\McpSchema\Server\Tools\DTO\ListToolsResult;
20
21 /**
22 * Handles tools-related MCP methods.
23 */
24 class ToolsHandler {
25 use HandlerHelperTrait;
26
27 /**
28 * Default MIME type for image results when none is specified.
29 *
30 * @var string
31 */
32 private const DEFAULT_IMAGE_MIME_TYPE = 'image/png';
33
34 /**
35 * The WordPress MCP instance.
36 *
37 * @var \WP\MCP\Core\McpServer
38 */
39 private McpServer $mcp;
40
41 /**
42 * Constructor.
43 *
44 * @param \WP\MCP\Core\McpServer $mcp The WordPress MCP instance.
45 */
46 public function __construct( McpServer $mcp ) {
47 $this->mcp = $mcp;
48 }
49
50 /**
51 * Handles the tools/list/all request.
52 *
53 * This is a custom extension to the MCP spec that includes availability status.
54 * Returns a ListToolsResult DTO containing all registered tools.
55 *
56 * Note: The 'available' flag is a non-standard extension and is not currently implemented.
57 *
58 * @return \WP\McpSchema\Server\Tools\DTO\ListToolsResult Response with all tools.
59 */
60 public function list_all_tools(): ListToolsResult {
61 // Return the standard tools list.
62 return $this->list_tools();
63 }
64
65 /**
66 * Handles the tools/list request.
67 *
68 * Returns a ListToolsResult DTO containing all registered tools.
69 * Tool DTOs are protocol-only; internal adapter metadata is stored in McpTool instances and is never exposed
70 * to MCP clients.
71 *
72 * @return \WP\McpSchema\Server\Tools\DTO\ListToolsResult Response with tools list.
73 */
74 public function list_tools(): ListToolsResult {
75 $tools = array_values( $this->mcp->get_tools() );
76
77 /**
78 * Filters the list of tools before returning to the client.
79 *
80 * Use this filter to hide tools per user/role, add dynamic tools,
81 * or reorder the tools list.
82 *
83 * @since 0.5.0
84 *
85 * @param array<\WP\McpSchema\Server\Tools\DTO\Tool> $tools Array of Tool DTOs.
86 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
87 */
88 $tools = $this->validate_filtered_list(
89 apply_filters( 'mcp_adapter_tools_list', $tools, $this->mcp ),
90 $tools,
91 'mcp_adapter_tools_list',
92 $this->mcp->get_error_handler()
93 );
94
95 return ListToolsResult::fromArray(
96 array(
97 'tools' => $tools,
98 )
99 );
100 }
101
102 /**
103 * Handles the tools/call request.
104 *
105 * Returns either a CallToolResult DTO (for success or tool execution errors)
106 * or a JSONRPCErrorResponse DTO (for protocol errors like tool not found).
107 *
108 * The MCP spec distinguishes between:
109 * 1. **Protocol errors** (tool not found, server error) → JSONRPCErrorResponse
110 * 2. **Tool execution errors** (permission denied, runtime error) → CallToolResult with isError=true
111 *
112 * This distinction is critical for LLM self-correction - execution errors are
113 * visible to the LLM, while protocol errors indicate infrastructure issues.
114 *
115 * @param array $params Request params.
116 * @param string|int|null $request_id Optional. The request ID for JSON-RPC. Default 0.
117 *
118 * @return \WP\McpSchema\Server\Tools\DTO\CallToolResult|\WP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse
119 */
120 public function call_tool( array $params, $request_id = 0 ) {
121 // Extract parameters using helper method.
122 $request_params = $this->extract_params( $params );
123
124 if ( ! isset( $request_params['name'] ) ) {
125 return McpErrorFactory::missing_parameter( $request_id, 'tool name' );
126 }
127
128 if ( isset( $request_params['arguments'] ) && ! is_array( $request_params['arguments'] ) ) {
129 return McpErrorFactory::invalid_params( $request_id, 'arguments must be an object' );
130 }
131
132 try {
133 $tool_name = trim( (string) $request_params['name'] );
134 $args = $request_params['arguments'] ?? array();
135
136 $mcp_tool = $this->mcp->get_mcp_tool( $tool_name );
137 if ( ! $mcp_tool ) {
138 $this->mcp->get_error_handler()->log(
139 'Tool not found',
140 array(
141 'tool_name' => $tool_name,
142 ),
143 'warning'
144 );
145
146 return McpErrorFactory::tool_not_found( $request_id, $tool_name );
147 }
148
149 $permission = $mcp_tool->check_permission( $args );
150 if ( true !== $permission ) {
151 $error_message = __( 'Permission denied', 'mcp-adapter' );
152 if ( is_wp_error( $permission ) ) {
153 $error_message = $permission->get_error_message();
154
155 $this->mcp->get_error_handler()->log(
156 'Tool permission check failed',
157 array(
158 'tool_name' => $tool_name,
159 'error_code' => $permission->get_error_code(),
160 'error_message' => $permission->get_error_message(),
161 'error_data' => $permission->get_error_data(),
162 'failure_reason' => FailureReason::PERMISSION_CHECK_FAILED,
163 )
164 );
165 }
166
167 return $this->create_error_result( $error_message );
168 }
169
170 /**
171 * Filters tool arguments before execution, or short-circuits execution entirely.
172 *
173 * Return the (optionally modified) arguments array to proceed with execution,
174 * or return a WP_Error to block execution and return an error to the client.
175 *
176 * @since 0.5.0
177 *
178 * @param array $args The tool arguments.
179 * @param string $tool_name The tool name being called.
180 * @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
181 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
182 */
183 $args = apply_filters( 'mcp_adapter_pre_tool_call', $args, $tool_name, $mcp_tool, $this->mcp );
184
185 // Allow pre-filter to short-circuit execution by returning WP_Error.
186 if ( is_wp_error( $args ) ) {
187 return $this->create_error_result( $args->get_error_message() );
188 }
189
190 $result = $mcp_tool->execute( $args );
191
192 /**
193 * Filters the tool execution result before response assembly.
194 *
195 * Use this filter for result transformation, PII redaction,
196 * audit logging, or content enrichment.
197 *
198 * @since 0.5.0
199 *
200 * @param mixed|\WP_Error $result The raw execution result (may be WP_Error).
201 * @param array $args The tool arguments used.
202 * @param string $tool_name The tool name that was called.
203 * @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
204 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
205 */
206 $result = apply_filters( 'mcp_adapter_tool_call_result', $result, $args, $tool_name, $mcp_tool, $this->mcp );
207
208 if ( is_wp_error( $result ) ) {
209 $this->mcp->get_error_handler()->log(
210 'Tool execution returned WP_Error',
211 array(
212 'tool_name' => $tool_name,
213 'error_code' => $result->get_error_code(),
214 'error_message' => $result->get_error_message(),
215 'error_data' => $result->get_error_data(),
216 )
217 );
218
219 return $this->create_error_result( $result->get_error_message() );
220 }
221
222 // Backward compatibility: treat `{ success: false, error: string }` as tool execution error.
223 if (
224 is_array( $result )
225 && array_key_exists( 'success', $result )
226 && false === $result['success']
227 && isset( $result['error'] )
228 && is_string( $result['error'] )
229 && '' !== trim( $result['error'] )
230 ) {
231 return $this->create_error_result( $result['error'] );
232 }
233
234 // Successful tool execution - build CallToolResult DTO.
235
236 // Handle embedded resource results (MCP ContentBlock type: "resource").
237 // This allows tools to return text/blob resources using the MCP schema's EmbeddedResource content block.
238 //
239 // Two shapes are accepted, and they place `_meta` differently:
240 //
241 // - Nested `{ type, resource: { uri, text, _meta }, _meta }` maps one-to-one onto
242 // the DTO tree, so the outer `_meta` belongs to the content block and the inner
243 // one to the resource contents.
244 // - Flat `{ type, uri, mimeType, text, _meta }` is a resource-contents literal
245 // carrying a `type` tag: every key beside `type` is a `ResourceContents` field,
246 // and `_meta` is declared there alongside them. Its `_meta` therefore describes
247 // the resource, which is what the same literal already means to
248 // `ResourcesHandler::create_content_dto()`. A caller who needs block-level
249 // `_meta` writes the nested form, which exists to express that distinction.
250 if ( isset( $result['type'] ) && 'resource' === $result['type'] ) {
251 $is_nested = isset( $result['resource'] ) && is_array( $result['resource'] );
252 $resource_item = $is_nested ? $result['resource'] : $result;
253
254 $uri = $resource_item['uri'] ?? null;
255 $mime_type = $resource_item['mimeType'] ?? null;
256
257 if ( is_string( $uri ) ) {
258 $uri = trim( $uri );
259 }
260
261 // Only return an EmbeddedResource if we have a valid URI and some content.
262 $has_text = isset( $resource_item['text'] ) && is_string( $resource_item['text'] );
263 $has_blob = isset( $resource_item['blob'] ) && is_string( $resource_item['blob'] );
264
265 if ( is_string( $uri ) && '' !== $uri && ( $has_text || $has_blob ) ) {
266 $block_meta = $is_nested
267 ? McpValidator::normalize_meta( $result['_meta'] ?? null )
268 : null;
269 $resource_meta = McpValidator::normalize_meta( $resource_item['_meta'] ?? null );
270
271 if ( $has_text ) {
272 return CallToolResult::fromArray(
273 array(
274 'content' => array(
275 ContentBlockHelper::embedded_text_resource(
276 $uri,
277 $resource_item['text'],
278 is_string( $mime_type ) ? $mime_type : null,
279 null,
280 $block_meta,
281 $resource_meta
282 ),
283 ),
284 'isError' => false,
285 )
286 );
287 }
288
289 if ( $has_blob ) {
290 return CallToolResult::fromArray(
291 array(
292 'content' => array(
293 ContentBlockHelper::embedded_blob_resource(
294 $uri,
295 $resource_item['blob'],
296 is_string( $mime_type ) ? $mime_type : null,
297 null,
298 $block_meta,
299 $resource_meta
300 ),
301 ),
302 'isError' => false,
303 )
304 );
305 }
306 }
307 }
308
309 // Handle image results.
310 //
311 // `type` marks this result as a description of a content block rather than tool
312 // data, so its sibling `_meta` is the block's, which is the reading the `resource`
313 // branch above already applies to the same key.
314 if ( isset( $result['type'] ) && 'image' === $result['type'] && isset( $result['results'] ) ) {
315 $image_data = base64_encode( $result['results'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
316 $mime_type = $result['mimeType'] ?? self::DEFAULT_IMAGE_MIME_TYPE;
317
318 return CallToolResult::fromArray(
319 array(
320 'content' => array(
321 ContentBlockHelper::image(
322 $image_data,
323 $mime_type,
324 null,
325 McpValidator::normalize_meta( $result['_meta'] ?? null )
326 ),
327 ),
328 'structuredContent' => null,
329 'isError' => false,
330 )
331 );
332 }
333
334 // The generic fallback carries no `type` marker, so every key it holds is tool
335 // data: the result is JSON-encoded into the text block and returned verbatim as
336 // `structuredContent`. Reading `_meta` off it would give one key two meanings,
337 // with nothing to tell metadata from a domain field.
338
339 // Standard result - JSON-encode for text content, include as structuredContent.
340 $json_text = wp_json_encode( $result );
341 if ( false === $json_text ) {
342 $json_text = '{}';
343 }
344
345 return CallToolResult::fromArray(
346 array(
347 'content' => array( ContentBlockHelper::text( $json_text ) ),
348 'structuredContent' => $result,
349 'isError' => false,
350 )
351 );
352 } catch ( \Throwable $exception ) {
353 $this->mcp->get_error_handler()->log(
354 'Error calling tool',
355 array(
356 'tool' => $request_params['name'],
357 'exception' => $exception->getMessage(),
358 )
359 );
360
361 return McpErrorFactory::internal_error( $request_id, 'Failed to execute tool' );
362 }
363 }
364
365 /**
366 * Create an error CallToolResult from a message string.
367 *
368 * @since 0.5.0
369 *
370 * @param string $message The error message.
371 *
372 * @return \WP\McpSchema\Server\Tools\DTO\CallToolResult
373 */
374 private function create_error_result( string $message ): CallToolResult {
375 return CallToolResult::fromArray(
376 array(
377 'content' => array( ContentBlockHelper::text( $message ) ),
378 'structuredContent' => null,
379 'isError' => true,
380 )
381 );
382 }
383 }
384