'; echo '
' . esc_html__( 'Other plugins can register their own WordPress Abilities. Any you turn on here becomes a governed MCP tool, wrapped in the same audit log, rate limit, and permission checks as everything else on this site. Each one stays off until you switch it on.', 'agent-abilities-for-mcp' ) . '
'; // WP < 6.9 has no Abilities registry to read. if ( ! function_exists( 'wp_get_abilities' ) ) { echo wp_kses( aafm_get_notice_html( 'warning', __( 'This needs the WordPress Abilities API, added in WordPress 6.9. Update WordPress to discover abilities from other plugins.', 'agent-abilities-for-mcp' ) ), aafm_admin_allowed_html() ); echo ''; return; } $groups = aafm_merge_bridge_groups_by_known_plugin( aafm_discover_foreign_abilities() ); // The RAW stored list, never the floored one. Read-only mode subtracts writes from what // registers, not from what the operator chose, so the switches here have to keep showing that // choice - a floored read would render every enabled write unticked and the next save would // make that lie permanent. $enabled = aafm_get_stored_bridged_abilities_raw(); // Enabled slugs that are no longer discoverable (their host plugin is inactive) still need a // home, so union them in as a disabled "Unavailable" group rather than dropping them silently. $discovered_slugs = array(); foreach ( $groups as $group ) { foreach ( $group['abilities'] as $ability ) { $discovered_slugs[] = (string) $ability['slug']; } } $orphans = array_values( array_diff( $enabled, $discovered_slugs ) ); if ( empty( $groups ) && empty( $orphans ) ) { echo wp_kses( aafm_get_notice_html( 'info', __( 'No abilities from other plugins yet. Activate a plugin that registers WordPress Abilities and they will show up here.', 'agent-abilities-for-mcp' ) ), aafm_admin_allowed_html() ); echo ''; return; } aafm_render_bridge_review_card( $groups, $enabled ); echo '' . esc_html( (string) $row['slug'] ) . '';
if ( '' !== $risk_label['note'] ) {
$body .= '' . esc_html( $risk_label['note'] ) . '
'; } if ( '' !== $notice ) { $body .= aafm_get_notice_html( 'info', $notice, array( 'inline' => true ) ); } $body .= '' . ' ' . '' . '
%s
', esc_html( sprintf( /* translators: 1: total abilities, 2: read/write/destructive breakdown, e.g. "27 read, 23 write, 2 destructive". */ _n( '%1$d ability · %2$s', '%1$d abilities · %2$s', count( $rows ), 'agent-abilities-for-mcp' ), count( $rows ), aafm_integration_count_breakdown( $counts ) ) ) ); echo ''; echo '' . esc_html( aafm_bridge_source_line( (string) ( $group['label'] ?? $ns ) ) ) . '
'; } $flagged = $disabled ? '' : aafm_bridge_group_personal_data_kind( $rows ); if ( '' !== $flagged ) { echo wp_kses( aafm_get_notice_html( 'info', aafm_bridge_personal_data_notice( $flagged ) ), aafm_admin_allowed_html() ); } // Per-group bulk controls: flip every ability in this plugin at once. Not on the orphan // group (its rows are disabled). Bulk enable is a deliberate action, so it turns on // destructive abilities too without the per-row confirm; the DESTRUCTIVE badge still warns. if ( ! $disabled && $rows ) { echo '' . esc_html__( 'This plugin labels none of its tools as read-only, so Allow only read tools turns all of them off.', 'agent-abilities-for-mcp' ) . '
'; } } echo '%1$s %2$s
%s
', esc_html( sprintf( /* translators: %s: source plugin display name. */ __( 'Permission determined by %s', 'agent-abilities-for-mcp' ), $plugin_label ) ) ); } /** * Render a single bridge ability toggle row. * * Bridge-specific by design (see MF-1): it emits name="bridged_abilities[]", derives its checked * state from the enabled bridged list, and its save posts to aafm_save_bridged_abilities - so it can * never write a foreign slug into the native aafm_enabled_abilities option. Every CSS class is * identical to the integrations row; only the field name, checked source, destructive confirm, and * the resulting-tool-name line differ. * * @param array%s
', esc_html( $hint ) ); $data_notice = $disabled ? '' : aafm_bridge_personal_data_row_notice( aafm_bridge_personal_data_kind( $slug ) ); if ( '' !== $data_notice ) { echo wp_kses( aafm_get_notice_html( 'info', $data_notice, array( 'inline' => true ) ), aafm_admin_allowed_html() ); } // An unavailable row has no annotations to read, so it says nothing about what the plugin labelled. if ( ! $disabled && '' !== $risk_label['note'] ) { printf( '%s
', esc_html( $risk_label['note'] ) ); } // The resulting MCP tool name as a muted monospace line (not a second badge). printf( '%s
' . esc_html( aafm_ability_lock_note( $lock_reason ) ) . '
'; } // Destructive confirm: an inline reveal, never a JS modal. admin.js shows it when the switch // is flipped on; "Enable anyway" keeps it on, "Cancel" flips it back off. A locked row has no // switch to flip, so it never renders one. if ( $destructive && ! $disabled && null === $lock_reason ) { echo ''; echo ' '; echo ''; echo '
'; echo '