PluginProbe
ZIP AI – AI Website Builder & AI Agent / 0.0.12
ZIP AI – AI Website Builder & AI Agent v0.0.12
0.0.13 0.0.12 0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / lib / mcp-adapter / includes / Handlers / Tools / ToolsHandler.php

ToolsHandler.php in ZIP AI – AI Website Builder & AI Agent 0.0.12, at lib/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