PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
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 / Transport / Infrastructure / HttpRequestHandler.php

HttpRequestHandler.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at lib/mcp-adapter/includes/Transport/Infrastructure/HttpRequestHandler.php

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