#!/usr/bin/env php 'Method not allowed. Use POST for MCP requests.']); exit(0); } // Check authentication if API key is set check_authentication(); // Read request body $request_body = file_get_contents('php://input'); if (empty($request_body)) { http_response_code(400); header('Content-Type: application/json'); echo json_encode(['error' => 'Empty request body']); exit(0); } try { // Parse JSON-RPC request $request = json_decode($request_body, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON: ' . json_last_error_msg()); } // Process MCP request $response = process_mcp_request($request); // Send response http_response_code(200); header('Content-Type: application/json'); echo json_encode($response); } catch (Exception $e) { http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'jsonrpc' => '2.0', 'id' => isset($request['id']) ? $request['id'] : null, 'error' => [ 'code' => -32603, 'message' => $e->getMessage() ] ]); } } /** * Process HTTP request (for standalone server) */ function process_http_request($http_request) { $lines = explode("\r\n", $http_request); $request_line = $lines[0]; $parts = explode(' ', $request_line); $method = $parts[0] ?? 'GET'; $path = $parts[1] ?? '/'; // Parse headers $headers = parse_http_headers($http_request); // Set default headers $response_headers = [ 'HTTP/1.1 200 OK', 'Content-Type: application/json', 'Access-Control-Allow-Origin: *', 'Access-Control-Allow-Methods: GET, POST, OPTIONS', 'Access-Control-Allow-Headers: Content-Type, X-API-Key', ]; // Handle OPTIONS (preflight) if ($method === 'OPTIONS') { $response_headers[0] = 'HTTP/1.1 204 No Content'; return implode("\r\n", $response_headers) . "\r\n\r\n"; } // Health check if ($method === 'GET' && $path === '/health') { $body = json_encode([ 'status' => 'ok', 'service' => 'wordpress-metasync-mcp', 'wordpress_version' => get_bloginfo('version'), 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown', 'timestamp' => time() ]); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } // MCP endpoint - must be POST if ($method !== 'POST') { $response_headers[0] = 'HTTP/1.1 405 Method Not Allowed'; $body = json_encode(['error' => 'Method not allowed']); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } // Check authentication — deny by default when no API key is configured $api_key = getenv('WP_MCP_API_KEY'); if (!$api_key) { $response_headers[0] = 'HTTP/1.1 401 Unauthorized'; $body = json_encode(['error' => 'MCP bridge authentication not configured. Set WP_MCP_API_KEY environment variable.']); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } if (!isset($headers['x-api-key']) || !hash_equals($api_key, $headers['x-api-key'])) { $response_headers[0] = 'HTTP/1.1 401 Unauthorized'; $body = json_encode(['error' => 'Invalid or missing API key']); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } // Extract request body $body_start = strpos($http_request, "\r\n\r\n"); $request_body = $body_start !== false ? substr($http_request, $body_start + 4) : ''; if (empty($request_body)) { $response_headers[0] = 'HTTP/1.1 400 Bad Request'; $body = json_encode(['error' => 'Empty request body']); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } try { // Parse JSON-RPC request $request = json_decode($request_body, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON: ' . json_last_error_msg()); } // Process MCP request $response = process_mcp_request($request); $body = json_encode($response); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } catch (Exception $e) { $response_headers[0] = 'HTTP/1.1 500 Internal Server Error'; $body = json_encode([ 'jsonrpc' => '2.0', 'id' => isset($request['id']) ? $request['id'] : null, 'error' => [ 'code' => -32603, 'message' => $e->getMessage() ] ]); $response_headers[] = 'Content-Length: ' . strlen($body); return implode("\r\n", $response_headers) . "\r\n\r\n" . $body; } } /** * Parse HTTP headers */ function parse_http_headers($http_request) { $headers = []; $lines = explode("\r\n", $http_request); foreach ($lines as $line) { if (strpos($line, ':') !== false) { list($key, $value) = explode(':', $line, 2); $headers[strtolower(trim($key))] = trim($value); } } return $headers; } /** * Set CORS headers */ function set_cors_headers() { $allowed_origins = array(); if (function_exists('home_url')) { $allowed_origins[] = home_url(); } if (function_exists('site_url')) { $allowed_origins[] = site_url(); } $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : ''; if (!empty($origin) && in_array($origin, $allowed_origins, true)) { header('Access-Control-Allow-Origin: ' . $origin); } header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-API-Key'); } /** * Check authentication */ function check_authentication() { $api_key = getenv('WP_MCP_API_KEY'); if (!$api_key) { // Deny by default when no API key is configured http_response_code(401); header('Content-Type: application/json'); echo json_encode(['error' => 'MCP bridge authentication not configured. Set WP_MCP_API_KEY environment variable.']); exit(0); } $provided_key = $_SERVER['HTTP_X_API_KEY'] ?? ''; if (!hash_equals($api_key, $provided_key)) { http_response_code(401); header('Content-Type: application/json'); echo json_encode(['error' => 'Invalid or missing API key']); exit(0); } } /** * Handle health check */ function handle_health_check() { global $metasync_mcp_server; $tools_count = count($metasync_mcp_server->get_tools()); http_response_code(200); header('Content-Type: application/json'); echo json_encode([ 'status' => 'ok', 'service' => 'wordpress-metasync-mcp', 'wordpress_version' => get_bloginfo('version'), 'metasync_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown', 'tools_count' => $tools_count, 'timestamp' => time() ]); } /** * Process MCP JSON-RPC request * * @param array $request JSON-RPC request * @return array JSON-RPC response */ function process_mcp_request($request) { global $metasync_mcp_server; $method = $request['method'] ?? ''; $params = $request['params'] ?? []; $id = $request['id'] ?? null; switch ($method) { case 'initialize': return handle_initialize($id, $params); case 'notifications/initialized': // Client confirms initialization - no response needed for notification return null; case 'tools/list': return handle_tools_list($id); case 'tools/call': return handle_tools_call($id, $params); case 'ping': return [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => [ 'status' => 'ok', 'timestamp' => time() ] ]; default: throw new Exception("Unknown method: $method"); } } /** * Handle initialize request */ function handle_initialize($id, $params) { $client_info = $params['clientInfo'] ?? []; return [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => [ 'protocolVersion' => '2024-11-05', 'capabilities' => [ 'tools' => (object)[] ], 'serverInfo' => [ 'name' => 'wordpress-metasync', 'version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '2.0.0' ] ] ]; } /** * Handle tools/list request */ function handle_tools_list($id) { global $metasync_mcp_server; $tools = []; $tool_objects = $metasync_mcp_server->get_tools(); foreach ($tool_objects as $tool) { $tools[] = [ 'name' => $tool->get_name(), 'description' => $tool->get_description(), 'inputSchema' => $tool->get_input_schema() ]; } return [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => [ 'tools' => $tools ] ]; } /** * Handle tools/call request */ function handle_tools_call($id, $params) { global $metasync_mcp_server; $tool_name = $params['name'] ?? ''; $arguments = $params['arguments'] ?? []; if (empty($tool_name)) { throw new Exception('Tool name is required'); } // Find tool $tool = $metasync_mcp_server->get_tool($tool_name); if (!$tool) { throw new Exception("Tool not found: $tool_name"); } // Execute tool $result = $tool->execute($arguments); // Format result as MCP response $content = []; if (is_array($result) || is_object($result)) { $content[] = [ 'type' => 'text', 'text' => json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ]; } else { $content[] = [ 'type' => 'text', 'text' => (string)$result ]; } return [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => [ 'content' => $content, 'isError' => false ] ]; }