From eef824cce5d6cfbac34d9c8af6d84ba7132dcef2 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 1 Jul 2026 12:52:33 +0200 Subject: [PATCH] agent_ui: Open MCP settings UI instead of modal (#60188) This makes it so that we show open the settings UI instead of the modal when adding an MCP server when clicking on `Add server`: image We still show the modal when installing an extension. Since we're hopefully replacing MCP extensions with MCP registry support soon, we can leave it as is for now. Release Notes: - N/A --- .../configure_context_server_modal.rs | 192 +++--------------- crates/agent_ui/src/agent_panel.rs | 24 ++- crates/agent_ui/src/agent_ui.rs | 51 +---- docs/src/ai/mcp.md | 4 +- 4 files changed, 45 insertions(+), 226 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 843a35068b..6a32b48860 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -6,8 +6,7 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension_host::ExtensionStore; use gpui::{ AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, - Subscription, Task, TaskExt, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, - prelude::*, + Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, }; use language::{Language, LanguageRegistry}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; @@ -32,12 +31,7 @@ use ui::{ use util::ResultExt as _; use workspace::{ModalView, Workspace}; -use crate::{AddContextServer, ContextServerType}; - enum ConfigurationTarget { - New { - server_type: ContextServerType, - }, Existing { id: ContextServerId, command: ContextServerCommand, @@ -56,14 +50,15 @@ enum ConfigurationTarget { }, } +enum ExistingServerType { + Local, + Remote, +} + enum ConfigurationSource { - New { - editor: Entity, - server_type: ContextServerType, - }, Existing { editor: Entity, - server_type: ContextServerType, + server_type: ExistingServerType, }, Extension { id: ContextServerId, @@ -79,10 +74,6 @@ impl ConfigurationSource { !matches!(self, ConfigurationSource::Extension { editor: None, .. }) } - fn is_new(&self) -> bool { - matches!(self, ConfigurationSource::New { .. }) - } - fn from_target( target: ConfigurationTarget, language_registry: Arc, @@ -109,18 +100,6 @@ impl ConfigurationSource { } match target { - ConfigurationTarget::New { server_type } => ConfigurationSource::New { - editor: create_editor( - match server_type { - ContextServerType::Remote => context_server_http_input(None), - ContextServerType::Local => context_server_input(None), - }, - jsonc_language, - window, - cx, - ), - server_type, - }, ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing { editor: create_editor( context_server_input(Some((id, command))), @@ -128,7 +107,7 @@ impl ConfigurationSource { window, cx, ), - server_type: ContextServerType::Local, + server_type: ExistingServerType::Local, }, ConfigurationTarget::ExistingHttp { id, @@ -142,7 +121,7 @@ impl ConfigurationSource { window, cx, ), - server_type: ContextServerType::Remote, + server_type: ExistingServerType::Remote, }, ConfigurationTarget::Extension { @@ -180,15 +159,11 @@ impl ConfigurationSource { fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> { match self { - ConfigurationSource::New { - editor, - server_type, - } - | ConfigurationSource::Existing { + ConfigurationSource::Existing { editor, server_type, } => match *server_type { - ContextServerType::Remote => { + ExistingServerType::Remote => { parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth, oauth)| { ( id, @@ -202,7 +177,7 @@ impl ConfigurationSource { ) }) } - ContextServerType::Local => { + ExistingServerType::Local => { parse_input(&editor.read(cx).text(cx)).map(|(id, command)| { ( id, @@ -459,13 +434,10 @@ impl ConfigureContextServerModal { target: &ConfigurationTarget, cx: &App, ) -> State { - let Some(server_id) = (match target { + let server_id = match target { ConfigurationTarget::Existing { id, .. } | ConfigurationTarget::ExistingHttp { id, .. } - | ConfigurationTarget::Extension { id, .. } => Some(id), - ConfigurationTarget::New { .. } => None, - }) else { - return State::Idle; + | ConfigurationTarget::Extension { id, .. } => id, }; match context_server_store.read(cx).status_for_server(server_id) { @@ -490,32 +462,6 @@ impl ConfigureContextServerModal { } } - pub fn register( - workspace: &mut Workspace, - language_registry: Arc, - _window: Option<&mut Window>, - _cx: &mut Context, - ) { - workspace.register_action({ - move |_workspace, action: &AddContextServer, window, cx| { - let workspace_handle = cx.weak_entity(); - let language_registry = language_registry.clone(); - let server_type = action.context_server_type; - window - .spawn(cx, async move |cx| { - Self::show_modal( - ConfigurationTarget::New { server_type }, - language_registry, - workspace_handle, - cx, - ) - .await - }) - .detach_and_log_err(cx); - } - }); - } - pub fn show_modal_for_existing_server( server_id: ContextServerId, language_registry: Arc, @@ -600,12 +546,11 @@ impl ConfigureContextServerModal { workspace: workspace_handle, state: Self::initial_state(&context_server_store, &target, cx), - original_server_id: match &target { - ConfigurationTarget::Existing { id, .. } => Some(id.clone()), - ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()), - ConfigurationTarget::Extension { id, .. } => Some(id.clone()), - ConfigurationTarget::New { .. } => None, - }, + original_server_id: Some(match &target { + ConfigurationTarget::Existing { id, .. } + | ConfigurationTarget::ExistingHttp { id, .. } + | ConfigurationTarget::Extension { id, .. } => id.clone(), + }), source: ConfigurationSource::from_target( target, language_registry, @@ -835,7 +780,6 @@ impl ModalView for ConfigureContextServerModal {} impl Focusable for ConfigureContextServerModal { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.source { - ConfigurationSource::New { editor, .. } => editor.focus_handle(cx), ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx), ConfigurationSource::Extension { editor, .. } => editor .as_ref() @@ -850,7 +794,6 @@ impl EventEmitter for ConfigureContextServerModal {} impl ConfigureContextServerModal { fn render_modal_header(&self) -> ModalHeader { let text: SharedString = match &self.source { - ConfigurationSource::New { .. } => "Add MCP Server".into(), ConfigurationSource::Existing { .. } => "Configure MCP Server".into(), ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(), }; @@ -881,79 +824,8 @@ impl ConfigureContextServerModal { } } - fn render_tab_bar(&self, cx: &mut Context) -> Option { - let is_http = match &self.source { - ConfigurationSource::New { server_type, .. } => { - *server_type == ContextServerType::Remote - } - _ => return None, - }; - - let tab = |label: &'static str, active: bool| { - div() - .id(label) - .cursor_pointer() - .p_1() - .text_sm() - .border_b_1() - .when_else( - active, - |this| this.border_color(cx.theme().colors().border_focused), - |this| { - this.border_color(gpui::transparent_black()) - .text_color(cx.theme().colors().text_muted) - .hover(|s| s.text_color(cx.theme().colors().text)) - }, - ) - .child(label) - }; - - Some( - h_flex() - .pt_1() - .mb_2p5() - .gap_1() - .border_b_1() - .border_color(cx.theme().colors().border.opacity(0.5)) - .child( - tab("Local", !is_http).on_click(cx.listener(|this, _, window, cx| { - if let ConfigurationSource::New { - editor, - server_type, - } = &mut this.source - && *server_type != ContextServerType::Local - { - *server_type = ContextServerType::Local; - let new_text = context_server_input(None); - editor.update(cx, |editor, cx| { - editor.set_text(new_text, window, cx); - }); - } - })), - ) - .child( - tab("Remote", is_http).on_click(cx.listener(|this, _, window, cx| { - if let ConfigurationSource::New { - editor, - server_type, - } = &mut this.source - && *server_type != ContextServerType::Remote - { - *server_type = ContextServerType::Remote; - let new_text = context_server_http_input(None); - editor.update(cx, |editor, cx| { - editor.set_text(new_text, window, cx); - }); - } - })), - ) - .into_any_element(), - ) - } - fn render_modal_content(&self, cx: &App) -> AnyElement { let editor = match &self.source { - ConfigurationSource::New { editor, .. } => editor, ConfigurationSource::Existing { editor, .. } => editor, ConfigurationSource::Extension { editor, .. } => { let Some(editor) = editor else { @@ -1053,24 +925,15 @@ impl ConfigureContextServerModal { ), ) .children(self.source.has_configuration_options().then(|| { - Button::new( - "add-server", - if self.source.is_new() { - "Add Server" - } else { - "Configure Server" - }, - ) - .disabled(is_busy) - .key_binding( - KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click( - cx.listener(|this, _event, _window, cx| { + Button::new("configure-server", "Configure Server") + .disabled(is_busy) + .key_binding( + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(cx.listener(|this, _event, _window, cx| { this.confirm(&menu::Confirm, cx) - }), - ) + })) })), ) } @@ -1278,7 +1141,6 @@ impl Render for ConfigureContextServerModal { .overflow_y_scroll() .track_scroll(&self.scroll_handle) .child(self.render_modal_description(window, cx)) - .children(self.render_tab_bar(cx)) .child(self.render_modal_content(cx)) .child(match &self.state { State::Idle => div(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 01b380044a..7fe2d2c0b9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -44,19 +44,19 @@ use crate::terminal_thread_metadata_store::{ }; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent}; use crate::{ - AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, - LoadThreadFromClipboard, NewTerminalThread, NewThread, OpenActiveThreadAsMarkdown, - OpenAgentDiff, ResetFastModeWarnings, ResetTrialEndUpsell, ResetTrialUpsell, - ShowAllSidebarThreadMetadata, ShowThreadMetadata, ToggleNewThreadMenu, ToggleOptionsMenu, + Agent, AgentInitialContent, AgentThreadSource, ExternalSourcePrompt, NewExternalAgentThread, + NewNativeAgentThreadFromSummary, +}; +use crate::{ + AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, LoadThreadFromClipboard, + NewTerminalThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, ResetFastModeWarnings, + ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata, + ToggleNewThreadMenu, ToggleOptionsMenu, conversation_view::{ AcpThreadViewEvent, RootThreadUpdated, ThreadView, reset_fast_mode_warnings, }, ui::{AgentNotification, AgentNotificationEvent, EndTrialUpsell}, }; -use crate::{ - Agent, AgentInitialContent, AgentThreadSource, ExternalSourcePrompt, NewExternalAgentThread, - NewNativeAgentThreadFromSummary, -}; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Context as _, Result, anyhow}; @@ -5560,7 +5560,13 @@ impl AgentPanel { if !showing_terminal { menu = menu .header("MCP Servers") - .action("Add Server…", Box::new(AddContextServer::local())) + .action( + "Add Server…", + Box::new(zed_actions::OpenSettingsAt { + path: "context_servers".to_string(), + target: None, + }), + ) .action( "Install New Servers…", Box::new(zed_actions::Extensions { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index ad5940d21c..1949a5965a 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -67,7 +67,7 @@ use std::any::TypeId; use std::path::{Path, PathBuf}; use workspace::Workspace; -use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; +use crate::agent_configuration::ManageProfilesModal; pub use crate::agent_connection_store::{ActiveAcpConnection, AgentConnectionStore}; pub use crate::agent_panel::{ AgentPanel, AgentPanelEvent, AgentPanelTerminalInfo, MaxIdleRetainedThreads, TerminalId, @@ -353,49 +353,6 @@ pub struct ToggleCommandPattern { #[serde(deny_unknown_fields)] pub struct NewThread; -/// The kind of context server to configure when adding a new one. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ContextServerType { - /// A context server that runs locally via stdin/stdout. - #[default] - Local, - /// A context server that is connected to over HTTP. - Remote, -} - -/// Adds a context server to the configuration. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = agent)] -#[serde(deny_unknown_fields)] -pub struct AddContextServer { - /// The kind of context server to add. - #[serde(default)] - pub context_server_type: ContextServerType, -} - -impl Default for AddContextServer { - fn default() -> Self { - Self::local() - } -} - -impl AddContextServer { - /// Returns an action that adds a local (stdin/stdout) context server. - pub fn local() -> Self { - Self { - context_server_type: ContextServerType::Local, - } - } - - /// Returns an action that adds a remote (HTTP) context server. - pub fn remote() -> Self { - Self { - context_server_type: ContextServerType::Remote, - } - } -} - /// Creates a new external agent conversation thread. #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] @@ -628,16 +585,12 @@ pub fn init( init_language_model_settings(cx); } agent_panel::init(cx); - context_server_configuration::init(language_registry.clone(), fs.clone(), cx); + context_server_configuration::init(language_registry, fs.clone(), cx); thread_metadata_store::init(cx); terminal_thread_metadata_store::init(cx); inline_assistant::init(fs.clone(), prompt_builder.clone(), cx); terminal_inline_assistant::init(fs.clone(), prompt_builder, cx); - cx.observe_new(move |workspace, window, cx| { - ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) - }) - .detach(); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action( move |workspace: &mut Workspace, diff --git a/docs/src/ai/mcp.md b/docs/src/ai/mcp.md index 83c456be0b..777fd0e9b0 100644 --- a/docs/src/ai/mcp.md +++ b/docs/src/ai/mcp.md @@ -51,7 +51,7 @@ Popular servers available as an extension include: ### As Custom Servers Creating an extension is not the only way to use MCP servers in Zed. -You can connect both local and remote MCP servers easily utilizing the MCP server modal, which you can open by invoking the {#action agent::AddContextServer} action. Your specified configuration will create entries in your settings file (which you can open with {#action zed::OpenSettingsFile}) similar to the ones below: +You can connect both local and remote MCP servers from **Settings → AI → MCP Servers** (also accessible via the {#action agent::OpenSettings} action, then selecting `MCP Servers`). Click `Add Server` in the page header, then choose `Add Local Server` or `Add Remote Server`. Your specified configuration will create entries in your settings file (which you can open with {#action zed::OpenSettingsFile}) similar to the ones below: ```json [settings] { @@ -72,8 +72,6 @@ You can connect both local and remote MCP servers easily utilizing the MCP serve } ``` -Alternatively, you can open the modal from **Settings → AI → MCP Servers** (also accessible via the {#action agent::OpenSettings} action, then selecting `MCP Servers`) and clicking the `Add Server` button in the page header, then choosing `Add Local Server` or `Add Remote Server`. - > Note: When a remote MCP server has no configured `"Authorization"` header, Zed will prompt you to authenticate yourself against the MCP server using the standard MCP OAuth flow. ## Using MCP Servers