PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / vendor / wordpress / mcp-adapter / includes / Transport / Infrastructure / HttpRequestHandler.php
commercebird / vendor / wordpress / mcp-adapter / includes / Transport / Infrastructure Last commit date
HttpRequestContext.php 2 months ago HttpRequestHandler.php 2 months ago HttpSessionValidator.php 2 months ago JsonRpcResponseBuilder.php 2 months ago McpTransportContext.php 2 months ago McpTransportHelperTrait.php 2 months ago RequestRouter.php 2 months ago SessionManager.php 2 months ago
HttpRequestHandler.php
336 lines
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