PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.3
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 2.3.1 All 196 releases
convertkit / vendor / wordpress / mcp-adapter / includes / Transport / Infrastructure / HttpRequestHandler.php

HttpRequestHandler.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.3, at vendor/wordpress/mcp-adapter/includes/Transport/Infrastructure/HttpRequestHandler.php

339 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * HTTP Request Handler for MCP Transport
4 *
5 * @package McpAdapter
6 */
7
8 declare( strict_types=1 );
9
10 namespace WP\MCP\Transport\Infrastructure;
11
12 use WP\MCP\Core\McpVersionNegotiator;
13 use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
14
15 // Exit if accessed directly.
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Handles HTTP request routing and processing for MCP transports.
20 *
21 * Centralizes request routing logic to eliminate duplication and provide
22 * consistent request handling across transport implementations.
23 *
24 * @internal
25 */
26 class HttpRequestHandler {
27
28 /**
29 * The transport context.
30 *
31 * @var \WP\MCP\Transport\Infrastructure\McpTransportContext
32 */
33 public McpTransportContext $transport_context;
34
35 /**
36 * Constructor.
37 *
38 * @param \WP\MCP\Transport\Infrastructure\McpTransportContext $transport_context The transport context.
39 */
40 public function __construct( McpTransportContext $transport_context ) {
41 $this->transport_context = $transport_context;
42 }
43
44 /**
45 * Get the transport context.
46 *
47 * @since 0.5.0
48 *
49 * @return \WP\MCP\Transport\Infrastructure\McpTransportContext
50 */
51 public function get_transport_context(): McpTransportContext {
52 return $this->transport_context;
53 }
54
55 /**
56 * Route HTTP request to appropriate handler.
57 *
58 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
59 *
60 * @return \WP_REST_Response HTTP response.
61 */
62 public function handle_request( HttpRequestContext $context ): \WP_REST_Response {
63 // Handle POST requests (sending MCP messages to server)
64 if ( 'POST' === $context->method ) {
65 return $this->handle_mcp_request( $context );
66 }
67
68 // Handle GET requests (reserved for SSE streaming; currently not implemented).
69 if ( 'GET' === $context->method ) {
70 return $this->handle_sse_request();
71 }
72
73 // Handle DELETE requests (session termination)
74 if ( 'DELETE' === $context->method ) {
75 return $this->handle_session_termination( $context );
76 }
77
78 // Method not allowed
79 return new \WP_REST_Response(
80 McpErrorFactory::invalid_request( null, 'Method not allowed' )->toArray(),
81 405
82 );
83 }
84
85
86 /**
87 * Handle MCP POST requests.
88 *
89 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
90 *
91 * @return \WP_REST_Response MCP response.
92 */
93 private function handle_mcp_request( HttpRequestContext $context ): \WP_REST_Response {
94 try {
95 // Validate request body
96 if ( null === $context->body ) {
97 return new \WP_REST_Response(
98 McpErrorFactory::parse_error( null, 'Invalid JSON in request body' )->toArray(),
99 400
100 );
101 }
102
103 return $this->process_mcp_messages( $context );
104 } catch ( \Throwable $exception ) {
105 $this->transport_context->mcp_server->get_error_handler()->log(
106 'Unexpected error in handle_mcp_request',
107 array(
108 'transport' => static::class,
109 'server_id' => $this->transport_context->mcp_server->get_server_id(),
110 'error' => $exception->getMessage(),
111 )
112 );
113
114 return new \WP_REST_Response(
115 McpErrorFactory::internal_error( null, 'Handler error occurred' )->toArray(),
116 500
117 );
118 }
119 }
120
121 /**
122 * Process MCP messages using JsonRpcResponseBuilder.
123 *
124 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
125 *
126 * @return \WP_REST_Response MCP response.
127 */
128 private function process_mcp_messages( HttpRequestContext $context ): \WP_REST_Response {
129 $is_batch_request = JsonRpcResponseBuilder::is_batch_request( $context->body );
130 $messages = JsonRpcResponseBuilder::normalize_messages( $context->body );
131
132 $response_body = JsonRpcResponseBuilder::process_messages(
133 $messages,
134 $is_batch_request,
135 function ( array $message ) use ( $context ) {
136 return $this->process_single_message( $message, $context );
137 }
138 );
139
140 // Per MCP spec 2025-06-18: Notifications return HTTP 202 Accepted with no body.
141 // A null response_body indicates only notifications were processed (no requests with IDs).
142 if ( null === $response_body ) {
143 return new \WP_REST_Response( null, 202 );
144 }
145
146 // Determine HTTP status code based on error type
147 if ( ! $is_batch_request && isset( $response_body['error'] ) ) {
148 $http_status = McpErrorFactory::get_http_status_for_error( $response_body );
149
150 return new \WP_REST_Response( $response_body, $http_status );
151 }
152
153 return new \WP_REST_Response( $response_body, 200 );
154 }
155
156 /**
157 * Process a single MCP message.
158 *
159 * @param array $message The MCP JSON-RPC message.
160 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
161 *
162 * @return array|null JSON-RPC response or null for notifications.
163 */
164 private function process_single_message( array $message, HttpRequestContext $context ): ?array {
165 // Validate JSON-RPC message format
166 $validation = McpErrorFactory::validate_jsonrpc_message( $message );
167 if ( true !== $validation ) {
168 return $validation->toArray();
169 }
170
171 // Handle notifications (no response required)
172 if ( isset( $message['method'] ) && ! isset( $message['id'] ) ) {
173 return null; // Notifications don't get a response
174 }
175
176 // Process requests with IDs
177 if ( isset( $message['method'] ) && isset( $message['id'] ) ) {
178 return $this->process_jsonrpc_request( $message, $context );
179 }
180
181 // JSON-RPC responses from client (has result/error, no method) also return null.
182 // Per MCP spec: client responses get HTTP 202 Accepted with no body, same as notifications.
183 return null;
184 }
185
186 /**
187 * Process a JSON-RPC request message.
188 *
189 * @param array $message The JSON-RPC message.
190 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
191 *
192 * @return array JSON-RPC response.
193 */
194 private function process_jsonrpc_request( array $message, HttpRequestContext $context ): array {
195 $request_id = $message['id']; // Preserve original scalar ID (string, number, or null)
196 $method = $message['method'];
197 $params = $message['params'] ?? array();
198
199 // Validate session for all requests except initialize (router will handle initialize session creation)
200 if ( 'initialize' !== $method ) {
201 $session_validation = HttpSessionValidator::validate_session_with_error_handler( $context, $this->transport_context->error_handler );
202 if ( true !== $session_validation ) {
203 return JsonRpcResponseBuilder::create_error_response( $request_id, $session_validation['error'] ?? $session_validation );
204 }
205
206 // Validate MCP-Protocol-Version header for non-initialize requests.
207 $protocol_version_error = $this->validate_protocol_version_header( $context );
208 if ( null !== $protocol_version_error ) {
209 return JsonRpcResponseBuilder::create_error_response( $request_id, $protocol_version_error );
210 }
211 }
212
213 // Route the request through the transport context
214 $result = $this->transport_context->request_router->route_request(
215 $method,
216 $params,
217 $request_id,
218 $this->get_transport_name(),
219 $context
220 );
221
222 // Handle session header if provided by router
223 if ( isset( $result['_session_id'] ) ) {
224 $this->add_session_header_to_response( $result['_session_id'] );
225 unset( $result['_session_id'] ); // Remove from actual response data
226 }
227
228 // Format response based on result
229 if ( isset( $result['error'] ) ) {
230 return JsonRpcResponseBuilder::create_error_response( $request_id, $result['error'] );
231 }
232
233 return JsonRpcResponseBuilder::create_success_response( $request_id, $result );
234 }
235
236 /**
237 * Get transport name for observability.
238 *
239 * @return string Transport name.
240 */
241 private function get_transport_name(): string {
242 return 'HTTP';
243 }
244
245 /**
246 * Validate the MCP-Protocol-Version header on non-initialize requests.
247 *
248 * A missing header is accepted (returns null). A header containing a supported
249 * version is also accepted. An unsupported version returns a JSON-RPC
250 * invalid-request error payload.
251 *
252 * @since 0.5.0
253 *
254 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
255 *
256 * @return array|null Null when the header is absent or valid, error payload otherwise.
257 */
258 private function validate_protocol_version_header( HttpRequestContext $context ): ?array {
259 if ( null === $context->protocol_version ) {
260 return null;
261 }
262
263 if ( McpVersionNegotiator::is_supported( $context->protocol_version ) ) {
264 return null;
265 }
266
267 return McpErrorFactory::create_error(
268 McpErrorFactory::INVALID_REQUEST,
269 sprintf(
270 'Bad Request: Unsupported protocol version: %s (supported versions: %s)',
271 $context->protocol_version,
272 implode( ', ', McpVersionNegotiator::SUPPORTED_PROTOCOL_VERSIONS )
273 )
274 )->toArray();
275 }
276
277 /**
278 * Add session header to the REST response.
279 *
280 * Uses a static flag to prevent multiple filters from being added
281 * if this method is called multiple times during a single request
282 * (e.g., during batch JSON-RPC processing).
283 *
284 * @param string $session_id The session ID to add to the response header.
285 *
286 * @return void
287 */
288 private function add_session_header_to_response( string $session_id ): void {
289 static $current_session_id = null;
290
291 // Only add filter once per request, or if session ID changes
292 if ( null !== $current_session_id && $current_session_id === $session_id ) {
293 return;
294 }
295
296 add_filter(
297 'rest_post_dispatch',
298 static function ( $response ) use ( $session_id ) {
299 if ( $response instanceof \WP_REST_Response ) {
300 $response->header( 'Mcp-Session-Id', $session_id );
301 }
302
303 return $response;
304 }
305 );
306
307 $current_session_id = $session_id;
308 }
309
310 /**
311 * Handle GET requests (SSE streaming).
312 *
313 * @return \WP_REST_Response SSE response.
314 */
315 private function handle_sse_request(): \WP_REST_Response {
316 // SSE streaming not yet implemented - return HTTP 405 with no body
317 return new \WP_REST_Response( null, 405 );
318 }
319
320 /**
321 * Handle DELETE requests (session termination).
322 *
323 * @param \WP\MCP\Transport\Infrastructure\HttpRequestContext $context The HTTP request context.
324 *
325 * @return \WP_REST_Response Termination response.
326 */
327 private function handle_session_termination( HttpRequestContext $context ): \WP_REST_Response {
328 $result = HttpSessionValidator::terminate_session_with_error_handler( $context, $this->transport_context->error_handler );
329
330 if ( true !== $result ) {
331 $http_status = McpErrorFactory::get_http_status_for_error( $result );
332
333 return new \WP_REST_Response( $result, $http_status );
334 }
335
336 return new \WP_REST_Response( null, 200 );
337 }
338 }
339