tag via the same shortcode/block."; // Scope enums that appear across every management tool. Kept in one place so the // list can never drift between tools (all seven scopes the API actually accepts). const SCOPES = ['function', 'backend', 'frontend', 'scheduled', 'persistent', 'content_php', 'content_js']; public function __construct( $core ) { $this->core = $core; // Initialize everything on 'init' to ensure options are loaded add_action( 'init', array( $this, 'init' ), 20 ); } public function init() { global $mwcode, $mwai; $this->api = $mwcode; // Nothing to do without AI Engine's MCP server. if ( !isset( $mwai ) ) { return; } // Two independent surfaces share AI Engine's MCP server, each behind its own // global master switch (and AI Engine's own MCP auth gate upstream): // - the management/internal API (the 'mcp_support' option) // - individual Callable functions, opted-in per snippet via 'functionMcp' and // only exposed when the 'mcp_functions' option is enabled // Either one alone is enough to justify hooking the filters. add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) ); add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 ); } public function register_tools( $tools ) { // Individual Callable functions that opted in to MCP, exposed as first-class tools. // Gated behind a global master switch (Settings > For Developers > MCP Functions) // in addition to each snippet's per-function opt-in. if ( $this->core->get_option( 'mcp_functions', false ) ) { $tools = $this->register_function_tools( $tools ); } // Code Engine's management/internal API (Settings > For Developers > MCP Support). if ( $this->core->get_option( 'mcp_support', false ) ) { $tools = $this->register_management_tools( $tools ); } return $tools; } /** * Map a Code Engine argument type to a JSON Schema type. * Returns null for 'mixed'/unknown so the schema leaves the type open. */ private function mcp_type( $type ) { switch ( $type ) { case 'number': return 'number'; case 'boolean': return 'boolean'; case 'array': return 'array'; case 'object': return 'object'; case 'string': return 'string'; default: return null; } } /** * Return the active Callable (function) snippets that opted in to MCP exposure. */ private function get_mcp_functions() { if ( $this->mcp_functions_cache !== null ) { return $this->mcp_functions_cache; } global $mwcode; if ( !isset( $mwcode ) || !method_exists( $mwcode, 'getSnippets' ) ) { return ( $this->mcp_functions_cache = [] ); } $functions = $mwcode->getSnippets( true, 'function' ); if ( empty( $functions ) ) { return ( $this->mcp_functions_cache = [] ); } return ( $this->mcp_functions_cache = array_values( array_filter( $functions, function ( $fn ) { return !empty( $fn['functionMcp'] ) && !empty( $fn['functionName'] ); } ) ) ); } /** * Register each opted-in Callable function as its own MCP tool, named after the * function, with an input schema derived from its declared arguments. */ public function register_function_tools( $tools ) { foreach ( $this->get_mcp_functions() as $fn ) { $properties = []; $required = []; $argsDict = isset( $fn['functionArgsDict'] ) && is_array( $fn['functionArgsDict'] ) ? $fn['functionArgsDict'] : []; foreach ( $argsDict as $argName => $arg ) { $name = ltrim( $argName, '$' ); if ( $name === '' ) { continue; } $prop = []; $type = $this->mcp_type( $arg['type'] ?? 'string' ); if ( $type !== null ) { $prop['type'] = $type; } if ( !empty( $arg['desc'] ) ) { $prop['description'] = $arg['desc']; } $properties[ $name ] = $prop; if ( !empty( $arg['required'] ) ) { $required[] = $name; } } $schema = [ 'type' => 'object', // Cast so an argument-less function still serializes as {} and not []. 'properties' => (object) $properties, ]; if ( !empty( $required ) ) { $schema['required'] = $required; } $tools[] = [ 'name' => $fn['functionName'], 'description' => !empty( $fn['description'] ) ? $fn['description'] : ( 'Code Engine function: ' . $fn['functionName'] ), 'category' => 'Code Engine (Functions)', 'inputSchema' => $schema, 'annotations' => [ 'openWorldHint' => true ], ]; } return $tools; } public function register_management_tools( $tools ) { // Get Snippet $tools[] = [ 'name' => 'mwcode_get_snippet', 'description' => 'Get a Code Engine snippet by its ID', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ], 'options' => [ 'type' => 'object', 'description' => 'Optional filtering options', 'properties' => [ 'php_ready_args' => [ 'type' => 'boolean', 'description' => 'When true (default), function argument names are returned PHP-ready with a leading $ (e.g. "$id"). Set false to get plain names (e.g. "id").' ] ] ] ], 'required' => ['id'] ] ]; // Get Snippet by Name $tools[] = [ 'name' => 'mwcode_get_snippet_by_name', 'description' => 'Get a Code Engine snippet by its name', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'The snippet name' ], 'options' => [ 'type' => 'object', 'description' => 'Optional filtering options', 'properties' => [ 'php_ready_args' => [ 'type' => 'boolean', 'description' => 'When true (default), function argument names are returned PHP-ready with a leading $ (e.g. "$id"). Set false to get plain names (e.g. "id").' ] ] ] ], 'required' => ['name'] ] ]; // Get Snippets $tools[] = [ 'name' => 'mwcode_get_snippets', 'description' => 'Get all Code Engine snippets, optionally filtered by scope', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'safe' => [ 'type' => 'boolean', 'description' => 'When true (default), skip function snippets whose function name is empty or invalid. Leave true unless you specifically need to inspect malformed snippets.', 'default' => true ], 'scope' => [ 'type' => 'string', 'description' => 'Optional scope filter. ' . self::SCOPE_DESC, 'enum' => self::SCOPES ] ] ] ]; // Execute Snippet $tools[] = [ 'name' => 'mwcode_execute_snippet', 'description' => 'Execute a Code Engine snippet by its ID', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ], 'args' => [ 'type' => 'object', 'description' => 'Arguments to pass to the snippet (key-value pairs)', 'additionalProperties' => true ] ], 'required' => ['id'] ] ]; // Execute Snippet by Name $tools[] = [ 'name' => 'mwcode_execute_snippet_by_name', 'description' => 'Execute a Code Engine snippet by its name', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'The snippet name' ], 'args' => [ 'type' => 'object', 'description' => 'Arguments to pass to the snippet (key-value pairs)', 'additionalProperties' => true ] ], 'required' => ['name'] ] ]; // Create Snippet $tools[] = [ 'name' => 'mwcode_create_snippet', 'description' => 'Create a new Code Engine snippet', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'Name of the snippet' ], 'code' => [ 'type' => 'string', 'description' => 'Code of the snippet' ], 'scope' => [ 'type' => 'string', 'description' => 'Scope of the snippet. ' . self::SCOPE_DESC . ' Defaults to "function".', 'enum' => self::SCOPES, 'default' => 'function' ], 'options' => [ 'type' => 'object', 'description' => 'Additional options for the snippet', 'properties' => [ 'target' => [ 'type' => 'string', 'enum' => ['php', 'js'], 'description' => 'Target language (for function snippets)' ], 'description' => [ 'type' => 'string', 'description' => 'Description of the snippet' ], 'args' => [ 'type' => 'array', 'description' => 'Arguments for function snippets', 'items' => [ 'type' => 'string' ] ], 'argsData' => [ 'type' => 'object', 'description' => 'Argument data for function snippets', 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'type' => [ 'type' => 'string' ], 'description' => [ 'type' => 'string' ], 'default' => [ 'type' => 'string' ] ] ] ], 'behavior' => [ 'type' => 'string', 'enum' => ['dynamic', 'static'], 'description' => 'Behavior for function snippets' ], 'mcp' => [ 'type' => 'boolean', 'description' => 'For function snippets: expose this function as its own MCP tool in AI Engine, named after the function and callable directly by external agents. Defaults to false.' ], 'tags' => [ 'type' => 'array', 'description' => 'Array of tags', 'items' => [ 'type' => 'string' ] ], 'priority' => [ 'type' => 'integer', 'description' => 'Execution priority' ], 'active' => [ 'type' => 'boolean', 'description' => 'Active status' ], 'endpoint' => [ 'type' => 'string', 'description' => 'REST endpoint' ], 'method' => [ 'type' => 'string', 'enum' => ['GET', 'POST'], 'description' => 'HTTP method' ], 'intervalHours' => [ 'type' => 'integer', 'description' => 'Hours for scheduled snippets' ], 'intervalMinutes' => [ 'type' => 'integer', 'description' => 'Minutes for scheduled snippets' ] ] ] ], 'required' => ['name', 'code'] ] ]; // Update Snippet $tools[] = [ 'name' => 'mwcode_update_snippet', 'description' => 'Update an existing Code Engine snippet', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'ID of the snippet to update' ], 'params' => [ 'type' => 'object', 'description' => 'Parameters to update', 'properties' => [ 'name' => [ 'type' => 'string' ], 'code' => [ 'type' => 'string' ], 'scope' => [ 'type' => 'string', 'description' => self::SCOPE_DESC, 'enum' => self::SCOPES ], 'description' => [ 'type' => 'string' ], 'active' => [ 'type' => 'boolean' ], 'priority' => [ 'type' => 'integer' ], 'tags' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'endpoint' => [ 'type' => 'string' ], 'method' => [ 'type' => 'string', 'enum' => ['GET', 'POST'] ], 'target' => [ 'type' => 'string', 'enum' => ['php', 'js'] ], 'args' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'argsData' => [ 'type' => 'object', 'additionalProperties' => true ], 'behavior' => [ 'type' => 'string', 'enum' => ['dynamic', 'static'] ], 'mcp' => [ 'type' => 'boolean', 'description' => 'For function snippets: expose this function as its own MCP tool in AI Engine, named after the function and callable directly by external agents.' ], 'intervalHours' => [ 'type' => 'integer' ], 'intervalMinutes' => [ 'type' => 'integer' ] ] ] ], 'required' => ['id'] ] ]; // Delete Snippet $tools[] = [ 'name' => 'mwcode_delete_snippet', 'description' => 'Delete a Code Engine snippet by its ID', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ] ], 'required' => ['id'] ] ]; // Delete Snippet by Name $tools[] = [ 'name' => 'mwcode_delete_snippet_by_name', 'description' => 'Delete a Code Engine snippet by its name', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'The snippet name' ] ], 'required' => ['name'] ] ]; // Activate Snippet $tools[] = [ 'name' => 'mwcode_activate_snippet', 'description' => 'Activate a Code Engine snippet', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ] ], 'required' => ['id'] ] ]; // Deactivate Snippet $tools[] = [ 'name' => 'mwcode_deactivate_snippet', 'description' => 'Deactivate a Code Engine snippet', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ] ], 'required' => ['id'] ] ]; // Get Snippets by Scope $tools[] = [ 'name' => 'mwcode_get_snippets_by_scope', 'description' => 'Get Code Engine snippets filtered by scope', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'scope' => [ 'type' => 'string', 'description' => 'The scope to filter by. ' . self::SCOPE_DESC, 'enum' => self::SCOPES ], 'filters' => [ 'type' => 'object', 'description' => 'Additional filters', 'properties' => [ 'active' => [ 'type' => 'boolean', 'description' => 'Filter by active status' ], 'tags' => [ 'type' => 'array', 'description' => 'Filter by tags', 'items' => [ 'type' => 'string' ] ] ] ] ], 'required' => ['scope'] ] ]; // Get Active Snippets $tools[] = [ 'name' => 'mwcode_get_active_snippets', 'description' => 'Get all active Code Engine snippets', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'scope' => [ 'type' => 'string', 'description' => 'Optional scope filter. ' . self::SCOPE_DESC, 'enum' => self::SCOPES ] ] ] ]; // Snippet Exists $tools[] = [ 'name' => 'mwcode_snippet_exists', 'description' => 'Check if a Code Engine snippet exists by ID', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The snippet ID' ] ], 'required' => ['id'] ] ]; // Snippet Exists by Name $tools[] = [ 'name' => 'mwcode_snippet_exists_by_name', 'description' => 'Check if a Code Engine snippet exists by name', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'The snippet name' ] ], 'required' => ['name'] ] ]; // Validate Snippet Code $tools[] = [ 'name' => 'mwcode_validate_snippet_code', 'description' => 'Validate snippet code syntax', 'category' => 'Code Engine', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'code' => [ 'type' => 'string', 'description' => 'The snippet code to validate' ], 'target' => [ 'type' => 'string', 'description' => 'The target language', 'enum' => ['php', 'js'], 'default' => 'php' ] ], 'required' => ['code'] ] ]; return $tools; } /** * Execute a Callable function exposed via MCP. Returns $result unchanged when the * tool name does not match one of our opted-in functions, so the filter chain * continues to the management tools (and other plugins). */ private function handle_function_execution( $result, $tool, $args ) { // Master switch: even an opted-in Callable is unreachable via MCP unless the // site has explicitly enabled function exposure. Returning $result unchanged // lets the filter chain fall through to the management tools and other plugins. if ( !$this->core->get_option( 'mcp_functions', false ) ) { return $result; } $match = null; foreach ( $this->get_mcp_functions() as $fn ) { if ( $fn['functionName'] === $tool ) { $match = $fn; break; } } if ( $match === null ) { return $result; } if ( !$this->api ) { return [ 'success' => false, 'error' => 'Code Engine API not initialized' ]; } // getSnippets() rows expose the database id as 'id' (function metadata uses 'snippetId'). $snippetId = $match['id'] ?? $match['snippetId'] ?? null; try { $output = $this->api->executeSnippet( $snippetId, is_array( $args ) ? $args : [] ); return [ 'success' => true, 'data' => $output ]; } // Snippet code is arbitrary PHP: a fatal surfaces as Error/TypeError/ParseError, // none of which are Exceptions. Catch Throwable so a bad snippet can never take // down the MCP request. catch ( \Throwable $e ) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } public function handle_tool_execution( $result, $tool, $args, $id ) { // Individual Callable functions exposed via MCP take priority (named after the function). $handled = $this->handle_function_execution( $result, $tool, $args ); if ( $handled !== $result ) { return $handled; } // Management/internal API tools are gated behind the master switch. if ( !$this->core->get_option( 'mcp_support', false ) ) { return $result; } // Only handle our tools if ( strpos( $tool, 'mwcode_' ) !== 0 ) { return $result; } // Ensure API is initialized if ( !$this->api ) { return [ 'success' => false, 'error' => 'Code Engine API not initialized' ]; } try { switch ( $tool ) { case 'mwcode_get_snippet': $data = $this->api->getSnippet( $args['id'], $args['options'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_get_snippet_by_name': $data = $this->api->getSnippetByName( $args['name'], $args['options'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_get_snippets': $data = $this->api->getSnippets( $args['safe'] ?? true, $args['scope'] ?? null ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_execute_snippet': $data = $this->api->executeSnippet( $args['id'], $args['args'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_execute_snippet_by_name': $data = $this->api->executeSnippetByName( $args['name'], $args['args'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_create_snippet': $data = $this->api->createSnippet( $args['name'], $args['code'], $args['scope'] ?? 'function', $args['options'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_update_snippet': $data = $this->api->updateSnippet( $args['id'], $args['params'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_delete_snippet': $success = $this->api->deleteSnippet( $args['id'] ); return [ 'success' => true, 'data' => [ 'deleted' => $success ] ]; case 'mwcode_delete_snippet_by_name': $success = $this->api->deleteSnippetByName( $args['name'] ); return [ 'success' => true, 'data' => [ 'deleted' => $success ] ]; case 'mwcode_activate_snippet': $success = $this->api->activateSnippet( $args['id'] ); return [ 'success' => true, 'data' => [ 'activated' => $success ] ]; case 'mwcode_deactivate_snippet': $success = $this->api->deactivateSnippet( $args['id'] ); return [ 'success' => true, 'data' => [ 'deactivated' => $success ] ]; case 'mwcode_get_snippets_by_scope': $data = $this->api->getSnippetsByScope( $args['scope'], $args['filters'] ?? [] ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_get_active_snippets': $data = $this->api->getActiveSnippets( $args['scope'] ?? null ); return [ 'success' => true, 'data' => $data ]; case 'mwcode_snippet_exists': $exists = $this->api->snippetExists( $args['id'] ); return [ 'success' => true, 'data' => [ 'exists' => $exists ] ]; case 'mwcode_snippet_exists_by_name': $exists = $this->api->snippetExistsByName( $args['name'] ); return [ 'success' => true, 'data' => [ 'exists' => $exists ] ]; case 'mwcode_validate_snippet_code': $validation = $this->api->validateSnippetCode( $args['code'], $args['target'] ?? 'php' ); return [ 'success' => true, 'data' => $validation ]; } } // executeSnippet() runs arbitrary snippet PHP, whose fatals are Errors, not // Exceptions. Catch Throwable so a broken snippet returns a clean error rather // than crashing the MCP request. catch ( \Throwable $e ) { return [ 'success' => false, 'error' => $e->getMessage() ]; } return $result; } }