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

402 lines 10.9 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 - Required 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.
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 // Authenticate every bridge request.
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 $result = process_mcp_request($request_body);
215 header('Content-Type: application/json');
216 http_response_code($result['status']);
217 foreach ($result['headers'] as $name => $value) {
218 header($name . ': ' . $value);
219 }
220 if ($result['body'] !== null) {
221 echo json_encode($result['body']);
222 }
223 }
224
225 /**
226 * Process HTTP request (for standalone server)
227 */
228 function process_http_request($http_request) {
229 $lines = explode("\r\n", $http_request);
230 $request_line = $lines[0];
231 $parts = explode(' ', $request_line);
232
233 $method = $parts[0] ?? 'GET';
234 $path = $parts[1] ?? '/';
235
236 // Parse headers
237 $headers = parse_http_headers($http_request);
238
239 // Set default headers
240 $response_headers = [
241 'HTTP/1.1 200 OK',
242 'Content-Type: application/json',
243 'Access-Control-Allow-Origin: *',
244 'Access-Control-Allow-Methods: GET, POST, OPTIONS',
245 'Access-Control-Allow-Headers: Content-Type, X-API-Key',
246 ];
247
248 // Handle OPTIONS (preflight)
249 if ($method === 'OPTIONS') {
250 $response_headers[0] = 'HTTP/1.1 204 No Content';
251 return implode("\r\n", $response_headers) . "\r\n\r\n";
252 }
253
254 // Health check
255 if ($method === 'GET' && $path === '/health') {
256 $body = json_encode([
257 'status' => 'ok',
258 'service' => 'wordpress-metasync-mcp',
259 'wordpress_version' => get_bloginfo('version'),
260 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
261 'timestamp' => time()
262 ]);
263
264 $response_headers[] = 'Content-Length: ' . strlen($body);
265 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
266 }
267
268 // MCP endpoint - must be POST
269 if ($method !== 'POST') {
270 $response_headers[0] = 'HTTP/1.1 405 Method Not Allowed';
271 $body = json_encode(['error' => 'Method not allowed']);
272 $response_headers[] = 'Content-Length: ' . strlen($body);
273 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
274 }
275
276 // Check authentication — deny by default when no API key is configured
277 $auth_result = authenticate_bridge_key($headers['x-api-key'] ?? '');
278 if (is_wp_error($auth_result)) {
279 $response_headers[0] = 'HTTP/1.1 401 Unauthorized';
280 $body = json_encode(['error' => $auth_result->get_error_message()]);
281 $response_headers[] = 'Content-Length: ' . strlen($body);
282 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
283 }
284
285 // Extract request body
286 $body_start = strpos($http_request, "\r\n\r\n");
287 $request_body = $body_start !== false ? substr($http_request, $body_start + 4) : '';
288
289 if (empty($request_body)) {
290 $response_headers[0] = 'HTTP/1.1 400 Bad Request';
291 $body = json_encode(['error' => 'Empty request body']);
292 $response_headers[] = 'Content-Length: ' . strlen($body);
293 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
294 }
295
296 $result = process_mcp_request($request_body);
297 if ($result['status'] === 429) {
298 $response_headers[0] = 'HTTP/1.1 429 Too Many Requests';
299 } elseif ($result['status'] >= 400) {
300 $response_headers[0] = 'HTTP/1.1 ' . $result['status'] . ' Error';
301 }
302 foreach ($result['headers'] as $name => $value) {
303 $response_headers[] = $name . ': ' . $value;
304 }
305 $body = $result['body'] === null ? '' : json_encode($result['body']);
306 $response_headers[] = 'Content-Length: ' . strlen($body);
307 return implode("\r\n", $response_headers) . "\r\n\r\n" . $body;
308 }
309
310 /**
311 * Parse HTTP headers
312 */
313 function parse_http_headers($http_request) {
314 $headers = [];
315 $lines = explode("\r\n", $http_request);
316
317 foreach ($lines as $line) {
318 if (strpos($line, ':') !== false) {
319 list($key, $value) = explode(':', $line, 2);
320 $headers[strtolower(trim($key))] = trim($value);
321 }
322 }
323
324 return $headers;
325 }
326
327 /**
328 * Set CORS headers
329 */
330 function set_cors_headers() {
331 $allowed_origins = array();
332 if (function_exists('home_url')) {
333 $allowed_origins[] = home_url();
334 }
335 if (function_exists('site_url')) {
336 $allowed_origins[] = site_url();
337 }
338 $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
339 if (!empty($origin) && in_array($origin, $allowed_origins, true)) {
340 header('Access-Control-Allow-Origin: ' . $origin);
341 }
342 header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
343 header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
344 }
345
346 /**
347 * Check authentication
348 */
349 function check_authentication() {
350 $auth_result = authenticate_bridge_key($_SERVER['HTTP_X_API_KEY'] ?? '');
351 if (is_wp_error($auth_result)) {
352 http_response_code(401);
353 header('Content-Type: application/json');
354 echo json_encode(['error' => $auth_result->get_error_message()]);
355 exit(0);
356 }
357 }
358
359 /**
360 * Handle health check
361 */
362 function handle_health_check() {
363 global $metasync_mcp_server;
364
365 $tools_count = $metasync_mcp_server->get_tool_registry()->get_tool_count();
366
367 http_response_code(200);
368 header('Content-Type: application/json');
369 echo json_encode([
370 'status' => 'ok',
371 'service' => 'wordpress-metasync-mcp',
372 'wordpress_version' => get_bloginfo('version'),
373 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
374 'tools_count' => $tools_count,
375 'timestamp' => time()
376 ]);
377 }
378
379 /**
380 * Authenticate a bridge request through the shared MCP server.
381 *
382 * @param string $provided_key API key supplied by the transport.
383 * @return true|WP_Error
384 */
385 function authenticate_bridge_key($provided_key) {
386 global $metasync_mcp_server;
387
388 return $metasync_mcp_server->authenticate_bridge_request($provided_key);
389 }
390
391 /**
392 * Process a raw JSON-RPC request through the shared MCP server path.
393 *
394 * @param string $request_body Raw JSON-RPC request body.
395 * @return array Processing result with body, status, and headers.
396 */
397 function process_mcp_request($request_body) {
398 global $metasync_mcp_server;
399
400 return $metasync_mcp_server->process_json_rpc_request($request_body);
401 }
402