agent: Improve the UX around interacting with MCP servers (#32622)

Danilo Leal , Bennet Bo Fenner , and Ben Brandt created

Still a work in progress! Todos before merging:

- [x] Allow to delete (not just turn off) an MCP server from the panel's
settings view
- [x] Also uninstall the extension upon deleting the server (check if
the extension just provides MCPs)
- [x] Resolve repository URL again
- [x] Add a button to open the configuration modal from the panel's
settings view
- [x] Improve modal UX to install and configure a non-extension MCP

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

Cargo.lock                                                             |   2 
assets/icons/zed_mcp_custom.svg                                        |   2 
assets/icons/zed_mcp_extension.svg                                     |   2 
crates/agent/Cargo.toml                                                |   2 
crates/agent/src/agent.rs                                              |   9 
crates/agent/src/agent_configuration.rs                                | 266 
crates/agent/src/agent_configuration/add_context_server_modal.rs       | 195 
crates/agent/src/agent_configuration/configure_context_server_modal.rs | 962 
crates/agent/src/agent_panel.rs                                        |  13 
crates/agent/src/context_server_configuration.rs                       | 111 
crates/extension_host/src/extension_host.rs                            |   9 
crates/extension_host/src/extension_store_test.rs                      |   4 
crates/extensions_ui/src/extensions_ui.rs                              |   6 
crates/icons/src/icons.rs                                              |   2 
crates/project/src/context_server_store.rs                             |   7 
crates/settings/src/settings_store.rs                                  |  33 
16 files changed, 918 insertions(+), 707 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -71,6 +71,7 @@ dependencies = [
  "db",
  "editor",
  "extension",
+ "extension_host",
  "feature_flags",
  "file_icons",
  "fs",
@@ -127,7 +128,6 @@ dependencies = [
  "time",
  "time_format",
  "ui",
- "ui_input",
  "urlencoding",
  "util",
  "uuid",

assets/icons/zed_mcp_custom.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>

assets/icons/zed_mcp_extension.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>

crates/agent/Cargo.toml 🔗

@@ -36,6 +36,7 @@ convert_case.workspace = true
 db.workspace = true
 editor.workspace = true
 extension.workspace = true
+extension_host.workspace = true
 feature_flags.workspace = true
 file_icons.workspace = true
 fs.workspace = true
@@ -90,7 +91,6 @@ thiserror.workspace = true
 time.workspace = true
 time_format.workspace = true
 ui.workspace = true
-ui_input.workspace = true
 urlencoding.workspace = true
 util.workspace = true
 uuid.workspace = true

crates/agent/src/agent.rs 🔗

@@ -46,7 +46,7 @@ use settings::{Settings as _, SettingsStore};
 use thread::ThreadId;
 
 pub use crate::active_thread::ActiveThread;
-use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
+use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
 pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
 pub use crate::context::{ContextLoadResult, LoadedContext};
 pub use crate::inline_assistant::InlineAssistant;
@@ -162,7 +162,7 @@ pub fn init(
     assistant_slash_command::init(cx);
     thread_store::init(cx);
     agent_panel::init(cx);
-    context_server_configuration::init(language_registry, fs.clone(), cx);
+    context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
 
     register_slash_commands(cx);
     inline_assistant::init(
@@ -178,7 +178,10 @@ pub fn init(
         cx,
     );
     indexed_docs::init(cx);
-    cx.observe_new(AddContextServerModal::register).detach();
+    cx.observe_new(move |workspace, window, cx| {
+        ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
+    })
+    .detach();
     cx.observe_new(ManageProfilesModal::register).detach();
 }
 

crates/agent/src/agent_configuration.rs 🔗

@@ -1,4 +1,3 @@
-mod add_context_server_modal;
 mod configure_context_server_modal;
 mod manage_profiles_modal;
 mod tool_picker;
@@ -9,22 +8,29 @@ use agent_settings::AgentSettings;
 use assistant_tool::{ToolSource, ToolWorkingSet};
 use collections::HashMap;
 use context_server::ContextServerId;
+use extension::ExtensionManifest;
+use extension_host::ExtensionStore;
 use fs::Fs;
 use gpui::{
-    Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
-    Focusable, ScrollHandle, Subscription, Transformation, percentage,
+    Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
+    Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
 };
+use language::LanguageRegistry;
 use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use project::context_server_store::{ContextServerStatus, ContextServerStore};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use project::{
+    context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
+    project_settings::ProjectSettings,
+};
 use settings::{Settings, update_settings_file};
 use ui::{
-    Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
-    prelude::*,
+    ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState,
+    Switch, SwitchColor, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
+use workspace::Workspace;
 use zed_actions::ExtensionCategoryFilter;
 
-pub(crate) use add_context_server_modal::AddContextServerModal;
 pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
 pub(crate) use manage_profiles_modal::ManageProfilesModal;
 
@@ -32,6 +38,8 @@ use crate::AddContextServer;
 
 pub struct AgentConfiguration {
     fs: Arc<dyn Fs>,
+    language_registry: Arc<LanguageRegistry>,
+    workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
     context_server_store: Entity<ContextServerStore>,
@@ -48,6 +56,8 @@ impl AgentConfiguration {
         fs: Arc<dyn Fs>,
         context_server_store: Entity<ContextServerStore>,
         tools: Entity<ToolWorkingSet>,
+        language_registry: Arc<LanguageRegistry>,
+        workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -70,11 +80,16 @@ impl AgentConfiguration {
             },
         );
 
+        cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
+            .detach();
+
         let scroll_handle = ScrollHandle::new();
         let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 
         let mut this = Self {
             fs,
+            language_registry,
+            workspace,
             focus_handle,
             configuration_views_by_provider: HashMap::default(),
             context_server_store,
@@ -460,9 +475,22 @@ impl AgentConfiguration {
             .read(cx)
             .status_for_server(&context_server_id)
             .unwrap_or(ContextServerStatus::Stopped);
+        let server_configuration = self
+            .context_server_store
+            .read(cx)
+            .configuration_for_server(&context_server_id);
 
         let is_running = matches!(server_status, ContextServerStatus::Running);
         let item_id = SharedString::from(context_server_id.0.clone());
+        let is_from_extension = server_configuration
+            .as_ref()
+            .map(|config| {
+                matches!(
+                    config.as_ref(),
+                    ContextServerConfiguration::Extension { .. }
+                )
+            })
+            .unwrap_or(false);
 
         let error = if let ContextServerStatus::Error(error) = server_status.clone() {
             Some(error)
@@ -484,6 +512,18 @@ impl AgentConfiguration {
 
         let border_color = cx.theme().colors().border.opacity(0.6);
 
+        let (source_icon, source_tooltip) = if is_from_extension {
+            (
+                IconName::ZedMcpExtension,
+                "This MCP server was installed from an extension.",
+            )
+        } else {
+            (
+                IconName::ZedMcpCustom,
+                "This custom MCP server was installed directly.",
+            )
+        };
+
         let (status_indicator, tooltip_text) = match server_status {
             ContextServerStatus::Starting => (
                 Icon::new(IconName::LoadCircle)
@@ -511,6 +551,105 @@ impl AgentConfiguration {
             ),
         };
 
+        let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
+            .trigger_with_tooltip(
+                IconButton::new("context-server-config-menu", IconName::Settings)
+                    .icon_color(Color::Muted)
+                    .icon_size(IconSize::Small),
+                Tooltip::text("Open MCP server options"),
+            )
+            .anchor(Corner::TopRight)
+            .menu({
+                let fs = self.fs.clone();
+                let context_server_id = context_server_id.clone();
+                let language_registry = self.language_registry.clone();
+                let context_server_store = self.context_server_store.clone();
+                let workspace = self.workspace.clone();
+                move |window, cx| {
+                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+                        menu.entry("Configure Server", None, {
+                            let context_server_id = context_server_id.clone();
+                            let language_registry = language_registry.clone();
+                            let workspace = workspace.clone();
+                            move |window, cx| {
+                                ConfigureContextServerModal::show_modal_for_existing_server(
+                                    context_server_id.clone(),
+                                    language_registry.clone(),
+                                    workspace.clone(),
+                                    window,
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                            }
+                        })
+                        .separator()
+                        .entry("Delete", None, {
+                            let fs = fs.clone();
+                            let context_server_id = context_server_id.clone();
+                            let context_server_store = context_server_store.clone();
+                            let workspace = workspace.clone();
+                            move |_, cx| {
+                                let is_provided_by_extension = context_server_store
+                                    .read(cx)
+                                    .configuration_for_server(&context_server_id)
+                                    .as_ref()
+                                    .map(|config| {
+                                        matches!(
+                                            config.as_ref(),
+                                            ContextServerConfiguration::Extension { .. }
+                                        )
+                                    })
+                                    .unwrap_or(false);
+
+                                let uninstall_extension_task = match (
+                                    is_provided_by_extension,
+                                    resolve_extension_for_context_server(&context_server_id, cx),
+                                ) {
+                                    (true, Some((id, manifest))) => {
+                                        if extension_only_provides_context_server(manifest.as_ref())
+                                        {
+                                            ExtensionStore::global(cx).update(cx, |store, cx| {
+                                                store.uninstall_extension(id, cx)
+                                            })
+                                        } else {
+                                            workspace.update(cx, |workspace, cx| {
+                                                show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
+                                            }).log_err();
+                                            Task::ready(Ok(()))
+                                        }
+                                    }
+                                    _ => Task::ready(Ok(())),
+                                };
+
+                                cx.spawn({
+                                    let fs = fs.clone();
+                                    let context_server_id = context_server_id.clone();
+                                    async move |cx| {
+                                        uninstall_extension_task.await?;
+                                        cx.update(|cx| {
+                                            update_settings_file::<ProjectSettings>(
+                                                fs.clone(),
+                                                cx,
+                                                {
+                                                    let context_server_id =
+                                                        context_server_id.clone();
+                                                    move |settings, _| {
+                                                        settings
+                                                            .context_servers
+                                                            .remove(&context_server_id.0);
+                                                    }
+                                                },
+                                            )
+                                        })
+                                    }
+                                })
+                                .detach_and_log_err(cx);
+                            }
+                        })
+                    }))
+                }
+            });
+
         v_flex()
             .id(item_id.clone())
             .border_1()
@@ -556,7 +695,19 @@ impl AgentConfiguration {
                                     .tooltip(Tooltip::text(tooltip_text))
                                     .child(status_indicator),
                             )
-                            .child(Label::new(item_id).ml_0p5().mr_1p5())
+                            .child(Label::new(item_id).ml_0p5())
+                            .child(
+                                div()
+                                    .id("extension-source")
+                                    .mt_0p5()
+                                    .mx_1()
+                                    .tooltip(Tooltip::text(source_tooltip))
+                                    .child(
+                                        Icon::new(source_icon)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted),
+                                    ),
+                            )
                             .when(is_running, |this| {
                                 this.child(
                                     Label::new(if tool_count == 1 {
@@ -570,28 +721,37 @@ impl AgentConfiguration {
                             }),
                     )
                     .child(
-                        Switch::new("context-server-switch", is_running.into())
-                            .color(SwitchColor::Accent)
-                            .on_click({
-                                let context_server_manager = self.context_server_store.clone();
-                                let context_server_id = context_server_id.clone();
-                                move |state, _window, cx| match state {
-                                    ToggleState::Unselected | ToggleState::Indeterminate => {
-                                        context_server_manager.update(cx, |this, cx| {
-                                            this.stop_server(&context_server_id, cx).log_err();
-                                        });
-                                    }
-                                    ToggleState::Selected => {
-                                        context_server_manager.update(cx, |this, cx| {
-                                            if let Some(server) =
-                                                this.get_server(&context_server_id)
-                                            {
-                                                this.start_server(server, cx);
+                        h_flex()
+                            .gap_1()
+                            .child(context_server_configuration_menu)
+                            .child(
+                                Switch::new("context-server-switch", is_running.into())
+                                    .color(SwitchColor::Accent)
+                                    .on_click({
+                                        let context_server_manager =
+                                            self.context_server_store.clone();
+                                        let context_server_id = context_server_id.clone();
+
+                                        move |state, _window, cx| match state {
+                                            ToggleState::Unselected
+                                            | ToggleState::Indeterminate => {
+                                                context_server_manager.update(cx, |this, cx| {
+                                                    this.stop_server(&context_server_id, cx)
+                                                        .log_err();
+                                                });
                                             }
-                                        })
-                                    }
-                                }
-                            }),
+                                            ToggleState::Selected => {
+                                                context_server_manager.update(cx, |this, cx| {
+                                                    if let Some(server) =
+                                                        this.get_server(&context_server_id)
+                                                    {
+                                                        this.start_server(server, cx);
+                                                    }
+                                                })
+                                            }
+                                        }
+                                    }),
+                            ),
                     ),
             )
             .map(|parent| {
@@ -701,3 +861,51 @@ impl Render for AgentConfiguration {
             )
     }
 }
+
+fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
+    manifest.context_servers.len() == 1
+        && manifest.themes.is_empty()
+        && manifest.icon_themes.is_empty()
+        && manifest.languages.is_empty()
+        && manifest.grammars.is_empty()
+        && manifest.language_servers.is_empty()
+        && manifest.slash_commands.is_empty()
+        && manifest.indexed_docs_providers.is_empty()
+        && manifest.snippets.is_none()
+        && manifest.debug_locators.is_empty()
+}
+
+pub(crate) fn resolve_extension_for_context_server(
+    id: &ContextServerId,
+    cx: &App,
+) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
+    ExtensionStore::global(cx)
+        .read(cx)
+        .installed_extensions()
+        .iter()
+        .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
+        .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
+}
+
+// This notification appears when trying to delete
+// an MCP server extension that not only provides
+// the server, but other things, too, like language servers and more.
+fn show_unable_to_uninstall_extension_with_context_server(
+    workspace: &mut Workspace,
+    id: ContextServerId,
+    cx: &mut App,
+) {
+    let status_toast = StatusToast::new(
+        format!(
+            "Unable to uninstall the {} extension, as it provides more than just the MCP server.",
+            id.0
+        ),
+        cx,
+        |this, _cx| {
+            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
+                .action("Dismiss", |_, _| {})
+        },
+    );
+
+    workspace.toggle_status_toast(status_toast, cx);
+}

crates/agent/src/agent_configuration/add_context_server_modal.rs 🔗

@@ -1,195 +0,0 @@
-use context_server::ContextServerCommand;
-use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
-use project::project_settings::{ContextServerSettings, ProjectSettings};
-use settings::update_settings_file;
-use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
-use workspace::{ModalView, Workspace};
-
-use crate::AddContextServer;
-
-pub struct AddContextServerModal {
-    workspace: WeakEntity<Workspace>,
-    name_editor: Entity<SingleLineInput>,
-    command_editor: Entity<SingleLineInput>,
-}
-
-impl AddContextServerModal {
-    pub fn register(
-        workspace: &mut Workspace,
-        _window: Option<&mut Window>,
-        _cx: &mut Context<Workspace>,
-    ) {
-        workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
-            let workspace_handle = cx.entity().downgrade();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                Self::new(workspace_handle, window, cx)
-            })
-        });
-    }
-
-    pub fn new(
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let name_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
-        let command_editor = cx.new(|cx| {
-            SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
-        });
-
-        Self {
-            name_editor,
-            command_editor,
-            workspace,
-        }
-    }
-
-    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
-        let name = self
-            .name_editor
-            .read(cx)
-            .editor()
-            .read(cx)
-            .text(cx)
-            .trim()
-            .to_string();
-        let command = self
-            .command_editor
-            .read(cx)
-            .editor()
-            .read(cx)
-            .text(cx)
-            .trim()
-            .to_string();
-
-        if name.is_empty() || command.is_empty() {
-            return;
-        }
-
-        let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
-        let Some(path) = command_parts.next() else {
-            return;
-        };
-        let args = command_parts.collect::<Vec<_>>();
-
-        if let Some(workspace) = self.workspace.upgrade() {
-            workspace.update(cx, |workspace, cx| {
-                let fs = workspace.app_state().fs.clone();
-                update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
-                    settings.context_servers.insert(
-                        name.into(),
-                        ContextServerSettings::Custom {
-                            command: ContextServerCommand {
-                                path,
-                                args,
-                                env: None,
-                            },
-                        },
-                    );
-                });
-            });
-        }
-
-        cx.emit(DismissEvent);
-    }
-
-    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
-        cx.emit(DismissEvent);
-    }
-}
-
-impl ModalView for AddContextServerModal {}
-
-impl Focusable for AddContextServerModal {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.name_editor.focus_handle(cx).clone()
-    }
-}
-
-impl EventEmitter<DismissEvent> for AddContextServerModal {}
-
-impl Render for AddContextServerModal {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let is_name_empty = self.name_editor.read(cx).is_empty(cx);
-        let is_command_empty = self.command_editor.read(cx).is_empty(cx);
-
-        let focus_handle = self.focus_handle(cx);
-
-        div()
-            .elevation_3(cx)
-            .w(rems(34.))
-            .key_context("AddContextServerModal")
-            .on_action(
-                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
-            )
-            .on_action(
-                cx.listener(|this, _: &menu::Confirm, _window, cx| {
-                    this.confirm(&menu::Confirm, cx)
-                }),
-            )
-            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
-                this.focus_handle(cx).focus(window);
-            }))
-            .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
-            .child(
-                Modal::new("add-context-server", None)
-                    .header(ModalHeader::new().headline("Add MCP Server"))
-                    .section(
-                        Section::new().child(
-                            v_flex()
-                                .gap_2()
-                                .child(self.name_editor.clone())
-                                .child(self.command_editor.clone()),
-                        ),
-                    )
-                    .footer(
-                        ModalFooter::new().end_slot(
-                            h_flex()
-                                .gap_2()
-                                .child(
-                                    Button::new("cancel", "Cancel")
-                                        .key_binding(
-                                            KeyBinding::for_action_in(
-                                                &menu::Cancel,
-                                                &focus_handle,
-                                                window,
-                                                cx,
-                                            )
-                                            .map(|kb| kb.size(rems_from_px(12.))),
-                                        )
-                                        .on_click(cx.listener(|this, _event, _window, cx| {
-                                            this.cancel(&menu::Cancel, cx)
-                                        })),
-                                )
-                                .child(
-                                    Button::new("add-server", "Add Server")
-                                        .disabled(is_name_empty || is_command_empty)
-                                        .key_binding(
-                                            KeyBinding::for_action_in(
-                                                &menu::Confirm,
-                                                &focus_handle,
-                                                window,
-                                                cx,
-                                            )
-                                            .map(|kb| kb.size(rems_from_px(12.))),
-                                        )
-                                        .map(|button| {
-                                            if is_name_empty {
-                                                button.tooltip(Tooltip::text("Name is required"))
-                                            } else if is_command_empty {
-                                                button.tooltip(Tooltip::text("Command is required"))
-                                            } else {
-                                                button
-                                            }
-                                        })
-                                        .on_click(cx.listener(|this, _event, _window, cx| {
-                                            this.confirm(&menu::Confirm, cx)
-                                        })),
-                                ),
-                        ),
-                    ),
-            )
-    }
-}

crates/agent/src/agent_configuration/configure_context_server_modal.rs 🔗

@@ -3,215 +3,384 @@ use std::{
     time::Duration,
 };
 
-use anyhow::Context as _;
-use context_server::ContextServerId;
+use anyhow::{Context as _, Result};
+use context_server::{ContextServerCommand, ContextServerId};
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{
-    Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
-    TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
+    Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
+    FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
+    WeakEntity, percentage, prelude::*,
 };
 use language::{Language, LanguageRegistry};
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    context_server_store::{ContextServerStatus, ContextServerStore},
+    context_server_store::{
+        ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
+    },
     project_settings::{ContextServerSettings, ProjectSettings},
+    worktree_store::WorktreeStore,
 };
 use settings::{Settings as _, update_settings_file};
 use theme::ThemeSettings;
 use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
-use util::ResultExt;
+use util::ResultExt as _;
 use workspace::{ModalView, Workspace};
 
-pub(crate) struct ConfigureContextServerModal {
-    workspace: WeakEntity<Workspace>,
-    focus_handle: FocusHandle,
-    context_servers_to_setup: Vec<ContextServerSetup>,
-    context_server_store: Entity<ContextServerStore>,
+use crate::AddContextServer;
+
+enum ConfigurationTarget {
+    New,
+    Existing {
+        id: ContextServerId,
+        command: ContextServerCommand,
+    },
+    Extension {
+        id: ContextServerId,
+        repository_url: Option<SharedString>,
+        installation: Option<extension::ContextServerConfiguration>,
+    },
 }
 
-enum Configuration {
-    NotAvailable,
-    Required(ConfigurationRequiredState),
+enum ConfigurationSource {
+    New {
+        editor: Entity<Editor>,
+    },
+    Existing {
+        editor: Entity<Editor>,
+    },
+    Extension {
+        id: ContextServerId,
+        editor: Option<Entity<Editor>>,
+        repository_url: Option<SharedString>,
+        installation_instructions: Option<Entity<markdown::Markdown>>,
+        settings_validator: Option<jsonschema::Validator>,
+    },
 }
 
-struct ConfigurationRequiredState {
-    installation_instructions: Entity<markdown::Markdown>,
-    settings_validator: Option<jsonschema::Validator>,
-    settings_editor: Entity<Editor>,
-    last_error: Option<SharedString>,
-    waiting_for_context_server: bool,
-}
+impl ConfigurationSource {
+    fn has_configuration_options(&self) -> bool {
+        !matches!(self, ConfigurationSource::Extension { editor: None, .. })
+    }
 
-struct ContextServerSetup {
-    id: ContextServerId,
-    repository_url: Option<SharedString>,
-    configuration: Configuration,
-}
+    fn is_new(&self) -> bool {
+        matches!(self, ConfigurationSource::New { .. })
+    }
 
-impl ConfigureContextServerModal {
-    pub fn new(
-        configurations: impl Iterator<Item = crate::context_server_configuration::Configuration>,
-        context_server_store: Entity<ContextServerStore>,
-        jsonc_language: Option<Arc<Language>>,
+    fn from_target(
+        target: ConfigurationTarget,
         language_registry: Arc<LanguageRegistry>,
-        workspace: WeakEntity<Workspace>,
+        jsonc_language: Option<Arc<Language>>,
         window: &mut Window,
-        cx: &mut Context<Self>,
+        cx: &mut App,
     ) -> Self {
-        let context_servers_to_setup = configurations
-            .map(|config| match config {
-                crate::context_server_configuration::Configuration::NotAvailable(
-                    context_server_id,
-                    repository_url,
-                ) => ContextServerSetup {
-                    id: context_server_id,
-                    repository_url,
-                    configuration: Configuration::NotAvailable,
-                },
-                crate::context_server_configuration::Configuration::Required(
-                    context_server_id,
-                    repository_url,
-                    config,
-                ) => {
-                    let jsonc_language = jsonc_language.clone();
-                    let settings_validator = jsonschema::validator_for(&config.settings_schema)
-                        .context("Failed to load JSON schema for context server settings")
-                        .log_err();
-                    let state = ConfigurationRequiredState {
-                        installation_instructions: cx.new(|cx| {
-                            Markdown::new(
-                                config.installation_instructions.clone().into(),
-                                Some(language_registry.clone()),
-                                None,
-                                cx,
-                            )
-                        }),
-                        settings_validator,
-                        settings_editor: cx.new(|cx| {
-                            let mut editor = Editor::auto_height(1, 16, window, cx);
-                            editor.set_text(config.default_settings.trim(), window, cx);
-                            editor.set_show_gutter(false, cx);
-                            editor.set_soft_wrap_mode(
-                                language::language_settings::SoftWrap::None,
-                                cx,
-                            );
-                            if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
-                                buffer.update(cx, |buffer, cx| {
-                                    buffer.set_language(jsonc_language, cx)
-                                })
-                            }
-                            editor
-                        }),
-                        waiting_for_context_server: false,
-                        last_error: None,
-                    };
-                    ContextServerSetup {
-                        id: context_server_id,
-                        repository_url,
-                        configuration: Configuration::Required(state),
-                    }
+        fn create_editor(
+            json: String,
+            jsonc_language: Option<Arc<Language>>,
+            window: &mut Window,
+            cx: &mut App,
+        ) -> Entity<Editor> {
+            cx.new(|cx| {
+                let mut editor = Editor::auto_height(4, 16, window, cx);
+                editor.set_text(json, window, cx);
+                editor.set_show_gutter(false, cx);
+                editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
+                if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
+                    buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
                 }
+                editor
             })
-            .collect::<Vec<_>>();
+        }
+
+        match target {
+            ConfigurationTarget::New => ConfigurationSource::New {
+                editor: create_editor(context_server_input(None), jsonc_language, window, cx),
+            },
+            ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
+                editor: create_editor(
+                    context_server_input(Some((id, command))),
+                    jsonc_language,
+                    window,
+                    cx,
+                ),
+            },
+            ConfigurationTarget::Extension {
+                id,
+                repository_url,
+                installation,
+            } => {
+                let settings_validator = installation.as_ref().and_then(|installation| {
+                    jsonschema::validator_for(&installation.settings_schema)
+                        .context("Failed to load JSON schema for context server settings")
+                        .log_err()
+                });
+                let installation_instructions = installation.as_ref().map(|installation| {
+                    cx.new(|cx| {
+                        Markdown::new(
+                            installation.installation_instructions.clone().into(),
+                            Some(language_registry.clone()),
+                            None,
+                            cx,
+                        )
+                    })
+                });
+                ConfigurationSource::Extension {
+                    id,
+                    repository_url,
+                    installation_instructions,
+                    settings_validator,
+                    editor: installation.map(|installation| {
+                        create_editor(installation.default_settings, jsonc_language, window, cx)
+                    }),
+                }
+            }
+        }
+    }
 
-        Self {
-            workspace,
-            focus_handle: cx.focus_handle(),
-            context_servers_to_setup,
-            context_server_store,
+    fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
+        match self {
+            ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
+                parse_input(&editor.read(cx).text(cx))
+                    .map(|(id, command)| (id, ContextServerSettings::Custom { command }))
+            }
+            ConfigurationSource::Extension {
+                id,
+                editor,
+                settings_validator,
+                ..
+            } => {
+                let text = editor
+                    .as_ref()
+                    .context("No output available")?
+                    .read(cx)
+                    .text(cx);
+                let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
+                if let Some(settings_validator) = settings_validator {
+                    if let Err(error) = settings_validator.validate(&settings) {
+                        return Err(anyhow::anyhow!(error.to_string()));
+                    }
+                }
+                Ok((id.clone(), ContextServerSettings::Extension { settings }))
+            }
         }
     }
 }
 
-impl ConfigureContextServerModal {
-    pub fn confirm(&mut self, cx: &mut Context<Self>) {
-        if self.context_servers_to_setup.is_empty() {
-            self.dismiss(cx);
-            return;
+fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
+    let (name, path, args, env) = match existing {
+        Some((id, cmd)) => {
+            let args = serde_json::to_string(&cmd.args).unwrap();
+            let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
+            (id.0.to_string(), cmd.path, args, env)
         }
+        None => (
+            "some-mcp-server".to_string(),
+            "".to_string(),
+            "[]".to_string(),
+            "{}".to_string(),
+        ),
+    };
+
+    format!(
+        r#"{{
+  /// The name of your MCP server
+  "{name}": {{
+    "command": {{
+      /// The path to the executable
+      "path": "{path}",
+      /// The arguments to pass to the executable
+      "args": {args},
+      /// The environment variables to set for the executable
+      "env": {env}
+    }}
+  }}
+}}"#
+    )
+}
 
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
+fn resolve_context_server_extension(
+    id: ContextServerId,
+    worktree_store: Entity<WorktreeStore>,
+    cx: &mut App,
+) -> Task<Option<ConfigurationTarget>> {
+    let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
+
+    let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
+        return Task::ready(None);
+    };
+
+    let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
+    cx.spawn(async move |cx| {
+        let installation = descriptor
+            .configuration(worktree_store, cx)
+            .await
+            .context("Failed to resolve context server configuration")
+            .log_err()
+            .flatten();
+
+        Some(ConfigurationTarget::Extension {
+            id,
+            repository_url: extension
+                .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
+            installation,
+        })
+    })
+}
 
-        let id = self.context_servers_to_setup[0].id.clone();
-        let configuration = match &mut self.context_servers_to_setup[0].configuration {
-            Configuration::NotAvailable => {
-                self.context_servers_to_setup.remove(0);
-                if self.context_servers_to_setup.is_empty() {
-                    self.dismiss(cx);
-                }
-                return;
-            }
-            Configuration::Required(state) => state,
-        };
+enum State {
+    Idle,
+    Waiting,
+    Error(SharedString),
+}
 
-        configuration.last_error.take();
-        if configuration.waiting_for_context_server {
-            return;
-        }
+pub struct ConfigureContextServerModal {
+    context_server_store: Entity<ContextServerStore>,
+    workspace: WeakEntity<Workspace>,
+    source: ConfigurationSource,
+    state: State,
+}
 
-        let settings_value = match serde_json_lenient::from_str::<serde_json::Value>(
-            &configuration.settings_editor.read(cx).text(cx),
-        ) {
-            Ok(value) => value,
-            Err(error) => {
-                configuration.last_error = Some(error.to_string().into());
-                cx.notify();
-                return;
+impl ConfigureContextServerModal {
+    pub fn register(
+        workspace: &mut Workspace,
+        language_registry: Arc<LanguageRegistry>,
+        _window: Option<&mut Window>,
+        _cx: &mut Context<Workspace>,
+    ) {
+        workspace.register_action({
+            let language_registry = language_registry.clone();
+            move |_workspace, _: &AddContextServer, window, cx| {
+                let workspace_handle = cx.weak_entity();
+                let language_registry = language_registry.clone();
+                window
+                    .spawn(cx, async move |cx| {
+                        Self::show_modal(
+                            ConfigurationTarget::New,
+                            language_registry,
+                            workspace_handle,
+                            cx,
+                        )
+                        .await
+                    })
+                    .detach_and_log_err(cx);
             }
+        });
+    }
+
+    pub fn show_modal_for_existing_server(
+        server_id: ContextServerId,
+        language_registry: Arc<LanguageRegistry>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<()>> {
+        let Some(settings) = ProjectSettings::get_global(cx)
+            .context_servers
+            .get(&server_id.0)
+            .cloned()
+            .or_else(|| {
+                ContextServerDescriptorRegistry::default_global(cx)
+                    .read(cx)
+                    .context_server_descriptor(&server_id.0)
+                    .map(|_| ContextServerSettings::Extension {
+                        settings: serde_json::json!({}),
+                    })
+            })
+        else {
+            return Task::ready(Err(anyhow::anyhow!("Context server not found")));
         };
 
-        if let Some(validator) = configuration.settings_validator.as_ref() {
-            if let Err(error) = validator.validate(&settings_value) {
-                configuration.last_error = Some(error.to_string().into());
-                cx.notify();
-                return;
+        window.spawn(cx, async move |cx| {
+            let target = match settings {
+                ContextServerSettings::Custom { command } => Some(ConfigurationTarget::Existing {
+                    id: server_id,
+                    command,
+                }),
+                ContextServerSettings::Extension { .. } => {
+                    match workspace
+                        .update(cx, |workspace, cx| {
+                            resolve_context_server_extension(
+                                server_id,
+                                workspace.project().read(cx).worktree_store(),
+                                cx,
+                            )
+                        })
+                        .ok()
+                    {
+                        Some(task) => task.await,
+                        None => None,
+                    }
+                }
+            };
+
+            match target {
+                Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
+                None => Err(anyhow::anyhow!("Failed to resolve context server")),
             }
-        }
-        let id = id.clone();
+        })
+    }
 
-        let settings_changed = ProjectSettings::get_global(cx)
-            .context_servers
-            .get(&id.0)
-            .map_or(true, |settings| match settings {
-                ContextServerSettings::Custom { .. } => false,
-                ContextServerSettings::Extension { settings } => settings != &settings_value,
-            });
+    fn show_modal(
+        target: ConfigurationTarget,
+        language_registry: Arc<LanguageRegistry>,
+        workspace: WeakEntity<Workspace>,
+        cx: &mut AsyncWindowContext,
+    ) -> Task<Result<()>> {
+        cx.spawn(async move |cx| {
+            let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
+            workspace.update_in(cx, |workspace, window, cx| {
+                let workspace_handle = cx.weak_entity();
+                let context_server_store = workspace.project().read(cx).context_server_store();
+                workspace.toggle_modal(window, cx, |window, cx| Self {
+                    context_server_store,
+                    workspace: workspace_handle,
+                    state: State::Idle,
+                    source: ConfigurationSource::from_target(
+                        target,
+                        language_registry,
+                        jsonc_language,
+                        window,
+                        cx,
+                    ),
+                })
+            })
+        })
+    }
 
-        let is_running = self.context_server_store.read(cx).status_for_server(&id)
-            == Some(ContextServerStatus::Running);
+    fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
+        self.state = State::Error(err.into());
+        cx.notify();
+    }
 
-        if !settings_changed && is_running {
-            self.complete_setup(id, cx);
+    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
+        self.state = State::Idle;
+        let Some(workspace) = self.workspace.upgrade() else {
             return;
-        }
+        };
 
-        configuration.waiting_for_context_server = true;
+        let (id, settings) = match self.source.output(cx) {
+            Ok(val) => val,
+            Err(error) => {
+                self.set_error(error.to_string(), cx);
+                return;
+            }
+        };
 
-        let task = wait_for_context_server(&self.context_server_store, id.clone(), cx);
+        self.state = State::Waiting;
+        let wait_for_context_server_task =
+            wait_for_context_server(&self.context_server_store, id.clone(), cx);
         cx.spawn({
             let id = id.clone();
             async move |this, cx| {
-                let result = task.await;
+                let result = wait_for_context_server_task.await;
                 this.update(cx, |this, cx| match result {
                     Ok(_) => {
-                        this.complete_setup(id, cx);
+                        this.state = State::Idle;
+                        this.show_configured_context_server_toast(id, cx);
+                        cx.emit(DismissEvent);
                     }
                     Err(err) => {
-                        if let Some(setup) = this.context_servers_to_setup.get_mut(0) {
-                            match &mut setup.configuration {
-                                Configuration::NotAvailable => {}
-                                Configuration::Required(state) => {
-                                    state.last_error = Some(err.into());
-                                    state.waiting_for_context_server = false;
-                                }
-                            }
-                        } else {
-                            this.dismiss(cx);
-                        }
-                        cx.notify();
+                        this.set_error(err, cx);
                     }
                 })
             }
@@ -219,32 +388,24 @@ impl ConfigureContextServerModal {
         .detach();
 
         // When we write the settings to the file, the context server will be restarted.
-        update_settings_file::<ProjectSettings>(workspace.read(cx).app_state().fs.clone(), cx, {
-            let id = id.clone();
-            |settings, _| {
-                settings.context_servers.insert(
-                    id.0,
-                    ContextServerSettings::Extension {
-                        settings: settings_value,
-                    },
-                );
-            }
+        workspace.update(cx, |workspace, cx| {
+            let fs = workspace.app_state().fs.clone();
+            update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
+                project_settings.context_servers.insert(id.0, settings);
+            });
         });
     }
 
-    fn complete_setup(&mut self, id: ContextServerId, cx: &mut Context<Self>) {
-        self.context_servers_to_setup.remove(0);
-        cx.notify();
-
-        if !self.context_servers_to_setup.is_empty() {
-            return;
-        }
+    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
 
+    fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
         self.workspace
             .update(cx, {
                 |workspace, cx| {
                     let status_toast = StatusToast::new(
-                        format!("{} configured successfully.", id),
+                        format!("{} configured successfully.", id.0),
                         cx,
                         |this, _cx| {
                             this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
@@ -256,12 +417,264 @@ impl ConfigureContextServerModal {
                 }
             })
             .log_err();
+    }
+}
+
+fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
+    let value: serde_json::Value = serde_json_lenient::from_str(text)?;
+    let object = value.as_object().context("Expected object")?;
+    anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
+    let (context_server_name, value) = object.into_iter().next().unwrap();
+    let command = value.get("command").context("Expected command")?;
+    let command: ContextServerCommand = serde_json::from_value(command.clone())?;
+    Ok((ContextServerId(context_server_name.clone().into()), command))
+}
+
+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()
+                .map(|editor| editor.focus_handle(cx))
+                .unwrap_or_else(|| cx.focus_handle()),
+        }
+    }
+}
 
-        self.dismiss(cx);
+impl EventEmitter<DismissEvent> 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(),
+        };
+        ModalHeader::new().headline(text)
     }
 
-    fn dismiss(&self, cx: &mut Context<Self>) {
-        cx.emit(DismissEvent);
+    fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+        const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
+
+        if let ConfigurationSource::Extension {
+            installation_instructions: Some(installation_instructions),
+            ..
+        } = &self.source
+        {
+            div()
+                .pb_2()
+                .text_sm()
+                .child(MarkdownElement::new(
+                    installation_instructions.clone(),
+                    default_markdown_style(window, cx),
+                ))
+                .into_any_element()
+        } else {
+            Label::new(MODAL_DESCRIPTION)
+                .color(Color::Muted)
+                .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 {
+                    return Label::new(
+                        "No configuration options available for this context server. Visit the Repository for any further instructions.",
+                    )
+                    .color(Color::Muted).into_any_element();
+                };
+                editor
+            }
+        };
+
+        div()
+            .p_2()
+            .rounded_md()
+            .border_1()
+            .border_color(cx.theme().colors().border_variant)
+            .bg(cx.theme().colors().editor_background)
+            .child({
+                let settings = ThemeSettings::get_global(cx);
+                let text_style = TextStyle {
+                    color: cx.theme().colors().text,
+                    font_family: settings.buffer_font.family.clone(),
+                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
+                    font_size: settings.buffer_font_size(cx).into(),
+                    font_weight: settings.buffer_font.weight,
+                    line_height: relative(settings.buffer_line_height.value()),
+                    ..Default::default()
+                };
+                EditorElement::new(
+                    editor,
+                    EditorStyle {
+                        background: cx.theme().colors().editor_background,
+                        local_player: cx.theme().players().local(),
+                        text: text_style,
+                        syntax: cx.theme().syntax().clone(),
+                        ..Default::default()
+                    },
+                )
+            })
+            .into_any_element()
+    }
+
+    fn render_modal_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> ModalFooter {
+        let focus_handle = self.focus_handle(cx);
+        let is_connecting = matches!(self.state, State::Waiting);
+
+        ModalFooter::new()
+            .start_slot::<Button>(
+                if let ConfigurationSource::Extension {
+                    repository_url: Some(repository_url),
+                    ..
+                } = &self.source
+                {
+                    Some(
+                        Button::new("open-repository", "Open Repository")
+                            .icon(IconName::ArrowUpRight)
+                            .icon_color(Color::Muted)
+                            .icon_size(IconSize::XSmall)
+                            .tooltip({
+                                let repository_url = repository_url.clone();
+                                move |window, cx| {
+                                    Tooltip::with_meta(
+                                        "Open Repository",
+                                        None,
+                                        repository_url.clone(),
+                                        window,
+                                        cx,
+                                    )
+                                }
+                            })
+                            .on_click({
+                                let repository_url = repository_url.clone();
+                                move |_, _, cx| cx.open_url(&repository_url)
+                            }),
+                    )
+                } else {
+                    None
+                },
+            )
+            .end_slot(
+                h_flex()
+                    .gap_2()
+                    .child(
+                        Button::new(
+                            "cancel",
+                            if self.source.has_configuration_options() {
+                                "Cancel"
+                            } else {
+                                "Dismiss"
+                            },
+                        )
+                        .key_binding(
+                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
+                                .map(|kb| kb.size(rems_from_px(12.))),
+                        )
+                        .on_click(
+                            cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
+                        ),
+                    )
+                    .children(self.source.has_configuration_options().then(|| {
+                        Button::new(
+                            "add-server",
+                            if self.source.is_new() {
+                                "Add Server"
+                            } else {
+                                "Configure Server"
+                            },
+                        )
+                        .disabled(is_connecting)
+                        .key_binding(
+                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
+                                .map(|kb| kb.size(rems_from_px(12.))),
+                        )
+                        .on_click(
+                            cx.listener(|this, _event, _window, cx| {
+                                this.confirm(&menu::Confirm, cx)
+                            }),
+                        )
+                    })),
+            )
+    }
+
+    fn render_waiting_for_context_server() -> Div {
+        h_flex()
+            .gap_2()
+            .child(
+                Icon::new(IconName::ArrowCircle)
+                    .size(IconSize::XSmall)
+                    .color(Color::Info)
+                    .with_animation(
+                        "arrow-circle",
+                        Animation::new(Duration::from_secs(2)).repeat(),
+                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+                    )
+                    .into_any_element(),
+            )
+            .child(
+                Label::new("Waiting for Context Server")
+                    .size(LabelSize::Small)
+                    .color(Color::Muted),
+            )
+    }
+
+    fn render_modal_error(error: SharedString) -> Div {
+        h_flex()
+            .gap_2()
+            .child(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::XSmall)
+                    .color(Color::Warning),
+            )
+            .child(
+                div()
+                    .w_full()
+                    .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
+            )
+    }
+}
+
+impl Render for ConfigureContextServerModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .elevation_3(cx)
+            .w(rems(34.))
+            .key_context("ConfigureContextServerModal")
+            .on_action(
+                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
+            )
+            .on_action(
+                cx.listener(|this, _: &menu::Confirm, _window, cx| {
+                    this.confirm(&menu::Confirm, cx)
+                }),
+            )
+            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
+                this.focus_handle(cx).focus(window);
+            }))
+            .child(
+                Modal::new("configure-context-server", None)
+                    .header(self.render_modal_header())
+                    .section(
+                        Section::new()
+                            .child(self.render_modal_description(window, cx))
+                            .child(self.render_modal_content(cx))
+                            .child(match &self.state {
+                                State::Idle => div(),
+                                State::Waiting => Self::render_waiting_for_context_server(),
+                                State::Error(error) => Self::render_modal_error(error.clone()),
+                            }),
+                    )
+                    .footer(self.render_modal_footer(window, cx)),
+            )
     }
 }
 
@@ -309,199 +722,6 @@ fn wait_for_context_server(
     })
 }
 
-impl Render for ConfigureContextServerModal {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let Some(setup) = self.context_servers_to_setup.first() else {
-            return div().into_any_element();
-        };
-
-        let focus_handle = self.focus_handle(cx);
-
-        div()
-            .elevation_3(cx)
-            .w(rems(42.))
-            .key_context("ConfigureContextServerModal")
-            .track_focus(&focus_handle)
-            .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
-            .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)))
-            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
-                this.focus_handle(cx).focus(window);
-            }))
-            .child(
-                Modal::new("configure-context-server", None)
-                    .header(ModalHeader::new().headline(format!("Configure {}", setup.id)))
-                    .section(match &setup.configuration {
-                        Configuration::NotAvailable => Section::new().child(
-                            Label::new(
-                                "No configuration options available for this context server. Visit the Repository for any further instructions.",
-                            )
-                            .color(Color::Muted),
-                        ),
-                        Configuration::Required(configuration) => Section::new()
-                            .child(div().pb_2().text_sm().child(MarkdownElement::new(
-                                configuration.installation_instructions.clone(),
-                                default_markdown_style(window, cx),
-                            )))
-                            .child(
-                                div()
-                                    .p_2()
-                                    .rounded_md()
-                                    .border_1()
-                                    .border_color(cx.theme().colors().border_variant)
-                                    .bg(cx.theme().colors().editor_background)
-                                    .gap_1()
-                                    .child({
-                                        let settings = ThemeSettings::get_global(cx);
-                                        let text_style = TextStyle {
-                                            color: cx.theme().colors().text,
-                                            font_family: settings.buffer_font.family.clone(),
-                                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
-                                            font_size: settings.buffer_font_size(cx).into(),
-                                            font_weight: settings.buffer_font.weight,
-                                            line_height: relative(
-                                                settings.buffer_line_height.value(),
-                                            ),
-                                            ..Default::default()
-                                        };
-                                        EditorElement::new(
-                                            &configuration.settings_editor,
-                                            EditorStyle {
-                                                background: cx.theme().colors().editor_background,
-                                                local_player: cx.theme().players().local(),
-                                                text: text_style,
-                                                syntax: cx.theme().syntax().clone(),
-                                                ..Default::default()
-                                            },
-                                        )
-                                    })
-                                    .when_some(configuration.last_error.clone(), |this, error| {
-                                        this.child(
-                                            h_flex()
-                                                .gap_2()
-                                                .px_2()
-                                                .py_1()
-                                                .child(
-                                                    Icon::new(IconName::Warning)
-                                                        .size(IconSize::XSmall)
-                                                        .color(Color::Warning),
-                                                )
-                                                .child(
-                                                    div().w_full().child(
-                                                        Label::new(error)
-                                                            .size(LabelSize::Small)
-                                                            .color(Color::Muted),
-                                                    ),
-                                                ),
-                                        )
-                                    }),
-                            )
-                            .when(configuration.waiting_for_context_server, |this| {
-                                this.child(
-                                    h_flex()
-                                        .gap_1p5()
-                                        .child(
-                                            Icon::new(IconName::ArrowCircle)
-                                                .size(IconSize::XSmall)
-                                                .color(Color::Info)
-                                                .with_animation(
-                                                    "arrow-circle",
-                                                    Animation::new(Duration::from_secs(2)).repeat(),
-                                                    |icon, delta| {
-                                                        icon.transform(Transformation::rotate(
-                                                            percentage(delta),
-                                                        ))
-                                                    },
-                                                )
-                                                .into_any_element(),
-                                        )
-                                        .child(
-                                            Label::new("Waiting for Context Server")
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted),
-                                        ),
-                                )
-                            }),
-                    })
-                    .footer(
-                        ModalFooter::new()
-                            .when_some(setup.repository_url.clone(), |this, repository_url| {
-                                this.start_slot(
-                                    h_flex().w_full().child(
-                                        Button::new("open-repository", "Open Repository")
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_color(Color::Muted)
-                                            .icon_size(IconSize::XSmall)
-                                            .tooltip({
-                                                let repository_url = repository_url.clone();
-                                                move |window, cx| {
-                                                    Tooltip::with_meta(
-                                                        "Open Repository",
-                                                        None,
-                                                        repository_url.clone(),
-                                                        window,
-                                                        cx,
-                                                    )
-                                                }
-                                            })
-                                            .on_click(move |_, _, cx| cx.open_url(&repository_url)),
-                                    ),
-                                )
-                            })
-                            .end_slot(match &setup.configuration {
-                                Configuration::NotAvailable => Button::new("dismiss", "Dismiss")
-                                    .key_binding(
-                                        KeyBinding::for_action_in(
-                                            &menu::Cancel,
-                                            &focus_handle,
-                                            window,
-                                            cx,
-                                        )
-                                        .map(|kb| kb.size(rems_from_px(12.))),
-                                    )
-                                    .on_click(
-                                        cx.listener(|this, _event, _window, cx| this.dismiss(cx)),
-                                    )
-                                    .into_any_element(),
-                                Configuration::Required(state) => h_flex()
-                                    .gap_2()
-                                    .child(
-                                        Button::new("cancel", "Cancel")
-                                            .key_binding(
-                                                KeyBinding::for_action_in(
-                                                    &menu::Cancel,
-                                                    &focus_handle,
-                                                    window,
-                                                    cx,
-                                                )
-                                                .map(|kb| kb.size(rems_from_px(12.))),
-                                            )
-                                            .on_click(cx.listener(|this, _event, _window, cx| {
-                                                this.dismiss(cx)
-                                            })),
-                                    )
-                                    .child(
-                                        Button::new("configure-server", "Configure MCP")
-                                            .disabled(state.waiting_for_context_server)
-                                            .key_binding(
-                                                KeyBinding::for_action_in(
-                                                    &menu::Confirm,
-                                                    &focus_handle,
-                                                    window,
-                                                    cx,
-                                                )
-                                                .map(|kb| kb.size(rems_from_px(12.))),
-                                            )
-                                            .on_click(cx.listener(|this, _event, _window, cx| {
-                                                this.confirm(cx)
-                                            })),
-                                    )
-                                    .into_any_element(),
-                            }),
-                    ),
-            ).into_any_element()
-    }
-}
-
 pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
     let theme_settings = ThemeSettings::get_global(cx);
     let colors = cx.theme().colors();

crates/agent/src/agent_panel.rs 🔗

@@ -1184,8 +1184,17 @@ impl AgentPanel {
         let fs = self.fs.clone();
 
         self.set_active_view(ActiveView::Configuration, window, cx);
-        self.configuration =
-            Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
+        self.configuration = Some(cx.new(|cx| {
+            AgentConfiguration::new(
+                fs,
+                context_server_store,
+                tools,
+                self.language_registry.clone(),
+                self.workspace.clone(),
+                window,
+                cx,
+            )
+        }));
 
         if let Some(configuration) = self.configuration.as_ref() {
             self.configuration_subscription = Some(cx.subscribe_in(

crates/agent/src/context_server_configuration.rs 🔗

@@ -1,15 +1,11 @@
 use std::sync::Arc;
 
-use anyhow::Context as _;
 use context_server::ContextServerId;
-use extension::{ContextServerConfiguration, ExtensionManifest};
+use extension::ExtensionManifest;
 use fs::Fs;
-use gpui::Task;
+use gpui::WeakEntity;
 use language::LanguageRegistry;
-use project::{
-    context_server_store::registry::ContextServerDescriptorRegistry,
-    project_settings::ProjectSettings,
-};
+use project::project_settings::ProjectSettings;
 use settings::update_settings_file;
 use ui::prelude::*;
 use util::ResultExt;
@@ -27,12 +23,12 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
             cx.subscribe_in(extension_events, window, {
                 let language_registry = language_registry.clone();
                 let fs = fs.clone();
-                move |workspace, _, event, window, cx| match event {
+                move |_, _, event, window, cx| match event {
                     extension::Event::ExtensionInstalled(manifest) => {
                         show_configure_mcp_modal(
                             language_registry.clone(),
                             manifest,
-                            workspace,
+                            cx.weak_entity(),
                             window,
                             cx,
                         );
@@ -49,7 +45,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
                             show_configure_mcp_modal(
                                 language_registry.clone(),
                                 manifest,
-                                workspace,
+                                cx.weak_entity(),
                                 window,
                                 cx,
                             );
@@ -80,19 +76,10 @@ fn remove_context_server_settings(
     });
 }
 
-pub enum Configuration {
-    NotAvailable(ContextServerId, Option<SharedString>),
-    Required(
-        ContextServerId,
-        Option<SharedString>,
-        ContextServerConfiguration,
-    ),
-}
-
 fn show_configure_mcp_modal(
     language_registry: Arc<LanguageRegistry>,
     manifest: &Arc<ExtensionManifest>,
-    workspace: &mut Workspace,
+    workspace: WeakEntity<Workspace>,
     window: &mut Window,
     cx: &mut Context<'_, Workspace>,
 ) {
@@ -100,70 +87,30 @@ fn show_configure_mcp_modal(
         return;
     }
 
-    let context_server_store = workspace.project().read(cx).context_server_store();
-    let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
+    let ids = manifest.context_servers.keys().cloned().collect::<Vec<_>>();
+    if ids.is_empty() {
+        return;
+    }
 
-    let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
-    let worktree_store = workspace.project().read(cx).worktree_store();
-    let configuration_tasks = manifest
-        .context_servers
-        .keys()
-        .cloned()
-        .map({
-            |key| {
-                let Some(descriptor) = registry.context_server_descriptor(&key) else {
-                    return Task::ready(Configuration::NotAvailable(
-                        ContextServerId(key),
-                        repository.clone(),
-                    ));
+    window
+        .spawn(cx, async move |cx| {
+            for id in ids {
+                let Some(task) = cx
+                    .update(|window, cx| {
+                        ConfigureContextServerModal::show_modal_for_existing_server(
+                            ContextServerId(id.clone()),
+                            language_registry.clone(),
+                            workspace.clone(),
+                            window,
+                            cx,
+                        )
+                    })
+                    .ok()
+                else {
+                    continue;
                 };
-                cx.spawn({
-                    let repository_url = repository.clone();
-                    let worktree_store = worktree_store.clone();
-                    async move |_, cx| {
-                        let configuration = descriptor
-                            .configuration(worktree_store.clone(), &cx)
-                            .await
-                            .context("Failed to resolve context server configuration")
-                            .log_err()
-                            .flatten();
-
-                        match configuration {
-                            Some(config) => Configuration::Required(
-                                ContextServerId(key),
-                                repository_url,
-                                config,
-                            ),
-                            None => {
-                                Configuration::NotAvailable(ContextServerId(key), repository_url)
-                            }
-                        }
-                    }
-                })
+                task.await.log_err();
             }
         })
-        .collect::<Vec<_>>();
-
-    let jsonc_language = language_registry.language_for_name("jsonc");
-
-    cx.spawn_in(window, async move |this, cx| {
-        let configurations = futures::future::join_all(configuration_tasks).await;
-        let jsonc_language = jsonc_language.await.ok();
-
-        this.update_in(cx, |this, window, cx| {
-            let workspace = cx.entity().downgrade();
-            this.toggle_modal(window, cx, |window, cx| {
-                ConfigureContextServerModal::new(
-                    configurations.into_iter(),
-                    context_server_store,
-                    jsonc_language,
-                    language_registry,
-                    workspace,
-                    window,
-                    cx,
-                )
-            });
-        })
-    })
-    .detach();
+        .detach();
 }

crates/extension_host/src/extension_host.rs 🔗

@@ -838,7 +838,11 @@ impl ExtensionStore {
         self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx)
     }
 
-    pub fn uninstall_extension(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
+    pub fn uninstall_extension(
+        &mut self,
+        extension_id: Arc<str>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
         let extension_dir = self.installed_dir.join(extension_id.as_ref());
         let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
         let fs = self.fs.clone();
@@ -846,7 +850,7 @@ impl ExtensionStore {
         let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned();
 
         match self.outstanding_operations.entry(extension_id.clone()) {
-            btree_map::Entry::Occupied(_) => return,
+            btree_map::Entry::Occupied(_) => return Task::ready(Ok(())),
             btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
         };
 
@@ -894,7 +898,6 @@ impl ExtensionStore {
 
             anyhow::Ok(())
         })
-        .detach_and_log_err(cx)
     }
 
     pub fn install_dev_extension(

crates/extension_host/src/extension_store_test.rs 🔗

@@ -482,7 +482,9 @@ async fn test_extension_store(cx: &mut TestAppContext) {
     });
 
     store.update(cx, |store, cx| {
-        store.uninstall_extension("zed-ruby".into(), cx)
+        store
+            .uninstall_extension("zed-ruby".into(), cx)
+            .detach_and_log_err(cx);
     });
 
     cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -583,7 +583,7 @@ impl ExtensionsPage {
                                         let extension_id = extension.id.clone();
                                         move |_, _, cx| {
                                             ExtensionStore::global(cx).update(cx, |store, cx| {
-                                                store.uninstall_extension(extension_id.clone(), cx)
+                                                store.uninstall_extension(extension_id.clone(), cx).detach_and_log_err(cx);
                                             });
                                         }
                                     }),
@@ -983,7 +983,9 @@ impl ExtensionsPage {
                     move |_, _, cx| {
                         telemetry::event!("Extension Uninstalled", extension_id);
                         ExtensionStore::global(cx).update(cx, |store, cx| {
-                            store.uninstall_extension(extension_id.clone(), cx)
+                            store
+                                .uninstall_extension(extension_id.clone(), cx)
+                                .detach_and_log_err(cx);
                         });
                     }
                 }),

crates/icons/src/icons.rs 🔗

@@ -263,6 +263,8 @@ pub enum IconName {
     ZedAssistantFilled,
     ZedBurnMode,
     ZedBurnModeOn,
+    ZedMcpCustom,
+    ZedMcpExtension,
     ZedPredict,
     ZedPredictDisabled,
     ZedPredictDown,

crates/project/src/context_server_store.rs 🔗

@@ -235,6 +235,13 @@ impl ContextServerStore {
         self.servers.get(id).map(ContextServerStatus::from_state)
     }
 
+    pub fn configuration_for_server(
+        &self,
+        id: &ContextServerId,
+    ) -> Option<Arc<ContextServerConfiguration>> {
+        self.servers.get(id).map(|state| state.configuration())
+    }
+
     pub fn all_server_ids(&self) -> Vec<ContextServerId> {
         self.servers.keys().cloned().collect()
     }

crates/settings/src/settings_store.rs 🔗

@@ -1496,25 +1496,24 @@ fn replace_value_in_json_text(
                 if between_comma_and_key.trim().is_empty() {
                     removal_start = comma_pos;
                 }
-            } else {
-                // No preceding comma, check for trailing comma
-                if let Some(remaining_text) = text.get(existing_value_range.end..) {
-                    let mut chars = remaining_text.char_indices();
-                    while let Some((offset, ch)) = chars.next() {
-                        if ch == ',' {
-                            removal_end = existing_value_range.end + offset + 1;
-                            // Also consume whitespace after the comma
-                            while let Some((_, next_ch)) = chars.next() {
-                                if next_ch.is_whitespace() {
-                                    removal_end += next_ch.len_utf8();
-                                } else {
-                                    break;
-                                }
+            }
+
+            if let Some(remaining_text) = text.get(existing_value_range.end..) {
+                let mut chars = remaining_text.char_indices();
+                while let Some((offset, ch)) = chars.next() {
+                    if ch == ',' {
+                        removal_end = existing_value_range.end + offset + 1;
+                        // Also consume whitespace after the comma
+                        while let Some((_, next_ch)) = chars.next() {
+                            if next_ch.is_whitespace() {
+                                removal_end += next_ch.len_utf8();
+                            } else {
+                                break;
                             }
-                            break;
-                        } else if !ch.is_whitespace() {
-                            break;
                         }
+                        break;
+                    } else if !ch.is_whitespace() {
+                        break;
                     }
                 }
             }