$server Server row data. * @return bool */ public function visible_for( array $server ): bool { return ServerTypes::FLEET !== ( $server['server_type'] ?? '' ); } /** * Renders the read-only info dashboard. * * @since 0.0.6 * @param array $server Server row data. * @return void */ protected function render_body( array $server ): void { echo '
'; TypeRequirementNotice::instance()->render( $server, 'overview' ); $this->render_info_table( $server ); $this->render_instructions_setting( $server ); // The Application Passwords notice and the "Supported MCP Clients" // list that used to close this tab are gone. Both described the CONNECT // tab from a distance — one explained where credentials generated there // end up, the other listed the clients whose tabs are already on // screen — so the Overview tab ended on a summary of somewhere else, // below the settings it actually owns. echo '
'; } /** * Renders the 9-row server info table (Name / Description / Source / * Slug / Status / MCP API URL / Route Namespace / Route / Version). * * @since 0.0.6 * @param array $server Server row data. * @return void */ private function render_info_table( array $server ): void { echo ''; // Surface the Recommended pill next to the managed AcrossAI row. $recommended = ProtectedServers::is_recommended( (string) $server['server_slug'] ) ? ' ' . ProtectedServers::recommended_badge() : ''; $this->render_row( __( 'Server Name', 'acrossai-mcp-manager' ), sprintf( '%s%s', esc_html( (string) $server['server_name'] ), $recommended ) ); // Match reference — description row hidden when empty. if ( ! empty( $server['description'] ) ) { $this->render_row( __( 'Description', 'acrossai-mcp-manager' ), esc_html( (string) $server['description'] ) ); } $this->render_row( __( 'Source', 'acrossai-mcp-manager' ), $this->render_source_badge( (string) ( $server['registered_from'] ?? 'plugin' ) ) ); $this->render_row( __( 'Slug', 'acrossai-mcp-manager' ), sprintf( '%s', esc_html( (string) $server['server_slug'] ) ) ); $this->render_row( __( 'Status', 'acrossai-mcp-manager' ), $this->render_status_control( $server ) ); $mcp_api_url = rest_url( trailingslashit( (string) $server['server_route_namespace'] ) . (string) $server['server_route'] ); $this->render_row( __( 'MCP API URL', 'acrossai-mcp-manager' ), sprintf( '%s', esc_html( $mcp_api_url ) ) ); $this->render_row( __( 'Route Namespace', 'acrossai-mcp-manager' ), sprintf( '%s', esc_html( (string) $server['server_route_namespace'] ) ) ); $this->render_row( __( 'Route', 'acrossai-mcp-manager' ), sprintf( '%s', esc_html( (string) $server['server_route'] ) ) ); $this->render_row( __( 'Version', 'acrossai-mcp-manager' ), sprintf( '%s', esc_html( (string) $server['server_version'] ) ) ); echo ''; } /** * Renders a single info row (label + value). Value is expected to be * pre-escaped HTML by the caller (badge/code/text). * * @since 0.0.6 * @param string $label Row label (i18n'd). * @param string $value Row value (pre-escaped HTML). * @return void */ private function render_row( string $label, string $value ): void { printf( '%1$s%2$s', esc_html( $label ), wp_kses_post( $value ) ); } /** * Renders the Source badge (Plugin | Database). * * @since 0.0.6 * @param string $registered_from Either 'plugin' or 'database'. * @return string Pre-escaped HTML. */ private function render_source_badge( string $registered_from ): string { $is_database = 'database' === $registered_from; $label = $is_database ? __( 'Database', 'acrossai-mcp-manager' ) : __( 'Plugin', 'acrossai-mcp-manager' ); $css_class = $is_database ? 'acrossai-source-badge acrossai-source-database' : 'acrossai-source-badge acrossai-source-plugin'; return sprintf( '%2$s', esc_attr( $css_class ), esc_html( $label ) ); } /** * Renders the Status control — colored badge + Enable/Disable toggle link. * * @since 0.0.6 * @param array $server Server row data. * @return string Pre-escaped HTML. */ private function render_status_control( array $server ): string { $is_enabled = ! empty( $server['is_enabled'] ); $badge = sprintf( '%2$s', $is_enabled ? 'acrossai-status-active' : 'acrossai-status-inactive', esc_html( $is_enabled ? __( 'Active', 'acrossai-mcp-manager' ) : __( 'Inactive', 'acrossai-mcp-manager' ) ) ); // redirect_to=edit tells Settings::handle_actions() to send the // user back to this edit page after the toggle, instead of the // server list — the toggle button is embedded on the edit page. $toggle_url = wp_nonce_url( add_query_arg( array( 'page' => AdminPageSlugs::PARENT, 'action' => 'toggle_status', 'server' => (int) $server['id'], 'redirect_to' => 'edit', ), admin_url( 'admin.php' ) ), 'acrossai_mcp_toggle_' . (int) $server['id'] ); $toggle_label = $is_enabled ? __( 'Disable', 'acrossai-mcp-manager' ) : __( 'Enable', 'acrossai-mcp-manager' ); $button_class = $is_enabled ? 'button button-small' : 'button button-primary button-small'; $toggle_link = sprintf( '  %3$s', esc_url( $toggle_url ), esc_attr( $button_class ), esc_html( $toggle_label ) ); return $badge . $toggle_link; } /** * The per-server connect message: system default, or the operator's own. * * Two states on purpose. "System default" stores NOTHING — a saved copy of * the default would freeze the wording as it read on the day it was saved * and quietly stop tracking the plugin when that wording improves. Absence * of the row IS the default, so there is nothing to go stale. * * The textarea is prefilled with what the server sends today, so "custom" * starts from the real text rather than an empty box. It is * `Controller::default_instructions_for()`, the same value the live path * builds, so what an operator edits cannot differ from what they were * getting. * * @since 0.3.6 * @param array $server Server row data. * @return void */ private function render_instructions_setting( array $server ): void { $server_id = (int) ( $server['id'] ?? 0 ); if ( $server_id <= 0 ) { return; } $row = Query::instance()->get_item( $server_id ); if ( ! $row ) { return; } $default = MCPController::default_instructions_for( $row ); $stored = MCPServerMetaQuery::get_meta( $server_id, MCPController::INSTRUCTIONS_META_KEY ); $custom = null !== $stored && '' !== $stored; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only flag; the save itself verified its nonce. if ( ! empty( $_GET['acrossai_mcp_manager_instructions_saved'] ) ) { printf( '

%s

', esc_html__( 'Connect message saved. Already-connected AI clients keep the message they received; new connections get this one.', 'acrossai-mcp-manager' ) ); } printf( '
', esc_url( add_query_arg( array( 'page' => AdminPageSlugs::PARENT, 'action' => 'save_instructions', 'server' => $server_id, ), admin_url( 'admin.php' ) ) ) ); wp_nonce_field( 'acrossai_mcp_manager_instructions_' . $server_id, 'acrossai_mcp_manager_instructions_nonce' ); echo '

' . esc_html__( 'Default server message', 'acrossai-mcp-manager' ) . '

'; echo '

'; esc_html_e( 'Sent to an AI client when it connects, after this server\'s description. It tells the client what kind of server this is and which tool to call first.', 'acrossai-mcp-manager' ); echo '

'; printf( '

', checked( $custom, false, false ), esc_html__( 'System default', 'acrossai-mcp-manager' ), esc_html__( '— kept up to date by the plugin. Recommended.', 'acrossai-mcp-manager' ) ); printf( '

', checked( $custom, true, false ), esc_html__( 'Custom', 'acrossai-mcp-manager' ), esc_html__( '— write your own. It replaces the message below, not your description.', 'acrossai-mcp-manager' ) ); // Wrapped, and hidden server-side when System default is selected, so // the field is not there to read as "this is what will be sent" when it // is not. `backend.js` toggles it as the radios change; the `hidden` // attribute below is what makes the first paint correct without // waiting for script. // // With JavaScript off the field simply stays as rendered — and the save // handler reads the RADIO, never the textarea's visibility, so a // no-script operator can still switch to Custom and have it work. printf( '
', $custom ? '' : ' hidden' ); printf( '

', esc_attr__( 'Leave empty to use the system default.', 'acrossai-mcp-manager' ), // Prefilled with the live default when nothing is stored, so // choosing Custom starts from what the server actually sends. esc_textarea( $custom ? (string) $stored : $default ) ); echo '

'; esc_html_e( 'Leaving this empty keeps the system default.', 'acrossai-mcp-manager' ); echo '

'; echo '
'; printf( '

', esc_html__( 'Save connect message', 'acrossai-mcp-manager' ) ); echo '
'; } }