Current File : /home/kelaby89/cartel.express/wp-content/plugins/ai-engine/labs/mcp.php |
<?php
/**
* AI Engine MCP Server
*
* This class implements a Model Context Protocol (MCP) server for AI Engine.
*
* Current Implementation:
* - Works reliably with Claude App through the mcp.js relay
* - The mcp.js relay handles proper disconnection, mwai/kill signals, and other AI Engine-specific features
* - OAuth authentication flow is currently disabled due to security concerns
* (only static bearer tokens are supported)
* - Works with OpenAI/ChatGPT, but OpenAI limits MCP to only 'search' and 'fetch' tools for their Deep Research feature
* (these tools are provided by AI Engine's Tuned Core module and search through WordPress posts/pages)
*
* Direct Connection Challenges:
* The goal is to support direct connections from Claude.ai and OpenAI to this MCP server without
* requiring the mcp.js relay. However, this is challenging due to:
* - PHP's blocking nature causing threads to freeze during long-running SSE connections
* - Difficulty in properly handling connection termination and cleanup
* - Protocol version differences between clients (e.g., Claude.ai uses 2024-11-05)
* - Multiple rapid reconnection attempts from AI services overwhelming the PHP server
*
* OpenAI Limitations:
* - OpenAI's MCP implementation is limited to Deep Research functionality only
* - Only 'search' and 'fetch' tools are supported (no other WordPress management tools)
* - This significantly limits the MCP capabilities compared to Claude's full implementation
*
* The mcp.js relay remains the recommended approach for production use until these
* direct connection issues are resolved.
*/
class Meow_MWAI_Labs_MCP {
private $core = null;
private $namespace = 'mcp/v1';
private $server_version = '0.0.1';
private $protocol_version = '2024-11-05';
private $queue_key = 'mwai_mcp_msg';
private $session_id = null;
private $logging = false;
private $last_action_time = 0;
private $bearer_token = null;
// Placeholder for OAuth integration. Currently unused and kept for
// future implementation once the security model is revised.
private $oauth = null;
#region Initialize
public function __construct( $core ) {
$this->core = $core;
// Set logging based on option
$this->logging = $this->core->get_option( 'mcp_debug_mode', false );
// OAuth support is temporarily disabled due to security concerns.
// The previous implementation allowed unvalidated redirect URIs which
// introduced an open redirect vulnerability and the possibility to
// steal authorization codes. Until proper client registration with
// strict redirect URI validation is implemented, the OAuth feature is
// not loaded. See labs/oauth.php for the previous code and take care
// when re‑enabling it in the future.
add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
// Log MCP-related requests when logging is enabled
if ( $this->logging ) {
add_action( 'init', [ $this, 'log_requests' ], 1 );
}
}
public function log_requests() {
if ( !$this->logging || empty( $_SERVER['REQUEST_METHOD'] ) || empty( $_SERVER['REQUEST_URI'] ) ) {
return;
}
$uri = $_SERVER['REQUEST_URI'];
// Only log MCP-related requests
if ( strpos( $uri, '/mcp/' ) === false && strpos( $uri, '/mwai/' ) === false && strpos( $uri, '/.well-known/oauth' ) === false ) {
return;
}
// Skip patterns we don't want to log
$skip_patterns = [
'/wp-admin/',
'/wp-cron.php',
'/favicon.ico',
];
foreach ( $skip_patterns as $pattern ) {
if ( strpos( $uri, $pattern ) !== false ) {
return;
}
}
// Get user agent (shortened)
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'Unknown';
if ( strpos( $user_agent, 'Mozilla' ) !== false ) {
$user_agent = 'Mozilla/5.0';
}
elseif ( strpos( $user_agent, 'python-httpx' ) !== false ) {
$user_agent = 'python-httpx/0.27.0';
}
elseif ( strpos( $user_agent, 'node' ) !== false ) {
$user_agent = 'node';
}
// Get IP address (considering proxies)
$ip = 'Unknown';
if ( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
elseif ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
// X-Forwarded-For can contain multiple IPs, get the first one
$ips = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
$ip = trim( $ips[0] );
}
elseif ( !empty( $_SERVER['REMOTE_ADDR'] ) ) {
$ip = $_SERVER['REMOTE_ADDR'];
}
// Simplify URI for readability
$uri_parts = parse_url( $_SERVER['REQUEST_URI'] );
$path = $uri_parts['path'] ?? $_SERVER['REQUEST_URI'];
$simple_path = str_replace( '/wp-json', '', $path );
// Only show session ID for /messages requests
if ( strpos( $path, '/messages' ) !== false && !empty( $uri_parts['query'] ) ) {
parse_str( $uri_parts['query'], $query_params );
if ( isset( $query_params['session_id'] ) ) {
$simple_path .= ' (' . substr( $query_params['session_id'], 0, 8 ) . '...)';
}
}
// Uncomment the line below to see ALL HTTP requests in logs (useful when debugging Claude, ChatGPT, etc)
// This shows every request made by AI services to understand their connection patterns
// error_log( '[AI Engine MCP] ' . $_SERVER['REQUEST_METHOD'] . ' ' . $simple_path );
}
public function is_logging_enabled() {
return $this->logging;
}
public function rest_api_init() {
// Load bearer token if not already loaded
if ( $this->bearer_token === null ) {
$this->bearer_token = $this->core->get_option( 'mcp_bearer_token' );
}
// Only add filter once
static $filter_added = false;
if ( !empty( $this->bearer_token ) && !$filter_added ) {
add_filter( 'mwai_allow_mcp', [ $this, 'auth_via_bearer_token' ], 10, 2 );
$filter_added = true;
}
register_rest_route( $this->namespace, '/sse', [
'methods' => 'GET',
'callback' => [ $this, 'handle_sse' ],
'permission_callback' => function ( $request ) {
return $this->can_access_mcp( $request );
},
] );
register_rest_route( $this->namespace, '/sse', [
'methods' => 'POST',
'callback' => [ $this, 'handle_sse' ],
'permission_callback' => function ( $request ) {
return $this->can_access_mcp( $request );
},
] );
register_rest_route( $this->namespace, '/messages', [
'methods' => 'POST',
'callback' => [ $this, 'handle_message' ],
'permission_callback' => function ( $request ) {
return $this->can_access_mcp( $request );
},
] );
// No-Auth URL endpoints (with token in path)
$noauth_enabled = $this->core->get_option( 'mcp_noauth_url' );
if ( $noauth_enabled && !empty( $this->bearer_token ) ) {
register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
'methods' => 'GET',
'callback' => [ $this, 'handle_sse' ],
'permission_callback' => function ( $request ) {
return $this->handle_noauth_access( $request );
},
] );
register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
'methods' => 'POST',
'callback' => [ $this, 'handle_sse' ],
'permission_callback' => function ( $request ) {
return $this->handle_noauth_access( $request );
},
] );
register_rest_route( $this->namespace, '/' . $this->bearer_token . '/messages', [
'methods' => 'POST',
'callback' => [ $this, 'handle_message' ],
'permission_callback' => function ( $request ) {
return $this->handle_noauth_access( $request );
},
] );
}
}
#endregion
#region Auth (Bearer token)
/**
* SECURITY: MCP provides powerful WordPress management capabilities, so access must be strictly controlled.
*
* By default, only administrators can access MCP endpoints. This prevents lower-privileged users
* (subscribers, contributors, etc.) from executing dangerous operations like creating admin users,
* deleting content, or modifying settings.
*
* When a bearer token is configured, it overrides the default admin check, but access is DENIED
* unless a valid token is provided. This ensures MCP is secure even with default settings.
*/
public function can_access_mcp( $request ) {
// Default to requiring administrator capability for security
$is_admin = current_user_can( 'administrator' );
return apply_filters( 'mwai_allow_mcp', $is_admin, $request );
}
public function auth_via_bearer_token( $allow, $request ) {
// Skip if already authenticated as admin
if ( $allow ) {
return $allow;
}
$hdr = $request->get_header( 'authorization' );
// If no authorization header but bearer token is configured, deny access
if ( !$hdr && !empty( $this->bearer_token ) ) {
if ( $this->logging ) {
error_log( '[AI Engine MCP] ❌ No authorization header provided.' );
}
return false;
}
// Check for Bearer token in header
if ( $hdr && preg_match( '/Bearer\s+(.+)/i', $hdr, $m ) ) {
$token = trim( $m[1] );
$auth_result = 'none';
// Check if it's an OAuth token
if ( $this->oauth ) {
$token_data = $this->oauth->validate_token( $token );
if ( $token_data ) {
// Set current user based on OAuth token
wp_set_current_user( $token_data['user_id'] );
$auth_result = 'oauth';
// Only log auth for SSE endpoint
if ( $this->logging && strpos( $request->get_route(), '/sse' ) !== false ) {
error_log( '[AI Engine MCP] 🔐 OAuth OK (user: ' . $token_data['user_id'] . ')' );
}
return true;
}
}
// Fall back to static bearer token if configured
if ( !empty( $this->bearer_token ) && hash_equals( $this->bearer_token, $token ) ) {
if ( $admin = $this->core->get_admin_user() ) {
wp_set_current_user( $admin->ID, $admin->user_login );
}
$auth_result = 'static';
// Only log auth for SSE endpoint
if ( $this->logging && strpos( $request->get_route(), '/sse' ) !== false ) {
error_log( '[AI Engine MCP] 🔐 Auth OK' );
}
return true;
}
if ( $this->logging && $auth_result === 'none' ) {
error_log( '[AI Engine MCP] ❌ Bearer token invalid.' );
}
// Explicitly deny access for invalid tokens
return false;
}
// ?token=xyz fallback (optional) - only for static bearer token
if ( !empty( $this->bearer_token ) ) {
$q = sanitize_text_field( $request->get_param( 'token' ) );
if ( $q && hash_equals( $this->bearer_token, $q ) ) {
if ( $admin = $this->core->get_admin_user() ) {
wp_set_current_user( $admin->ID, $admin->user_login );
}
return true;
}
}
// If bearer token is configured but no valid auth provided, deny access
if ( !empty( $this->bearer_token ) ) {
return false;
}
return $allow;
}
public function handle_noauth_access( $request ) {
// For no-auth URLs, the token is already verified by being in the URL path
// Double-check that the route actually contains the token
$route = $request->get_route();
if ( strpos( $route, '/' . $this->bearer_token . '/' ) === false ) {
if ( $this->logging ) {
error_log( '[AI Engine MCP] ❌ Invalid no-auth URL access attempt.' );
}
return false;
}
// Set the current user to admin since token is valid
if ( $admin = $this->core->get_admin_user() ) {
wp_set_current_user( $admin->ID, $admin->user_login );
}
return true;
}
#endregion
#region Helpers (log / JSON-RPC utils)
private function log( $msg ) {
// This method is for internal UI logs - keep it minimal
if ( $this->logging ) {
// Only log important messages to UI
if ( strpos( $msg, 'queued' ) === false && strpos( $msg, 'flush' ) === false ) {
Meow_MWAI_Logging::log( "[AI Engine MCP] {$msg}" );
}
}
}
/** Wrap a JSON-RPC error object */
private function rpc_error( $id, int $code, string $msg, $extra = null ): array {
$err = [ 'code' => $code, 'message' => $msg ];
if ( $extra !== null ) {
$err['data'] = $extra;
}
return [ 'jsonrpc' => '2.0', 'id' => $id, 'error' => $err ];
}
/** Queue an error for SSE delivery */
private function queue_error( $sess, $id, int $code, string $msg, $extra = null ): void {
$this->store_message( $sess, $this->rpc_error( $id, $code, $msg, $extra ) );
}
/** Format tool result for MCP protocol */
private function format_tool_result( $result ): array {
// If result is a string, wrap it in the MCP content format
if ( is_string( $result ) ) {
return [
'content' => [
[
'type' => 'text',
'text' => $result,
],
],
];
}
// If result has 'content' key, assume it's already properly formatted
if ( is_array( $result ) && isset( $result['content'] ) ) {
return $result;
}
// If result is an array without 'content' key, wrap it as JSON
if ( is_array( $result ) ) {
return [
'content' => [
[
'type' => 'text',
'text' => wp_json_encode( $result, JSON_PRETTY_PRINT ),
],
],
'data' => $result,
];
}
// For any other type, convert to string and wrap
return [
'content' => [
[
'type' => 'text',
'text' => (string) $result,
],
],
];
}
#endregion
#region Handle direct JSON-RPC (for Claude's MCP client)
/**
* Claude's MCP client (via Anthropic API) sends JSON-RPC requests directly to the SSE endpoint
* as POST requests, rather than following the typical SSE flow:
* - Normal flow: GET /sse → establish SSE stream → POST /messages for JSON-RPC
* - Claude's flow: POST /sse with JSON-RPC body → expect immediate JSON response
*
* This method handles the direct JSON-RPC requests to maintain compatibility with Claude.
*/
private function handle_direct_jsonrpc( WP_REST_Request $request, $data ) {
$id = $data['id'] ?? null;
$method = $data['method'] ?? null;
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_REST_Response( [
'jsonrpc' => '2.0',
'id' => null,
'error' => [ 'code' => -32700, 'message' => 'Parse error: invalid JSON' ]
], 200 );
}
if ( !is_array( $data ) || !$method ) {
return new WP_REST_Response( [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [ 'code' => -32600, 'message' => 'Invalid Request' ]
], 200 );
}
try {
$reply = null;
switch ( $method ) {
case 'initialize':
// Check if client requests a specific protocol version
$params = $data['params'] ?? [];
$requested_version = $params['protocolVersion'] ?? null;
$client_info = $params['clientInfo'] ?? null;
if ( $this->logging && $client_info ) {
$client_name = $client_info['name'] ?? 'unknown';
$client_version = $client_info['version'] ?? 'unknown';
error_log( "[AI Engine MCP] Client: {$client_name} v{$client_version}" );
}
if ( $requested_version && $requested_version !== $this->protocol_version ) {
if ( $this->logging ) {
Meow_MWAI_Logging::warn( "[AI Engine MCP] Client requested protocol version {$requested_version}, but we only support {$this->protocol_version}" );
}
}
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'protocolVersion' => $this->protocol_version,
'serverInfo' => (object) [
'name' => get_bloginfo( 'name' ) . ' MCP',
'version' => $this->server_version,
],
'capabilities' => [
'tools' => [ 'listChanged' => true ],
'prompts' => [ 'subscribe' => false, 'listChanged' => false ],
'resources' => [ 'subscribe' => false, 'listChanged' => false ],
],
],
];
break;
case 'tools/list':
// Don't log every tools/list request as it's too repetitive
// Check if this is OpenAI by checking the User-Agent
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
$is_openai = strpos( $user_agent, 'openai-mcp' ) !== false;
if ( $is_openai && $this->logging ) {
error_log( '[AI Engine MCP] 🎯 OpenAI client detected - filtering tools for deep research only.' );
}
$tools = $this->get_tools_list();
// Filter tools for OpenAI - they only support search and fetch for deep research
if ( $is_openai ) {
$filtered_tools = [];
foreach ( $tools as $tool ) {
if ( in_array( $tool['name'], ['search', 'fetch'] ) ) {
$filtered_tools[] = $tool;
}
}
$tools = $filtered_tools;
if ( $this->logging && count( $filtered_tools ) === 0 ) {
error_log( '[AI Engine MCP] ⚠️ Warning: No search or fetch tools found for OpenAI!' );
}
}
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [ 'tools' => $tools ],
];
if ( $this->logging ) {
error_log( '[AI Engine MCP] 📤 Returning ' . count( $tools ) . ' tools.' );
}
break;
case 'tools/call':
$params = $data['params'] ?? [];
$tool = $params['name'] ?? '';
$arguments = $params['arguments'] ?? [];
$reply = $this->execute_tool( $tool, $arguments, $id );
break;
case 'notifications/initialized':
// This is a notification from the client indicating it has initialized
// No response needed for notifications
// Client initialized - no need to log
return new WP_REST_Response( null, 204 );
break;
default:
// Check if it's a notification (no id)
if ( $id === null && strpos( $method, 'notifications/' ) === 0 ) {
if ( $this->logging ) {
error_log( '[AI Engine MCP] 📨 Notification received: ' . $method );
}
return new WP_REST_Response( null, 204 );
}
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [ 'code' => -32601, 'message' => "Method not found: {$method}" ]
];
}
// Ensure proper JSON-RPC response
$response = new WP_REST_Response( $reply, 200 );
$response->set_headers( [ 'Content-Type' => 'application/json' ] );
return $response;
}
catch ( Exception $e ) {
$error_response = new WP_REST_Response( [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [ 'code' => -32603, 'message' => 'Internal error', 'data' => $e->getMessage() ]
], 200 );
$error_response->set_headers( [ 'Content-Type' => 'application/json' ] );
return $error_response;
}
}
#endregion
#region Handle SSE (stream loop)
private function reply( string $event, $data = null, string $enc = 'json' ) {
// Handle special events
if ( $event === 'bye' ) {
echo "event: bye\ndata: \n\n";
if ( ob_get_level() ) {
ob_end_flush();
}
flush();
$this->last_action_time = time();
$this->log( 'Clean disconnection' );
return;
}
if ( $enc === 'json' && $data === null ) {
$this->log( "no data for {$event}" );
return;
}
echo "event: {$event}\n";
if ( $enc === 'json' ) {
$data = $data === null ? '{}' : wp_json_encode( $data, JSON_UNESCAPED_UNICODE );
}
echo 'data: ' . $data . "\n\n";
if ( ob_get_level() ) {
ob_end_flush();
}
flush();
$this->last_action_time = time();
// Only log endpoint announcements
if ( $event === 'endpoint' ) {
$this->log( 'SSE endpoint ready' );
}
}
private function generate_sse_id( $req ) {
$last = $req ? $req->get_header( 'last-event-id' ) : '';
return $last ?: str_replace( '-', '', wp_generate_uuid4() );
}
public function handle_sse( WP_REST_Request $request ) {
$raw_body = $request->get_body();
// Handle POST request with JSON-RPC body (Direct MCP client behavior)
// Both Claude.ai and OpenAI/ChatGPT send JSON-RPC requests directly to the SSE endpoint
// instead of establishing an SSE connection first. This is non-standard but we need to support it.
// Expected flow: GET /sse (establish stream) → POST /messages (send JSON-RPC)
// Actual flow: POST /sse with JSON-RPC body → expects immediate JSON response
if ( $request->get_method() === 'POST' && !empty( $raw_body ) ) {
$data = json_decode( $raw_body, true );
if ( $data && isset( $data['method'] ) ) {
// Don't log here - it's already logged by log_requests()
// Process as a direct JSON-RPC request instead of starting SSE stream
return $this->handle_direct_jsonrpc( $request, $data );
}
}
@ini_set( 'zlib.output_compression', '0' );
@ini_set( 'output_buffering', '0' );
@ini_set( 'implicit_flush', '1' );
if ( function_exists( 'ob_implicit_flush' ) ) {
ob_implicit_flush( true );
}
header( 'Content-Type: text/event-stream' );
header( 'Cache-Control: no-cache' );
header( 'X-Accel-Buffering: no' );
header( 'Connection: keep-alive' );
while ( ob_get_level() ) {
ob_end_flush();
}
/* — greet client —*/
$this->session_id = $this->generate_sse_id( $request );
$this->last_action_time = time();
echo "id: {$this->session_id}\n\n";
flush();
$msg_uri = sprintf(
'%s/messages?session_id=%s',
rest_url( $this->namespace ),
$this->session_id
);
$this->reply( 'endpoint', $msg_uri, 'text' );
if ( $this->logging ) {
error_log( '[AI Engine MCP] ✅ SSE connected (' . substr( $this->session_id, 0, 8 ) . '...)' );
}
/* — main loop —*/
while ( true ) {
// Use shorter timeout in debug mode for easier testing
$max_time = $this->logging ? 30 : 60 * 5; // 30 seconds in debug, 5 minutes in production
$idle = ( time() - $this->last_action_time ) >= $max_time;
if ( connection_aborted() || $idle ) {
$this->reply( 'bye' );
if ( $this->logging ) {
error_log( '[AI Engine MCP] 🔚 SSE closed (' . ( $idle ? 'idle' : 'abort' ) . ')' );
}
break;
}
foreach ( $this->fetch_messages( $this->session_id ) as $p ) {
// Check for kill signal in the message queue
if ( isset( $p['method'] ) && $p['method'] === 'mwai/kill' ) {
if ( $this->logging ) {
error_log( '[AI Engine MCP] Kill signal - terminating' );
}
$this->reply( 'bye' );
exit;
}
// Don't log SSE responses - they clutter the logs
$this->reply( 'message', $p );
}
usleep( 200000 ); // 200 ms
}
exit;
}
#endregion
#region Handle /messages (JSON-RPC ingress)
public function handle_message( WP_REST_Request $request ) {
$sess = sanitize_text_field( $request->get_param( 'session_id' ) );
$raw = $request->get_body();
$dat = json_decode( $raw, true );
// Only log important methods in detail
if ( $this->logging && $dat && isset( $dat['method'] ) ) {
$method = $dat['method'];
// Skip logging for repetitive/less important notifications
if ( !in_array( $method, ['notifications/initialized', 'notifications/cancelled'] ) ) {
error_log( '[AI Engine MCP] ↓ ' . $method );
}
}
if ( json_last_error() !== JSON_ERROR_NONE ) {
$this->queue_error( $sess, null, -32700, 'Parse error: invalid JSON' );
return new WP_REST_Response( null, 204 );
}
if ( !is_array( $dat ) ) {
$this->queue_error( $sess, null, -32600, 'Invalid Request' );
return new WP_REST_Response( null, 204 );
}
$id = $dat['id'] ?? null;
$method = $dat['method'] ?? null;
/* — notifications —*/
if ( $method === 'initialized' ) {
return new WP_REST_Response( null, 204 );
}
if ( $method === 'mwai/kill' ) {
// Kill signal received - no need for verbose logging
// Queue the kill message for SSE to pick up before exiting
$this->store_message( $sess, [
'jsonrpc' => '2.0',
'method' => 'mwai/kill'
] );
// Give it a moment to be stored
usleep( 100000 ); // 100ms
return new WP_REST_Response( null, 204 );
}
// It's a notification, no ID = no reply
if ( $id === null && $method !== null ) {
return new WP_REST_Response( null, 204 );
}
if ( !$method ) {
$this->queue_error( $sess, $id, -32600, 'Invalid Request: method missing' );
return new WP_REST_Response( null, 204 );
}
try {
$reply = null;
#region Methods switch
switch ( $method ) {
case 'initialize':
// Check if client requests a specific protocol version
$params = $dat['params'] ?? [];
$requested_version = $params['protocolVersion'] ?? null;
$client_info = $params['clientInfo'] ?? null;
if ( $this->logging && $client_info ) {
$client_name = $client_info['name'] ?? 'unknown';
$client_version = $client_info['version'] ?? 'unknown';
error_log( "[AI Engine MCP] Client: {$client_name} v{$client_version}" );
}
if ( $requested_version && $requested_version !== $this->protocol_version ) {
if ( $this->logging ) {
Meow_MWAI_Logging::warn( "[AI Engine MCP] Client requested protocol version {$requested_version}, but we only support {$this->protocol_version}" );
}
}
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'protocolVersion' => $this->protocol_version,
'serverInfo' => (object) [
'name' => get_bloginfo( 'name' ) . ' MCP',
'version' => $this->server_version,
],
'capabilities' => [
'tools' => [ 'listChanged' => true ],
'prompts' => [ 'subscribe' => false, 'listChanged' => false ],
'resources' => [ 'subscribe' => false, 'listChanged' => false ],
],
],
];
break;
case 'tools/list':
// Don't log every tools/list request as it's too repetitive
// Check if this is OpenAI by checking the User-Agent
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
$is_openai = strpos( $user_agent, 'openai-mcp' ) !== false;
if ( $is_openai && $this->logging ) {
error_log( '[AI Engine MCP] 🎯 OpenAI client detected - filtering tools for deep research only.' );
}
$tools = $this->get_tools_list();
// Filter tools for OpenAI - they only support search and fetch for deep research
if ( $is_openai ) {
$filtered_tools = [];
foreach ( $tools as $tool ) {
if ( in_array( $tool['name'], ['search', 'fetch'] ) ) {
$filtered_tools[] = $tool;
}
}
$tools = $filtered_tools;
if ( $this->logging && count( $filtered_tools ) === 0 ) {
error_log( '[AI Engine MCP] ⚠️ Warning: No search or fetch tools found for OpenAI!' );
}
}
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [ 'tools' => $tools ],
];
break;
case 'resources/list':
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [ 'resources' => $this->get_resources_list() ],
];
break;
case 'prompts/list':
$reply = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [ 'prompts' => $this->get_prompts_list() ],
];
break;
case 'tools/call':
$params = $dat['params'] ?? [];
$tool = $params['name'] ?? '';
$arguments = $params['arguments'] ?? [];
$reply = $this->execute_tool( $tool, $arguments, $id );
break;
default:
$reply = $this->rpc_error( $id, -32601, "Method not found: {$method}" );
}
#endregion
if ( $reply ) {
// Don't log response queuing - it's too noisy
$this->store_message( $sess, $reply );
}
}
catch ( Exception $e ) {
$this->queue_error( $sess, $id, -32603, 'Internal error', $e->getMessage() );
}
return new WP_REST_Response( null, 204 );
}
#endregion
#region Tools Definitions
private function get_tools_list() {
$base_tools = [
[
'name' => 'mcp_ping',
'description' => 'Simple connectivity check. Returns the current GMT time and the WordPress site name. Whenever a tool call fails (error or timeout), immediately invoke mcp_ping to verify the server; if mcp_ping itself does not respond, assume the server is temporarily unreachable and pause additional tool calls.',
'inputSchema' => [
'type' => 'object',
'properties' => (object) [],
'required' => []
],
],
];
return apply_filters( 'mwai_mcp_tools', $base_tools );
}
#endregion
#region Resources Definitions
private function get_resources_list() {
return [];
}
#endregion
#region Prompts Definitions
private function get_prompts_list() {
return [];
}
#endregion
#region Tools Call (execute_tool)
private function execute_tool( $tool, $args, $id ) {
try {
// Handle built-in tools first
if ( $tool === 'mcp_ping' ) {
if ( $this->logging ) {
$this->log( '🛠️ Tool: mcp_ping' );
}
$ping_data = [
'time' => gmdate( 'Y-m-d H:i:s' ),
'name' => get_bloginfo( 'name' ),
];
return [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
[
'type' => 'text',
'text' => 'Ping successful: ' . wp_json_encode( $ping_data, JSON_PRETTY_PRINT ),
],
],
'data' => $ping_data,
],
];
}
// Let other modules handle their tools
if ( $this->logging ) {
// Log tool calls with more context
$args_preview = '';
if ( !empty( $args ) ) {
// Show key args for common tools
if ( isset( $args['ID'] ) ) {
$args_preview = ' (ID: ' . $args['ID'] . ')';
}
elseif ( isset( $args['query'] ) ) {
$args_preview = ' (query: "' . substr( $args['query'], 0, 30 ) . '...")';
}
elseif ( isset( $args['message'] ) ) {
$args_preview = ' (message: "' . substr( $args['message'], 0, 30 ) . '...")';
}
}
// Log to both error log and UI
error_log( '[AI Engine MCP] 🛠️ ' . $tool . $args_preview );
$this->log( '🛠️ Tool: ' . $tool . $args_preview );
}
$filtered = apply_filters( 'mwai_mcp_callback', null, $tool, $args, $id, $this );
if ( $filtered !== null ) {
// Check if it's already a full JSON-RPC response (backward compatibility)
if ( is_array( $filtered ) && isset( $filtered['jsonrpc'] ) && isset( $filtered['id'] ) ) {
return $filtered;
}
// Otherwise, wrap the result in proper JSON-RPC format
return [
'jsonrpc' => '2.0',
'id' => $id,
'result' => $this->format_tool_result( $filtered ),
];
}
throw new Exception( "Unknown tool: {$tool}" );
}
catch ( Exception $e ) {
return $this->rpc_error( $id, -32603, $e->getMessage() );
}
}
#endregion
#region Message Queue (per-message transient)
private function transient_key( $sess, $id ) {
return "{$this->queue_key}_{$sess}_{$id}";
}
private function store_message( $sess, $payload ) {
if ( !$sess ) {
return;
}
$idKey = array_key_exists( 'id', $payload ) ? ( $payload['id'] ?? 'NULL' ) : 'N/A';
set_transient( $this->transient_key( $sess, $idKey ), $payload, 30 );
$this->log( "queued #{$idKey}" );
}
private function fetch_messages( $sess ) {
global $wpdb;
$like = $wpdb->esc_like( '_transient_' . "{$this->queue_key}_{$sess}_" ) . '%';
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
$like
),
ARRAY_A
);
$msgs = [];
foreach ( $rows as $r ) {
$msgs[] = maybe_unserialize( $r['option_value'] );
delete_option( $r['option_name'] );
}
usort( $msgs, fn ( $a, $b ) => ( $a['id'] ?? 0 ) <=> ( $b['id'] ?? 0 ) );
if ( $msgs ) {
$this->log( 'flush ' . count( $msgs ) . ' msg(s)' );
}
return $msgs;
}
#endregion
#region Resources (note)
/*--------------------------------------------------*/
/**
* MCP also supports “resources” – static or dynamic data a client can
* retrieve by URL (e.g. `mcp://resource/posts/123`).
*/
#endregion
}