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.2 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 All 455 releases
elementor / vendor / wordpress / mcp-adapter / includes / Handlers / Resources / ResourcesHandler.php

ResourcesHandler.php in Elementor Website Builder – more than just a page builder 4.3.1, at vendor/wordpress/mcp-adapter/includes/Handlers/Resources/ResourcesHandler.php

319 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Resources method handlers for MCP requests.
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Handlers\Resources;
11
12 use WP\MCP\Core\McpServer;
13 use WP\MCP\Domain\Utils\McpValidator;
14 use WP\MCP\Handlers\HandlerHelperTrait;
15 use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
16 use WP\McpSchema\Common\Protocol\DTO\BlobResourceContents;
17 use WP\McpSchema\Common\Protocol\DTO\TextResourceContents;
18 use WP\McpSchema\Server\Resources\DTO\ListResourceTemplatesResult;
19 use WP\McpSchema\Server\Resources\DTO\ListResourcesResult;
20 use WP\McpSchema\Server\Resources\DTO\ReadResourceResult;
21
22 /**
23 * Handles resources-related MCP methods.
24 */
25 class ResourcesHandler {
26 use HandlerHelperTrait;
27
28 /**
29 * The WordPress MCP instance.
30 *
31 * @var \WP\MCP\Core\McpServer
32 */
33 private McpServer $mcp;
34
35 /**
36 * Constructor.
37 *
38 * @param \WP\MCP\Core\McpServer $mcp The WordPress MCP instance.
39 */
40 public function __construct( McpServer $mcp ) {
41 $this->mcp = $mcp;
42 }
43
44
45 /**
46 * Handles the resources/list request.
47 *
48 * Returns a ListResourcesResult DTO containing all registered resources.
49 * Returns protocol DTOs as-is; any `_meta` fields are passed through unchanged.
50 *
51 * @return \WP\McpSchema\Server\Resources\DTO\ListResourcesResult Response with resources list.
52 */
53 public function list_resources(): ListResourcesResult {
54 $resources = array_values( $this->mcp->get_resources() );
55
56 /**
57 * Filters the list of resources before returning to the client.
58 *
59 * Use this filter to filter resources by context, add dynamic resources,
60 * or reorder the resources list.
61 *
62 * @since 0.5.0
63 *
64 * @param array<\WP\McpSchema\Server\Resources\DTO\Resource> $resources Array of Resource DTOs.
65 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
66 */
67 $resources = $this->validate_filtered_list(
68 apply_filters( 'mcp_adapter_resources_list', $resources, $this->mcp ),
69 $resources,
70 'mcp_adapter_resources_list',
71 $this->mcp->get_error_handler()
72 );
73
74 return ListResourcesResult::fromArray(
75 array(
76 'resources' => $resources,
77 )
78 );
79 }
80
81 /**
82 * Handles the resources/templates/list request.
83 *
84 * The adapter has no resource-template concept, so this always returns an empty
85 * list. The method still needs a handler: `resources/templates/list` is part of
86 * the base `resources` capability (no sub-flag gates it), which the server always
87 * advertises, so spec-compliant clients call it during resource discovery.
88 *
89 * @return \WP\McpSchema\Server\Resources\DTO\ListResourceTemplatesResult Empty resource-templates list.
90 */
91 public function list_resource_templates(): ListResourceTemplatesResult {
92 return ListResourceTemplatesResult::fromArray(
93 array(
94 'resourceTemplates' => array(),
95 )
96 );
97 }
98
99 /**
100 * Handles the resources/read request.
101 *
102 * Returns either a ReadResourceResult DTO (for success) or a JSONRPCErrorResponse DTO
103 * (for protocol errors like missing parameter or resource not found).
104 *
105 * Unlike tools, resources don't have a concept of "execution errors" that should be
106 * reported with isError=true. Resource reads either succeed or fail at the protocol level.
107 *
108 * @param array $params Request parameters.
109 * @param string|int|null $request_id Optional. The request ID for JSON-RPC. Default 0.
110 *
111 * @return \WP\McpSchema\Server\Resources\DTO\ReadResourceResult|\WP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse
112 */
113 public function read_resource( array $params, $request_id = 0 ) {
114 // Extract parameters using helper method.
115 $request_params = $this->extract_params( $params );
116
117 if ( ! isset( $request_params['uri'] ) ) {
118 return McpErrorFactory::missing_parameter( $request_id, 'uri' );
119 }
120
121 $uri = $request_params['uri'];
122 $uri = is_string( $uri ) ? trim( $uri ) : '';
123
124 $mcp_resource = $this->mcp->get_mcp_resource( $uri );
125 if ( ! $mcp_resource ) {
126 return McpErrorFactory::resource_not_found( $request_id, $uri );
127 }
128
129 /** @var \WP\McpSchema\Server\Resources\DTO\Resource $resource */
130 $resource = $mcp_resource->get_protocol_dto();
131
132 try {
133 $has_permission = $mcp_resource->check_permission( $request_params );
134 if ( true !== $has_permission ) {
135 // Extract detailed error message if WP_Error was returned.
136 $error_message = 'Access denied for resource: ' . $resource->getName();
137
138 if ( is_wp_error( $has_permission ) ) {
139 $error_message = $has_permission->get_error_message();
140 }
141
142 return McpErrorFactory::permission_denied( $request_id, $error_message );
143 }
144
145 /**
146 * Filters resource parameters before execution, or short-circuits execution entirely.
147 *
148 * Return the (optionally modified) parameters array to proceed with execution,
149 * or return a WP_Error to block execution and return an error to the client.
150 *
151 * @since 0.5.0
152 *
153 * @param array $params The request parameters.
154 * @param string $uri The resource URI.
155 * @param \WP\MCP\Domain\Resources\McpResource $mcp_resource The MCP resource instance.
156 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
157 */
158 $request_params = apply_filters( 'mcp_adapter_pre_resource_read', $request_params, $uri, $mcp_resource, $this->mcp );
159
160 // Allow pre-filter to short-circuit execution by returning WP_Error.
161 if ( is_wp_error( $request_params ) ) {
162 return McpErrorFactory::internal_error( $request_id, $request_params->get_error_message() );
163 }
164
165 $contents = $mcp_resource->execute( $request_params );
166
167 /**
168 * Filters the resource contents after execution.
169 *
170 * Use this filter for content transformation, caching storage,
171 * PII redaction, or audit logging.
172 *
173 * @since 0.5.0
174 *
175 * @param mixed|\WP_Error $contents The raw resource contents (may be WP_Error).
176 * @param array $params The request parameters used.
177 * @param string $uri The resource URI.
178 * @param \WP\MCP\Domain\Resources\McpResource $mcp_resource The MCP resource instance.
179 * @param \WP\MCP\Core\McpServer $server The MCP server instance.
180 */
181 $contents = apply_filters( 'mcp_adapter_resource_read_result', $contents, $request_params, $uri, $mcp_resource, $this->mcp );
182
183 // Handle WP_Error objects returned by McpResource execution.
184 if ( is_wp_error( $contents ) ) {
185 $this->mcp->get_error_handler()->log(
186 'Resource execution returned WP_Error object',
187 array(
188 'uri' => $uri,
189 'error_code' => $contents->get_error_code(),
190 'error_message' => $contents->get_error_message(),
191 )
192 );
193
194 return McpErrorFactory::internal_error( $request_id, $contents->get_error_message() );
195 }
196
197 // Successful execution - convert contents to DTOs.
198 // Contents should be an array of resource content items.
199 // If it's already an array of properly formatted items, convert each to a DTO.
200 // Otherwise, wrap the result as text content.
201 //
202 // Seed the fallback content URI with the advertised URI, not the client's
203 // request URI, so contents[].uri matches resources/list even when the client
204 // lowercased the scheme (RFC 3986 3.1). For an exact-case read the two are equal.
205 $content_dtos = $this->convert_contents_to_dtos( $contents, $resource->getUri() );
206
207 return ReadResourceResult::fromArray(
208 array(
209 'contents' => $content_dtos,
210 )
211 );
212 } catch ( \Throwable $exception ) {
213 $this->mcp->get_error_handler()->log(
214 'Error reading resource',
215 array(
216 'uri' => $uri,
217 'exception' => $exception->getMessage(),
218 )
219 );
220
221 return McpErrorFactory::internal_error( $request_id, 'Failed to read resource' );
222 }
223 }
224
225 /**
226 * Convert ability execution results to resource content DTOs.
227 *
228 * The MCP spec expects contents to be an array of TextResourceContents or BlobResourceContents.
229 * This method handles various return formats from abilities and normalizes them.
230 *
231 * @param mixed $contents The contents returned by the ability.
232 * @param string $uri The resource URI.
233 *
234 * @return array<\WP\McpSchema\Common\Protocol\DTO\TextResourceContents|\WP\McpSchema\Common\Protocol\DTO\BlobResourceContents>
235 */
236 private function convert_contents_to_dtos( $contents, string $uri ): array {
237 // If contents is already an array of properly structured items, convert each.
238 if ( is_array( $contents ) && ! empty( $contents ) ) {
239 // Check if this is an array of content items (has 'uri', 'text', or 'blob' in first item).
240 $first_item = reset( $contents );
241 if ( is_array( $first_item ) && ( isset( $first_item['uri'] ) || isset( $first_item['text'] ) || isset( $first_item['blob'] ) ) ) {
242 return array_map(
243 function ( $item ) use ( $uri ) {
244 return $this->create_content_dto( $item, $uri );
245 },
246 $contents
247 );
248 }
249 }
250
251 // Fallback: wrap as a single text content item.
252 if ( is_string( $contents ) ) {
253 $text = $contents;
254 } else {
255 $text = wp_json_encode( $contents );
256 if ( false === $text ) {
257 $text = '{}';
258 }
259 }
260
261 return array(
262 TextResourceContents::fromArray(
263 array(
264 'uri' => $uri,
265 'text' => $text,
266 )
267 ),
268 );
269 }
270
271 /**
272 * Create a content DTO from an array item.
273 *
274 * `_meta` is carried through from the item so metadata a handler attaches to its
275 * resource contents reaches the client. MCP Apps UI resources rely on this: they
276 * put CSP config and border hints under `_meta.ui` alongside the HTML body.
277 *
278 * A list-shaped `_meta` is omitted because MCP declares this field as a JSON object.
279 *
280 * Every key is optional and read defensively, because a handler returns whatever
281 * WordPress handed it: `blob` and `text` are cast to string, `mimeType` is kept
282 * only when it already is one, and an absent `uri` falls back to $default_uri.
283 *
284 * @param array{uri?: mixed, mimeType?: mixed, text?: mixed, blob?: mixed, _meta?: mixed} $item The content item array.
285 * @param string $default_uri The URI to use when the item names none.
286 *
287 * @return \WP\McpSchema\Common\Protocol\DTO\TextResourceContents|\WP\McpSchema\Common\Protocol\DTO\BlobResourceContents
288 */
289 private function create_content_dto( array $item, string $default_uri ) {
290 $item_uri = $item['uri'] ?? $default_uri;
291 $mime_type = $item['mimeType'] ?? null;
292 $meta = McpValidator::normalize_meta( $item['_meta'] ?? null );
293
294 // If there's blob data, create BlobResourceContents.
295 if ( isset( $item['blob'] ) ) {
296 return BlobResourceContents::fromArray(
297 array(
298 'uri' => $item_uri,
299 'blob' => (string) $item['blob'],
300 'mimeType' => is_string( $mime_type ) ? $mime_type : null,
301 '_meta' => $meta,
302 )
303 );
304 }
305
306 // Default to TextResourceContents.
307 $text = $item['text'] ?? '';
308
309 return TextResourceContents::fromArray(
310 array(
311 'uri' => $item_uri,
312 'text' => (string) $text,
313 'mimeType' => is_string( $mime_type ) ? $mime_type : null,
314 '_meta' => $meta,
315 )
316 );
317 }
318 }
319