array( 'name' => 'miintg_selftest', 'description' => 'Self-test for the Quick Integrity Scanner plugin. Walks a WordPress directory and returns file counts, timing and a small metadata sample. Read-only; returns no file contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'target' => array( 'type' => 'string', 'enum' => array( 'wp-includes', 'wp-admin', 'wp-content-root', 'mu-plugins' ), 'description' => 'Directory to walk. Defaults to wp-includes.', ), 'sample' => array( 'type' => 'integer', 'description' => 'Number of sample file entries to return (0-25, default 5).', ), ), 'required' => array(), ), ), 'wp_core_verify_checksums' => array( 'name' => 'wp_core_verify_checksums', 'description' => 'Compares wp-admin/, wp-includes/, and checksummed root files against the official wordpress.org manifest for the installed WP version. Reports modified, missing, and unexpected files. wp-content is out of scope. Read-only; returns no file contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'locale' => array( 'type' => 'string', 'description' => 'WP locale to check against, e.g. en_US. Defaults to the site locale.', ), ), 'required' => array(), ), ), 'wp_list_autoloaded_code' => array( 'name' => 'wp_list_autoloaded_code', 'description' => 'Inventories mu-plugins and WordPress drop-ins (object-cache.php, advanced-cache.php, db.php, maintenance.php, sunrise.php) in wp-content. Both autoload with no admin-UI listing. Hashes only, no comparison manifest exists for either. Read-only; returns no file contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => (object) array(), 'required' => array(), ), ), 'wp_scan_uploads_anomalies' => array( 'name' => 'wp_scan_uploads_anomalies', 'description' => 'Scans wp-content/uploads for backdoor indicators: executable/double extensions, PHP tags in non-PHP files, .htaccess presence, files orphaned from the Media Library, and boolean obfuscation-pattern signature flags (eval, base64_decode, gzinflate, str_rot13, assert, create_function). Read-only; returns no file contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => (object) array(), 'required' => array(), ), ), 'wp_verify_plugin_checksums' => array( 'name' => 'wp_verify_plugin_checksums', 'description' => 'Compares installed plugin files against WordPress.org per-slug checksum manifests. Plugins only — no equivalent manifest API exists for themes. Non-.org/premium plugins are reported with manifest_available:false. Read-only; returns no file contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'plugins' => array( 'type' => 'string', 'description' => 'Optional comma-separated plugin folder slugs to limit the scan to. Omit to scan every installed plugin.', ), 'max_plugins' => array( 'type' => 'integer', 'description' => 'Safety cap on plugins scanned in one call. Default 50.', ), ), 'required' => array(), ), ), 'wp_check_config_integrity' => array( 'name' => 'wp_check_config_integrity', 'description' => 'wp-config.php and .htaccess: md5/size/mtime snapshot only (no manifest exists for either, so this is not a pass/fail comparison). Also reports DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS, and WP_DEBUG from PHP\'s already-evaluated runtime constants. Read-only; never reads or returns either file\'s contents.', 'inputSchema' => array( 'type' => 'object', 'properties' => (object) array(), 'required' => array(), ), ), 'wp_integrity_scan' => array( 'name' => 'wp_integrity_scan', 'description' => 'Wrapper: runs wp_core_verify_checksums, wp_list_autoloaded_code, wp_scan_uploads_anomalies, wp_verify_plugin_checksums, wp_check_config_integrity, and wp_audit_privileged_users in one call and sums their severities. Use for a full-site sweep instead of calling all six separately.', 'inputSchema' => array( 'type' => 'object', 'properties' => array( 'checks' => array( 'type' => 'string', 'description' => 'Optional comma-separated subset: core,autoloaded,uploads,plugins,config,users. Omit to run all six.', ), 'plugins' => array( 'type' => 'string', 'description' => 'Passed through to wp_verify_plugin_checksums.', ), 'max_plugins' => array( 'type' => 'integer', 'description' => 'Passed through to wp_verify_plugin_checksums.', ), 'locale' => array( 'type' => 'string', 'description' => 'Passed through to wp_core_verify_checksums.', ), ), 'required' => array(), ), ), 'wp_audit_privileged_users' => array( 'name' => 'wp_audit_privileged_users', 'description' => 'Reads the capabilities usermeta directly via $wpdb rather than get_users(), which a backdoor can hook (pre_user_query) to hide an account from admin listings. Flags administrators visible only in the direct query, and capability keys granted to a user outside any role. Read-only; no passwords, emails, or other personal data.', 'inputSchema' => array( 'type' => 'object', 'properties' => (object) array(), 'required' => array(), ), ), ); } /** * Map tool name => callable. Keeps advertising and dispatch in sync. * * @return array */ function miintg_tool_callbacks() { return array( 'miintg_selftest' => 'miintg_selftest', 'wp_core_verify_checksums' => 'miintg_core_verify_checksums', 'wp_list_autoloaded_code' => 'miintg_list_autoloaded_code', 'wp_scan_uploads_anomalies' => 'miintg_scan_uploads_anomalies', 'wp_verify_plugin_checksums' => 'miintg_verify_plugin_checksums', 'wp_check_config_integrity' => 'miintg_check_config_integrity', 'wp_integrity_scan' => 'miintg_full_scan', 'wp_audit_privileged_users' => 'miintg_audit_privileged_users', ); } /** * Capability required per tool. * * @param string $tool Tool name. * @return string */ function miintg_tool_capability( $tool ) { $caps = array( 'miintg_selftest' => 'manage_options', 'wp_core_verify_checksums' => 'manage_options', 'wp_list_autoloaded_code' => 'manage_options', 'wp_scan_uploads_anomalies' => 'manage_options', 'wp_verify_plugin_checksums' => 'manage_options', 'wp_check_config_integrity' => 'manage_options', 'wp_integrity_scan' => 'manage_options', 'wp_audit_privileged_users' => 'manage_options', ); return isset( $caps[ $tool ] ) ? $caps[ $tool ] : 'manage_options'; } /* ------------------------------------------------------------------------- * 1. Advertise — waic_mcp_tools * ---------------------------------------------------------------------- */ add_filter( 'waic_mcp_tools', 'miintg_filter_mcp_tools', 10, 1 ); /** * @param array $tools Existing tool definitions keyed by name. * @return array */ function miintg_filter_mcp_tools( $tools ) { if ( ! is_array( $tools ) ) { $tools = array(); } // Not advertised until the site owner confirms data sharing. if ( ! miintg_ai_consent() ) { return $tools; } foreach ( miintg_tool_defs() as $name => $def ) { // Never clobber a core tool of the same name. if ( ! isset( $tools[ $name ] ) ) { $tools[ $name ] = $def; } } return $tools; } /* ------------------------------------------------------------------------- * 2. Execute — waic_mcp_callback * * Signature: ( $result, $tool, $args, $id, $module ) * * Return null to pass through to other handlers. AI Copilot registers its own * handleCallback() at priority 10; running at 9 means unknown-to-core tools * are handled here first, and core tools fall through untouched. * ---------------------------------------------------------------------- */ add_filter( 'waic_mcp_callback', 'miintg_mcp_callback', 9, 5 ); /** * @param mixed $result Result from a prior handler, or null. * @param string $tool Tool name. * @param array $args Tool arguments. * @param int $id JSON-RPC request id. * @param object $module AI Copilot MCP module instance. * @return mixed Null to pass through, or a JSON-RPC response array. */ function miintg_mcp_callback( $result, $tool, $args, $id, $module = null ) { // Another handler already answered. if ( ! empty( $result ) ) { return $result; } // Not available until the site owner confirms data sharing. if ( ! miintg_ai_consent() ) { return $result; } $callbacks = miintg_tool_callbacks(); // Not one of ours — pass through. if ( ! isset( $callbacks[ $tool ] ) ) { return $result; } $callback = $callbacks[ $tool ]; if ( ! is_callable( $callback ) ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => -42611, 'message' => 'Quick Integrity Scanner: callback not callable for ' . $tool, ), ); } // Own capability check. dispatchTool() is never reached for our tools, // so the plugin's canUseTool() does not run. $capability = miintg_tool_capability( $tool ); if ( $capability && ! current_user_can( $capability ) ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => -42610, 'message' => 'Current user is not allowed to use this MCP tool', ), ); } if ( ! is_array( $args ) ) { $args = array(); } try { $payload = call_user_func( $callback, $args ); } catch ( Exception $e ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => -42612, 'message' => 'Quick Integrity Scanner: ' . $e->getMessage(), ), ); } // Full JSON-RPC envelope. Returning this shape means AI Copilot passes it // through verbatim rather than re-wrapping via formatToolResult(). return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array( 'content' => array( array( 'type' => 'text', 'text' => wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), ), ), 'data' => $payload, ), ); } /* ------------------------------------------------------------------------- * REST fallback * * GET /wp-json/mi-integrity/v1/selftest?target=wp-includes&sample=5 * * Requires a logged-in user with manage_options. * ---------------------------------------------------------------------- */ add_action( 'rest_api_init', 'miintg_register_rest' ); function miintg_register_rest() { register_rest_route( 'mi-integrity/v1', '/selftest', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'miintg_rest_selftest', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'target' => array( 'type' => 'string', 'required' => false, ), 'sample' => array( 'type' => 'integer', 'required' => false, ), ), ) ); // Diagnostic: confirms whether the MCP filters attached on this install. register_rest_route( 'mi-integrity/v1', '/status', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'miintg_rest_status', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); } /** * @param WP_REST_Request $request Request. * @return WP_REST_Response */ function miintg_rest_selftest( $request ) { return rest_ensure_response( miintg_selftest( array( 'target' => $request->get_param( 'target' ), 'sample' => $request->get_param( 'sample' ), ) ) ); } /** * @return WP_REST_Response */ function miintg_rest_status() { return rest_ensure_response( miintg_envelope( 'miintg_status', array( 'plugin_version' => MIINTG_VERSION, 'ai_copilot_detected' => class_exists( 'WaicDispatcher' ), 'tools_filter_hooked' => (bool) has_filter( 'waic_mcp_tools', 'miintg_filter_mcp_tools' ), 'callback_hooked' => (bool) has_filter( 'waic_mcp_callback', 'miintg_mcp_callback' ), 'declared_tools' => array_keys( miintg_tool_defs() ), ) ) ); } /* ------------------------------------------------------------------------- * WP-CLI fallback * * wp mi-integrity selftest --target=wp-includes --sample=5 * wp mi-integrity status * ---------------------------------------------------------------------- */ if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'mi-integrity selftest', function ( $args, $assoc_args ) { $out = miintg_selftest( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity status', function () { $out = miintg_rest_status(); WP_CLI::log( wp_json_encode( $out->get_data(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity verify-core', function ( $args, $assoc_args ) { $out = miintg_core_verify_checksums( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity autoloaded', function ( $args, $assoc_args ) { $out = miintg_list_autoloaded_code( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity scan-uploads', function ( $args, $assoc_args ) { $out = miintg_scan_uploads_anomalies( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity verify-plugins', function ( $args, $assoc_args ) { $out = miintg_verify_plugin_checksums( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity check-config', function ( $args, $assoc_args ) { $out = miintg_check_config_integrity( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity scan-all', function ( $args, $assoc_args ) { $out = miintg_full_scan( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); WP_CLI::add_command( 'mi-integrity audit-users', function ( $args, $assoc_args ) { $out = miintg_audit_privileged_users( $assoc_args ); WP_CLI::log( wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } ); }