PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.5.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.5.23
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.5.23, at wp-mcp-server/mcp-http-bridge.php

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