PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.4
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.4
3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 All 197 releases
convertkit / vendor / wordpress / mcp-adapter / includes / Handlers / Resources / ResourcesHandler.php

ResourcesHandler.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.4, 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