diff --git a/crates/op-editor-core/src/agent_settings_connection.rs b/crates/op-editor-core/src/agent_settings_connection.rs index 975aa921d..3748c4c0e 100644 --- a/crates/op-editor-core/src/agent_settings_connection.rs +++ b/crates/op-editor-core/src/agent_settings_connection.rs @@ -111,6 +111,27 @@ impl McpServer { } impl AgentSettings { + /// True when an embedding host owns the MCP lifecycle + /// (`embed_mcp_url` set): the server card then reads as + /// always-running at the host's port, with no start/stop button + /// and no port editing — the daemon-internal `mcp_server` state + /// is meaningless in that surface. + pub fn mcp_host_managed(&self) -> bool { + self.embed_mcp_url.is_some() + } + + /// The port component of `embed_mcp_url`, for the read-only port + /// display (e.g. `"63655"` from `http://127.0.0.1:63655/mcp`). + pub fn embed_mcp_port_text(&self) -> Option<&str> { + let url = self.embed_mcp_url.as_deref()?; + let after_scheme = url.split_once("://").map_or(url, |(_, rest)| rest); + let authority = after_scheme + .split(['/', '?', '#']) + .next() + .unwrap_or_default(); + authority.rsplit_once(':').map(|(_, port)| port) + } + /// The client-config card's one-line display text. Prefers the /// embedding host's real endpoint (`embed_mcp_url`, set via the /// bridge init) over the daemon-internal `mcp_server` port. @@ -346,3 +367,18 @@ mod embed_mcp_url_tests { .contains("\"url\": \"http://127.0.0.1:63655/mcp\"")); } } + +#[cfg(test)] +mod embed_mcp_host_tests { + use super::*; + + #[test] + fn host_managed_state_and_port_derive_from_the_embed_url() { + let mut settings = AgentSettings::default(); + assert!(!settings.mcp_host_managed()); + assert_eq!(settings.embed_mcp_port_text(), None); + settings.embed_mcp_url = Some("http://127.0.0.1:63655/mcp".into()); + assert!(settings.mcp_host_managed()); + assert_eq!(settings.embed_mcp_port_text(), Some("63655")); + } +} diff --git a/crates/op-editor-ui/src/widgets/agent_settings_mcp.rs b/crates/op-editor-ui/src/widgets/agent_settings_mcp.rs index 38f319670..fb84d8dda 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_mcp.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_mcp.rs @@ -45,7 +45,7 @@ fn server_card_top(content: Rect) -> f32 { } fn client_config_block_h(settings: &AgentSettings) -> f32 { - if settings.mcp_server.running { + if settings.mcp_host_managed() || settings.mcp_server.running { CLIENT_CONFIG_GAP + CLIENT_CONFIG_H } else { 0.0 @@ -156,13 +156,21 @@ fn client_config_copy_button_rect(content: Rect) -> Rect { pub(super) const CLI_INTEGRATIONS_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); pub fn hit_test(content: Rect, settings: &AgentSettings, scrolled: Point2D) -> McpHit { - if (server_button_rect(content)).contains(scrolled) { + // Host-managed (embed): the extension owns the lifecycle — no + // start/stop, no port editing; the config card always copies. + let host_managed = settings.mcp_host_managed(); + if !host_managed && (server_button_rect(content)).contains(scrolled) { return McpHit::ToggleServer; } - if settings.mcp_server.running && (client_config_copy_button_rect(content)).contains(scrolled) { + if (host_managed || settings.mcp_server.running) + && (client_config_copy_button_rect(content)).contains(scrolled) + { return McpHit::CopyClientConfig; } - if !settings.mcp_server.running && (port_field_rect(content)).contains(scrolled) { + if !host_managed + && !settings.mcp_server.running + && (port_field_rect(content)).contains(scrolled) + { return McpHit::FocusPort; } if CLI_INTEGRATIONS_AVAILABLE { @@ -255,7 +263,10 @@ fn paint_server_card( radius: 10.0, } .paint(cx.backend, card, &tokens_from_theme(theme)); - let running = settings.mcp_server.running; + // Host-managed (embed): the extension's proxy is alive by the time any + // editor mounts — the card reads always-running at the host's port. + let host_managed = settings.mcp_host_managed(); + let running = host_managed || settings.mcp_server.running; let mid_y = card.origin.y + SERVER_CARD_H / 2.0; let dot = Rect { origin: Point2D::new(card.origin.x + 16.0, mid_y - 4.0), @@ -303,10 +314,15 @@ fn paint_server_card( Point2D::new(port_field_x - 8.0 - port_label_w, mid_y + 4.0), ); let port_field = port_field_rect(content); - let port_editable = !running; + let port_editable = !running && !host_managed; let focused = port_editable && matches!(settings.focus, Some(SettingsFocus::McpPort)); let port_str = if focused { ui.settings_input.text().to_owned() + } else if host_managed { + settings + .embed_mcp_port_text() + .unwrap_or_default() + .to_owned() } else { format!("{}", settings.mcp_server.port) }; @@ -349,6 +365,11 @@ fn paint_server_card( .draw_text(&port_layout, Point2D::new(port_x, port_y)); } + // Host-managed: no start/stop affordance — the extension owns the + // lifecycle (hit_test skips ToggleServer under the same gate). + if host_managed { + return; + } let btn_bg = if running { theme.muted } else { theme.primary }; let btn_fg = if running { theme.foreground @@ -398,7 +419,7 @@ fn paint_client_config( content: Rect, now_ms: u64, ) { - if !settings.mcp_server.running { + if !settings.mcp_host_managed() && !settings.mcp_server.running { return; } let rect = client_config_rect(content);