PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.0
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.0
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wordpress / mcp-adapter / includes / Handlers / Tools / ToolsHandler.php

ToolsHandler.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.0, at vendor/wordpress/mcp-adapter/includes/Handlers/Tools/ToolsHandler.php

343 lines 10.6 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\Handlers\HandlerHelperTrait;
15 use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
16 use WP\MCP\Infrastructure\Observability\FailureReason;
17 use WP\McpSchema\Server\Tools\DTO\CallToolResult;
18 use WP\McpSchema\Server\Tools\DTO\ListToolsResult;
19
20 /**
21 * Handles tools-related MCP methods.
22 */
23 class ToolsHandler {
24 use HandlerHelperTrait;
25
26 /**
27 * Default MIME type for image results when none is specified.
28 *
29 * @var string
30 */
31 private const DEFAULT_IMAGE_MIME_TYPE = 'image/png';
32
33 /**
34 * The WordPress MCP instance.
35 *
36 * @var \WP\MCP\Core\McpServer
37 */
38 private McpServer $mcp;
39
40 /**
41 * Constructor.
42 *
43 * @param \WP\MCP\Core\McpServer $mcp The WordPress MCP instance.
44 */
45 public function __construct( McpServer $mcp ) {
46 $this->mcp = $mcp;
47 }
48
49 /**
50 * Handles the tools/list/all request.
51 *
52 * This is a custom extension to the MCP spec that includes availability status.
53 * Returns a ListToolsResult DTO containing all registered tools.
54 *
55 * Note: The 'available' flag is a non-standard extension and is not currently implemented.
56 *
57 * @return \WP\McpSchema\Server\Tools\DTO\ListToolsResult Response with all tools.
58 */
59 public function list_all_tools(): ListToolsResult {
60 // Return the standard tools list.
61 return $this->list_tools();
62 }
63
64 /**
65 * Handles the tools/list request.
66 *
67 * Returns a ListToolsResult DTO containing all registered tools.
68 * Tool DTOs are protocol-only; internal adapter metadata is stored in McpTool instances and is never exposed
69 * to MCP clients.
70 *
71 * @return \WP\McpSchema\Server\Tools\DTO\ListToolsResult Response with tools list.
72 */
73 public function list_tools(): ListToolsResult {
74 $tools = array_values( $this->mcp->get_tools() );
75
76 /**
77 * Filters the list of tools before returning to the client.
78 *
79 * Use this filter to hide tools per user/role, add dynamic tools,
80 * or reorder the tools list.
81 *
82 * @since 0.5.0
83 *
84 * @param array<\WP\McpSchema\Server\Tools\DTO\Tool> $tools Array of Tool DTOs.
85 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
86 */
87 $tools = $this->validate_filtered_list(
88 apply_filters( 'mcp_adapter_tools_list', $tools, $this->mcp ),
89 $tools,
90 'mcp_adapter_tools_list',
91 $this->mcp->get_error_handler()
92 );
93
94 return ListToolsResult::fromArray(
95 array(
96 'tools' => $tools,
97 )
98 );
99 }
100
101 /**
102 * Handles the tools/call request.
103 *
104 * Returns either a CallToolResult DTO (for success or tool execution errors)
105 * or a JSONRPCErrorResponse DTO (for protocol errors like tool not found).
106 *
107 * The MCP spec distinguishes between:
108 * 1. **Protocol errors** (tool not found, server error) → JSONRPCErrorResponse
109 * 2. **Tool execution errors** (permission denied, runtime error) → CallToolResult with isError=true
110 *
111 * This distinction is critical for LLM self-correction - execution errors are
112 * visible to the LLM, while protocol errors indicate infrastructure issues.
113 *
114 * @param array $params Request params.
115 * @param string|int|null $request_id Optional. The request ID for JSON-RPC. Default 0.
116 *
117 * @return \WP\McpSchema\Server\Tools\DTO\CallToolResult|\WP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse
118 */
119 public function call_tool( array $params, $request_id = 0 ) {
120 // Extract parameters using helper method.
121 $request_params = $this->extract_params( $params );
122
123 if ( ! isset( $request_params['name'] ) ) {
124 return McpErrorFactory::missing_parameter( $request_id, 'tool name' );
125 }
126
127 if ( isset( $request_params['arguments'] ) && ! is_array( $request_params['arguments'] ) ) {
128 return McpErrorFactory::invalid_params( $request_id, 'arguments must be an object' );
129 }
130
131 try {
132 $tool_name = trim( (string) $request_params['name'] );
133 $args = $request_params['arguments'] ?? array();
134
135 $mcp_tool = $this->mcp->get_mcp_tool( $tool_name );
136 if ( ! $mcp_tool ) {
137 $this->mcp->get_error_handler()->log(
138 'Tool not found',
139 array(
140 'tool_name' => $tool_name,
141 ),
142 'warning'
143 );
144
145 return McpErrorFactory::tool_not_found( $request_id, $tool_name );
146 }
147
148 $permission = $mcp_tool->check_permission( $args );
149 if ( true !== $permission ) {
150 $error_message = __( 'Permission denied', 'mcp-adapter' );
151 if ( is_wp_error( $permission ) ) {
152 $error_message = $permission->get_error_message();
153
154 $this->mcp->get_error_handler()->log(
155 'Tool permission check failed',
156 array(
157 'tool_name' => $tool_name,
158 'error_code' => $permission->get_error_code(),
159 'error_message' => $permission->get_error_message(),
160 'error_data' => $permission->get_error_data(),
161 'failure_reason' => FailureReason::PERMISSION_CHECK_FAILED,
162 )
163 );
164 }
165
166 return $this->create_error_result( $error_message );
167 }
168
169 /**
170 * Filters tool arguments before execution, or short-circuits execution entirely.
171 *
172 * Return the (optionally modified) arguments array to proceed with execution,
173 * or return a WP_Error to block execution and return an error to the client.
174 *
175 * @since 0.5.0
176 *
177 * @param array $args The tool arguments.
178 * @param string $tool_name The tool name being called.
179 * @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
180 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
181 */
182 $args = apply_filters( 'mcp_adapter_pre_tool_call', $args, $tool_name, $mcp_tool, $this->mcp );
183
184 // Allow pre-filter to short-circuit execution by returning WP_Error.
185 if ( is_wp_error( $args ) ) {
186 return $this->create_error_result( $args->get_error_message() );
187 }
188
189 $result = $mcp_tool->execute( $args );
190
191 /**
192 * Filters the tool execution result before response assembly.
193 *
194 * Use this filter for result transformation, PII redaction,
195 * audit logging, or content enrichment.
196 *
197 * @since 0.5.0
198 *
199 * @param mixed|\WP_Error $result The raw execution result (may be WP_Error).
200 * @param array $args The tool arguments used.
201 * @param string $tool_name The tool name that was called.
202 * @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
203 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
204 */
205 $result = apply_filters( 'mcp_adapter_tool_call_result', $result, $args, $tool_name, $mcp_tool, $this->mcp );
206
207 if ( is_wp_error( $result ) ) {
208 $this->mcp->get_error_handler()->log(
209 'Tool execution returned WP_Error',
210 array(
211 'tool_name' => $tool_name,
212 'error_code' => $result->get_error_code(),
213 'error_message' => $result->get_error_message(),
214 'error_data' => $result->get_error_data(),
215 )
216 );
217
218 return $this->create_error_result( $result->get_error_message() );
219 }
220
221 // Backward compatibility: treat `{ success: false, error: string }` as tool execution error.
222 if (
223 is_array( $result )
224 && array_key_exists( 'success', $result )
225 && false === $result['success']
226 && isset( $result['error'] )
227 && is_string( $result['error'] )
228 && '' !== trim( $result['error'] )
229 ) {
230 return $this->create_error_result( $result['error'] );
231 }
232
233 // Successful tool execution - build CallToolResult DTO.
234
235 // Handle embedded resource results (MCP ContentBlock type: "resource").
236 // This allows tools to return text/blob resources using the MCP schema's EmbeddedResource content block.
237 if ( isset( $result['type'] ) && 'resource' === $result['type'] ) {
238 $resource_item = $result;
239 if ( isset( $result['resource'] ) && is_array( $result['resource'] ) ) {
240 $resource_item = $result['resource'];
241 }
242
243 $uri = $resource_item['uri'] ?? null;
244 $mime_type = $resource_item['mimeType'] ?? null;
245
246 if ( is_string( $uri ) ) {
247 $uri = trim( $uri );
248 }
249
250 // Only return an EmbeddedResource if we have a valid URI and some content.
251 if ( is_string( $uri ) && '' !== $uri ) {
252 if ( isset( $resource_item['text'] ) && is_string( $resource_item['text'] ) ) {
253 return CallToolResult::fromArray(
254 array(
255 'content' => array(
256 ContentBlockHelper::embedded_text_resource(
257 $uri,
258 $resource_item['text'],
259 is_string( $mime_type ) ? $mime_type : null
260 ),
261 ),
262 'isError' => false,
263 )
264 );
265 }
266
267 if ( isset( $resource_item['blob'] ) && is_string( $resource_item['blob'] ) ) {
268 return CallToolResult::fromArray(
269 array(
270 'content' => array(
271 ContentBlockHelper::embedded_blob_resource(
272 $uri,
273 $resource_item['blob'],
274 is_string( $mime_type ) ? $mime_type : null
275 ),
276 ),
277 'isError' => false,
278 )
279 );
280 }
281 }
282 }
283
284 // Handle image results.
285 if ( isset( $result['type'] ) && 'image' === $result['type'] && isset( $result['results'] ) ) {
286 $image_data = base64_encode( $result['results'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
287 $mime_type = $result['mimeType'] ?? self::DEFAULT_IMAGE_MIME_TYPE;
288
289 return CallToolResult::fromArray(
290 array(
291 'content' => array( ContentBlockHelper::image( $image_data, $mime_type ) ),
292 'structuredContent' => null,
293 'isError' => false,
294 )
295 );
296 }
297
298 // Standard result - JSON-encode for text content, include as structuredContent.
299 $json_text = wp_json_encode( $result );
300 if ( false === $json_text ) {
301 $json_text = '{}';
302 }
303
304 return CallToolResult::fromArray(
305 array(
306 'content' => array( ContentBlockHelper::text( $json_text ) ),
307 'structuredContent' => $result,
308 'isError' => false,
309 )
310 );
311 } catch ( \Throwable $exception ) {
312 $this->mcp->get_error_handler()->log(
313 'Error calling tool',
314 array(
315 'tool' => $request_params['name'],
316 'exception' => $exception->getMessage(),
317 )
318 );
319
320 return McpErrorFactory::internal_error( $request_id, 'Failed to execute tool' );
321 }
322 }
323
324 /**
325 * Create an error CallToolResult from a message string.
326 *
327 * @since 0.5.0
328 *
329 * @param string $message The error message.
330 *
331 * @return \WP\McpSchema\Server\Tools\DTO\CallToolResult
332 */
333 private function create_error_result( string $message ): CallToolResult {
334 return CallToolResult::fromArray(
335 array(
336 'content' => array( ContentBlockHelper::text( $message ) ),
337 'structuredContent' => null,
338 'isError' => true,
339 )
340 );
341 }
342 }
343