PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / mcp-http-bridge.php

mcp-http-bridge.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at wp-mcp-server/mcp-http-bridge.php

544 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 #!/usr/bin/env php
2 <?php
3 /**
4 * MCP HTTP Bridge for WordPress (PHP Implementation)
5 *
6 * This bridge allows MCP clients to communicate with WordPress MCP tools
7 * via HTTP using JSON-RPC protocol.
8 *
9 * Usage:
10 * # Run standalone HTTP server
11 * php mcp-http-bridge.php --port=3000 --host=localhost
12 *
13 * # Or use PHP built-in server
14 * php -S localhost:3000 mcp-http-bridge.php
15 *
16 * Configuration (Claude Desktop/Code):
17 * {
18 * "mcpServers": {
19 * "wordpress-metasync": {
20 * "url": "http://localhost:3000",
21 * "transport": "http"
22 * }
23 * }
24 * }
25 *
26 * Environment Variables:
27 * WP_MCP_PORT=3000 - HTTP server port
28 * WP_MCP_HOST=localhost - HTTP server host
29 * WP_MCP_API_KEY=secret - Optional API key for authentication
30 *
31 * @package Metasync
32 * @subpackage Metasync/wp-mcp-server
33 * @since 2.0.0
34 */
35
36 // Signal to metasync_init_mcp_server() that MCP init must not be skipped (WP-255).
37 define('METASYNC_MCP_BRIDGE', true);
38
39 // Check if running as standalone server or via PHP built-in server
40 $is_builtin_server = php_sapi_name() === 'cli-server';
41 $is_cli = php_sapi_name() === 'cli';
42
43 if (!$is_builtin_server && !$is_cli) {
44 http_response_code(500);
45 die("Error: This script must be run from the command line or via PHP built-in server\n");
46 }
47
48 // Suppress unnecessary WordPress output
49 define('WP_CLI', true);
50 define('DOING_AJAX', true);
51 define('WP_USE_THEMES', false);
52 define('DISABLE_WP_CRON', true);
53
54 // Determine WordPress root path
55 // Try multiple possible locations for WordPress installation
56
57 $possible_paths = [
58 // Docker container path
59 '/var/www/html/wp-load.php',
60 // Standard installation (5 levels up from script)
61 dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/wp-load.php',
62 // Alternative: 4 levels up
63 dirname(dirname(dirname(dirname(__FILE__)))) . '/wp-load.php',
64 // Bedrock/custom structure
65 dirname(dirname(dirname(dirname(dirname(dirname(__FILE__)))))) . '/wp-load.php',
66 ];
67
68 $wp_load_path = null;
69 foreach ($possible_paths as $path) {
70 if (file_exists($path)) {
71 $wp_load_path = $path;
72 break;
73 }
74 }
75
76 if (!$wp_load_path) {
77 $error_msg = "Error: Cannot find WordPress wp-load.php\nTried: " . implode(', ', $possible_paths);
78 if ($is_builtin_server) {
79 http_response_code(500);
80 die($error_msg);
81 } else {
82 fwrite(STDERR, $error_msg . "\n");
83 exit(1);
84 }
85 }
86
87 // Bootstrap WordPress
88 require_once $wp_load_path;
89
90 // Verify MCP server is available
91 global $metasync_mcp_server;
92 if (!isset($metasync_mcp_server) || !$metasync_mcp_server) {
93 if ($is_builtin_server) {
94 http_response_code(500);
95 die("Error: Metasync MCP server not initialized");
96 } else {
97 fwrite(STDERR, "Error: Metasync MCP server not initialized\n");
98 exit(1);
99 }
100 }
101
102 // Disable output buffering
103 ob_implicit_flush(true);
104 while (ob_get_level()) {
105 ob_end_clean();
106 }
107
108 // If running via PHP built-in server, handle the request immediately
109 if ($is_builtin_server) {
110 handle_http_request();
111 exit(0);
112 }
113
114 // Otherwise, start standalone HTTP server
115 start_standalone_server();
116
117 /**
118 * Start standalone HTTP server (socket-based)
119 */
120 function start_standalone_server() {
121 // Parse command line arguments
122 $options = getopt('', ['port:', 'host:']);
123 $port = $options['port'] ?? getenv('WP_MCP_PORT') ?: 3000;
124 $host = $options['host'] ?? getenv('WP_MCP_HOST') ?: 'localhost';
125
126 echo "Starting MCP HTTP Bridge (PHP)...\n";
127 echo "WordPress version: " . get_bloginfo('version') . "\n";
128 echo "Metasync version: " . (defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown') . "\n";
129 echo "Listening on http://{$host}:{$port}\n";
130 echo "Press Ctrl+C to stop\n\n";
131
132 // Create socket
133 $socket = @stream_socket_server("tcp://{$host}:{$port}", $errno, $errstr);
134
135 if (!$socket) {
136 fwrite(STDERR, "Error: Could not create socket: $errstr ($errno)\n");
137 exit(1);
138 }
139
140 // Accept connections in loop
141 while (true) {
142 $client = @stream_socket_accept($socket, -1);
143 if (!$client) {
144 continue;
145 }
146
147 // Read HTTP request
148 $request = '';
149 while (!feof($client)) {
150 $line = fgets($client);
151 $request .= $line;
152 if (trim($line) === '') {
153 // Headers ended, read body if present
154 $headers = parse_http_headers($request);
155 if (isset($headers['content-length'])) {
156 $body = fread($client, (int)$headers['content-length']);
157 $request .= $body;
158 }
159 break;
160 }
161 }
162
163 // Process request
164 $response = process_http_request($request);
165
166 // Send response
167 fwrite($client, $response);
168 fclose($client);
169 }
170
171 fclose($socket);
172 }
173
174 /**
175 * Handle HTTP request (for PHP built-in server)
176 */
177 function handle_http_request() {
178 // Set CORS headers
179 set_cors_headers();
180
181 // Handle OPTIONS (preflight)
182 if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
183 http_response_code(204);
184 exit(0);
185 }
186
187 // Health check endpoint
188 if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_SERVER['REQUEST_URI'] === '/health') {
189 handle_health_check();
190 exit(0);
191 }
192
193 // MCP endpoint - must be POST
194 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
195 http_response_code(405);
196 header('Content-Type: application/json');
197 echo json_encode(['error' => 'Method not allowed. Use POST for MCP requests.']);
198 exit(0);
199 }
200
201 // Check authentication if API key is set
202 check_authentication();
203
204 // Read request body
205 $request_body = file_get_contents('php://input');
206
207 if (empty($request_body)) {
208 http_response_code(400);
209 header('Content-Type: application/json');
210 echo json_encode(['error' => 'Empty request body']);
211 exit(0);
212 }
213
214 try {
215 // Parse JSON-RPC request
216 $request = json_decode($request_body, true);
217 if (json_last_error() !== JSON_ERROR_NONE) {
218 throw new Exception('Invalid JSON: ' . json_last_error_msg());
219 }
220
221 // Process MCP request
222 $response = process_mcp_request($request);
223
224 // Send response
225 http_response_code(200);
226 header('Content-Type: application/json');
227 echo json_encode($response);
228
229 } catch (Exception $e) {
230 http_response_code(500);
231 header('Content-Type: application/json');
232 echo json_encode([
233 'jsonrpc' => '2.0',
234 'id' => isset($request['id']) ? $request['id'] : null,
235 'error' => [
236 'code' => -32603,
237 'message' => $e->getMessage()
238 ]
239 ]);
240 }
241 }
242
243 /**
244 * Process HTTP request (for standalone server)
245 */
246 function process_http_request($http_request) {
247 $lines = explode("\r\n", $http_request);
248 $request_line = $lines[0];
249 $parts = explode(' ', $request_line);
250
251 $method = $parts[0] ?? 'GET';
252 $path = $parts[1] ?? '/';
253
254 // Parse headers
255 $headers = parse_http_headers($http_request);
256
257 // Set default headers
258 $response_headers = [
259 'HTTP/1.1 200 OK',
260 'Content-Type: application/json',
261 'Access-Control-Allow-Origin: *',
262 'Access-Control-Allow-Methods: GET, POST, OPTIONS',
263 'Access-Control-Allow-Headers: Content-Type, X-API-Key',
264 ];
265
266 // Handle OPTIONS (preflight)
267 if ($method === 'OPTIONS') {
268 $response_headers[0] = 'HTTP/1.1 204 No Content';
269 return implode("\r\n", $response_headers) . "\r\n\r\n";
270 }
271
272 // Health check
273 if ($method === 'GET' && $path === '/health') {
274 $body = json_encode([
275 'status' => 'ok',
276 'service' => 'wordpress-metasync-mcp',
277 'wordpress_version' => get_bloginfo('version'),
278 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
279 'timestamp' => time()
280 ]);
281
282 $response_headers[] = 'Content-Length: ' . strlen($body);
283 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
284 }
285
286 // MCP endpoint - must be POST
287 if ($method !== 'POST') {
288 $response_headers[0] = 'HTTP/1.1 405 Method Not Allowed';
289 $body = json_encode(['error' => 'Method not allowed']);
290 $response_headers[] = 'Content-Length: ' . strlen($body);
291 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
292 }
293
294 // Check authentication
295 $api_key = getenv('WP_MCP_API_KEY');
296 if ($api_key && (!isset($headers['x-api-key']) || $headers['x-api-key'] !== $api_key)) {
297 $response_headers[0] = 'HTTP/1.1 401 Unauthorized';
298 $body = json_encode(['error' => 'Invalid or missing API key']);
299 $response_headers[] = 'Content-Length: ' . strlen($body);
300 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
301 }
302
303 // Extract request body
304 $body_start = strpos($http_request, "\r\n\r\n");
305 $request_body = $body_start !== false ? substr($http_request, $body_start + 4) : '';
306
307 if (empty($request_body)) {
308 $response_headers[0] = 'HTTP/1.1 400 Bad Request';
309 $body = json_encode(['error' => 'Empty request body']);
310 $response_headers[] = 'Content-Length: ' . strlen($body);
311 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
312 }
313
314 try {
315 // Parse JSON-RPC request
316 $request = json_decode($request_body, true);
317 if (json_last_error() !== JSON_ERROR_NONE) {
318 throw new Exception('Invalid JSON: ' . json_last_error_msg());
319 }
320
321 // Process MCP request
322 $response = process_mcp_request($request);
323 $body = json_encode($response);
324
325 $response_headers[] = 'Content-Length: ' . strlen($body);
326 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
327
328 } catch (Exception $e) {
329 $response_headers[0] = 'HTTP/1.1 500 Internal Server Error';
330 $body = json_encode([
331 'jsonrpc' => '2.0',
332 'id' => isset($request['id']) ? $request['id'] : null,
333 'error' => [
334 'code' => -32603,
335 'message' => $e->getMessage()
336 ]
337 ]);
338 $response_headers[] = 'Content-Length: ' . strlen($body);
339 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
340 }
341 }
342
343 /**
344 * Parse HTTP headers
345 */
346 function parse_http_headers($http_request) {
347 $headers = [];
348 $lines = explode("\r\n", $http_request);
349
350 foreach ($lines as $line) {
351 if (strpos($line, ':') !== false) {
352 list($key, $value) = explode(':', $line, 2);
353 $headers[strtolower(trim($key))] = trim($value);
354 }
355 }
356
357 return $headers;
358 }
359
360 /**
361 * Set CORS headers
362 */
363 function set_cors_headers() {
364 header('Access-Control-Allow-Origin: *');
365 header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
366 header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
367 }
368
369 /**
370 * Check authentication
371 */
372 function check_authentication() {
373 $api_key = getenv('WP_MCP_API_KEY');
374 if (!$api_key) {
375 return; // No authentication required
376 }
377
378 $provided_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
379
380 if ($provided_key !== $api_key) {
381 http_response_code(401);
382 header('Content-Type: application/json');
383 echo json_encode(['error' => 'Invalid or missing API key']);
384 exit(0);
385 }
386 }
387
388 /**
389 * Handle health check
390 */
391 function handle_health_check() {
392 global $metasync_mcp_server;
393
394 $tools_count = count($metasync_mcp_server->get_tools());
395
396 http_response_code(200);
397 header('Content-Type: application/json');
398 echo json_encode([
399 'status' => 'ok',
400 'service' => 'wordpress-metasync-mcp',
401 'wordpress_version' => get_bloginfo('version'),
402 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
403 'tools_count' => $tools_count,
404 'timestamp' => time()
405 ]);
406 }
407
408 /**
409 * Process MCP JSON-RPC request
410 *
411 * @param array $request JSON-RPC request
412 * @return array JSON-RPC response
413 */
414 function process_mcp_request($request) {
415 global $metasync_mcp_server;
416
417 $method = $request['method'] ?? '';
418 $params = $request['params'] ?? [];
419 $id = $request['id'] ?? null;
420
421 switch ($method) {
422 case 'initialize':
423 return handle_initialize($id, $params);
424
425 case 'notifications/initialized':
426 // Client confirms initialization - no response needed for notification
427 return null;
428
429 case 'tools/list':
430 return handle_tools_list($id);
431
432 case 'tools/call':
433 return handle_tools_call($id, $params);
434
435 case 'ping':
436 return [
437 'jsonrpc' => '2.0',
438 'id' => $id,
439 'result' => [
440 'status' => 'ok',
441 'timestamp' => time()
442 ]
443 ];
444
445 default:
446 throw new Exception("Unknown method: $method");
447 }
448 }
449
450 /**
451 * Handle initialize request
452 */
453 function handle_initialize($id, $params) {
454 $client_info = $params['clientInfo'] ?? [];
455
456 return [
457 'jsonrpc' => '2.0',
458 'id' => $id,
459 'result' => [
460 'protocolVersion' => '2024-11-05',
461 'capabilities' => [
462 'tools' => (object)[]
463 ],
464 'serverInfo' => [
465 'name' => 'wordpress-metasync',
466 'version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '2.0.0'
467 ]
468 ]
469 ];
470 }
471
472 /**
473 * Handle tools/list request
474 */
475 function handle_tools_list($id) {
476 global $metasync_mcp_server;
477
478 $tools = [];
479 $tool_objects = $metasync_mcp_server->get_tools();
480
481 foreach ($tool_objects as $tool) {
482 $tools[] = [
483 'name' => $tool->get_name(),
484 'description' => $tool->get_description(),
485 'inputSchema' => $tool->get_input_schema()
486 ];
487 }
488
489 return [
490 'jsonrpc' => '2.0',
491 'id' => $id,
492 'result' => [
493 'tools' => $tools
494 ]
495 ];
496 }
497
498 /**
499 * Handle tools/call request
500 */
501 function handle_tools_call($id, $params) {
502 global $metasync_mcp_server;
503
504 $tool_name = $params['name'] ?? '';
505 $arguments = $params['arguments'] ?? [];
506
507 if (empty($tool_name)) {
508 throw new Exception('Tool name is required');
509 }
510
511 // Find tool
512 $tool = $metasync_mcp_server->get_tool($tool_name);
513 if (!$tool) {
514 throw new Exception("Tool not found: $tool_name");
515 }
516
517 // Execute tool
518 $result = $tool->execute($arguments);
519
520 // Format result as MCP response
521 $content = [];
522
523 if (is_array($result) || is_object($result)) {
524 $content[] = [
525 'type' => 'text',
526 'text' => json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
527 ];
528 } else {
529 $content[] = [
530 'type' => 'text',
531 'text' => (string)$result
532 ];
533 }
534
535 return [
536 'jsonrpc' => '2.0',
537 'id' => $id,
538 'result' => [
539 'content' => $content,
540 'isError' => false
541 ]
542 ];
543 }
544