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