'; 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__( 'The same plugin also wrote each tool\'s read label, its permission check and the description the agent reads, and none of that has been reviewed here.', 'agent-abilities-for-mcp' ) . '
' . '' . esc_html__( 'A plugin that doesn\'t mark its reads as read-only loses them in read-only mode and under Allow only read tools. People on lower roles can see the names of tools they can\'t run, and the plugin\'s own check refuses the call.', 'agent-abilities-for-mcp' ) . '
'; echo wp_kses( aafm_get_notice_html( 'warning', '' . esc_html__( 'These abilities are written by other plugins, so what they read and change is up to them, and their output is not filtered or redacted here. They can only touch what the connected account\'s WordPress role allows, and every call is recorded in the activity log. Turn on only what you trust the connected agent to run.', 'agent-abilities-for-mcp' ) . '
' . aafm_get_set_more_html( __( 'See more about tools from other plugins', 'agent-abilities-for-mcp' ), $disclaimer_more ), array( 'html' => true ) ), aafm_admin_allowed_html() ); 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 '