PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.15
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.15
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 2.6.4 2.6.3 2.5.23 All 138 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.15, at wp-mcp-server/mcp-http-bridge.php

564 lines 14.0 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 — deny by default when no API key is configured
295 $api_key = getenv('WP_MCP_API_KEY');
296 if (!$api_key) {
297 $response_headers[0] = 'HTTP/1.1 401 Unauthorized';
298 $body = json_encode(['error' => 'MCP bridge authentication not configured. Set WP_MCP_API_KEY environment variable.']);
299 $response_headers[] = 'Content-Length: ' . strlen($body);
300 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
301 }
302 if (!isset($headers['x-api-key']) || !hash_equals($api_key, $headers['x-api-key'])) {
303 $response_headers[0] = 'HTTP/1.1 401 Unauthorized';
304 $body = json_encode(['error' => 'Invalid or missing API key']);
305 $response_headers[] = 'Content-Length: ' . strlen($body);
306 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
307 }
308
309 // Extract request body
310 $body_start = strpos($http_request, "\r\n\r\n");
311 $request_body = $body_start !== false ? substr($http_request, $body_start + 4) : '';
312
313 if (empty($request_body)) {
314 $response_headers[0] = 'HTTP/1.1 400 Bad Request';
315 $body = json_encode(['error' => 'Empty request body']);
316 $response_headers[] = 'Content-Length: ' . strlen($body);
317 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
318 }
319
320 try {
321 // Parse JSON-RPC request
322 $request = json_decode($request_body, true);
323 if (json_last_error() !== JSON_ERROR_NONE) {
324 throw new Exception('Invalid JSON: ' . json_last_error_msg());
325 }
326
327 // Process MCP request
328 $response = process_mcp_request($request);
329 $body = json_encode($response);
330
331 $response_headers[] = 'Content-Length: ' . strlen($body);
332 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
333
334 } catch (Exception $e) {
335 $response_headers[0] = 'HTTP/1.1 500 Internal Server Error';
336 $body = json_encode([
337 'jsonrpc' => '2.0',
338 'id' => isset($request['id']) ? $request['id'] : null,
339 'error' => [
340 'code' => -32603,
341 'message' => $e->getMessage()
342 ]
343 ]);
344 $response_headers[] = 'Content-Length: ' . strlen($body);
345 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
346 }
347 }
348
349 /**
350 * Parse HTTP headers
351 */
352 function parse_http_headers($http_request) {
353 $headers = [];
354 $lines = explode("\r\n", $http_request);
355
356 foreach ($lines as $line) {
357 if (strpos($line, ':') !== false) {
358 list($key, $value) = explode(':', $line, 2);
359 $headers[strtolower(trim($key))] = trim($value);
360 }
361 }
362
363 return $headers;
364 }
365
366 /**
367 * Set CORS headers
368 */
369 function set_cors_headers() {
370 $allowed_origins = array();
371 if (function_exists('home_url')) {
372 $allowed_origins[] = home_url();
373 }
374 if (function_exists('site_url')) {
375 $allowed_origins[] = site_url();
376 }
377 $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
378 if (!empty($origin) && in_array($origin, $allowed_origins, true)) {
379 header('Access-Control-Allow-Origin: ' . $origin);
380 }
381 header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
382 header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
383 }
384
385 /**
386 * Check authentication
387 */
388 function check_authentication() {
389 $api_key = getenv('WP_MCP_API_KEY');
390 if (!$api_key) {
391 // Deny by default when no API key is configured
392 http_response_code(401);
393 header('Content-Type: application/json');
394 echo json_encode(['error' => 'MCP bridge authentication not configured. Set WP_MCP_API_KEY environment variable.']);
395 exit(0);
396 }
397
398 $provided_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
399
400 if (!hash_equals($api_key, $provided_key)) {
401 http_response_code(401);
402 header('Content-Type: application/json');
403 echo json_encode(['error' => 'Invalid or missing API key']);
404 exit(0);
405 }
406 }
407
408 /**
409 * Handle health check
410 */
411 function handle_health_check() {
412 global $metasync_mcp_server;
413
414 $tools_count = count($metasync_mcp_server->get_tools());
415
416 http_response_code(200);
417 header('Content-Type: application/json');
418 echo json_encode([
419 'status' => 'ok',
420 'service' => 'wordpress-metasync-mcp',
421 'wordpress_version' => get_bloginfo('version'),
422 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
423 'tools_count' => $tools_count,
424 'timestamp' => time()
425 ]);
426 }
427
428 /**
429 * Process MCP JSON-RPC request
430 *
431 * @param array $request JSON-RPC request
432 * @return array JSON-RPC response
433 */
434 function process_mcp_request($request) {
435 global $metasync_mcp_server;
436
437 $method = $request['method'] ?? '';
438 $params = $request['params'] ?? [];
439 $id = $request['id'] ?? null;
440
441 switch ($method) {
442 case 'initialize':
443 return handle_initialize($id, $params);
444
445 case 'notifications/initialized':
446 // Client confirms initialization - no response needed for notification
447 return null;
448
449 case 'tools/list':
450 return handle_tools_list($id);
451
452 case 'tools/call':
453 return handle_tools_call($id, $params);
454
455 case 'ping':
456 return [
457 'jsonrpc' => '2.0',
458 'id' => $id,
459 'result' => [
460 'status' => 'ok',
461 'timestamp' => time()
462 ]
463 ];
464
465 default:
466 throw new Exception("Unknown method: $method");
467 }
468 }
469
470 /**
471 * Handle initialize request
472 */
473 function handle_initialize($id, $params) {
474 $client_info = $params['clientInfo'] ?? [];
475
476 return [
477 'jsonrpc' => '2.0',
478 'id' => $id,
479 'result' => [
480 'protocolVersion' => '2024-11-05',
481 'capabilities' => [
482 'tools' => (object)[]
483 ],
484 'serverInfo' => [
485 'name' => 'wordpress-metasync',
486 'version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '2.0.0'
487 ]
488 ]
489 ];
490 }
491
492 /**
493 * Handle tools/list request
494 */
495 function handle_tools_list($id) {
496 global $metasync_mcp_server;
497
498 $tools = [];
499 $tool_objects = $metasync_mcp_server->get_tools();
500
501 foreach ($tool_objects as $tool) {
502 $tools[] = [
503 'name' => $tool->get_name(),
504 'description' => $tool->get_description(),
505 'inputSchema' => $tool->get_input_schema()
506 ];
507 }
508
509 return [
510 'jsonrpc' => '2.0',
511 'id' => $id,
512 'result' => [
513 'tools' => $tools
514 ]
515 ];
516 }
517
518 /**
519 * Handle tools/call request
520 */
521 function handle_tools_call($id, $params) {
522 global $metasync_mcp_server;
523
524 $tool_name = $params['name'] ?? '';
525 $arguments = $params['arguments'] ?? [];
526
527 if (empty($tool_name)) {
528 throw new Exception('Tool name is required');
529 }
530
531 // Find tool
532 $tool = $metasync_mcp_server->get_tool($tool_name);
533 if (!$tool) {
534 throw new Exception("Tool not found: $tool_name");
535 }
536
537 // Execute tool
538 $result = $tool->execute($arguments);
539
540 // Format result as MCP response
541 $content = [];
542
543 if (is_array($result) || is_object($result)) {
544 $content[] = [
545 'type' => 'text',
546 'text' => json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
547 ];
548 } else {
549 $content[] = [
550 'type' => 'text',
551 'text' => (string)$result
552 ];
553 }
554
555 return [
556 'jsonrpc' => '2.0',
557 'id' => $id,
558 'result' => [
559 'content' => $content,
560 'isError' => false
561 ]
562 ];
563 }
564