diff --git a/Cargo.lock b/Cargo.lock
index f37877bf2f..27e91430c4 100644
--- a/Cargo.lock
+++ b/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",
diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_mcp_custom.svg
new file mode 100644
index 0000000000..6410a26fca
--- /dev/null
+++ b/assets/icons/zed_mcp_custom.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_mcp_extension.svg
new file mode 100644
index 0000000000..996e0c1920
--- /dev/null
+++ b/assets/icons/zed_mcp_extension.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml
index 66e4a5c78f..69edd5a189 100644
--- a/crates/agent/Cargo.toml
+++ b/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
diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs
index 94edf76e97..ff108e06cb 100644
--- a/crates/agent/src/agent.rs
+++ b/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();
}
diff --git a/crates/agent/src/agent_configuration.rs b/crates/agent/src/agent_configuration.rs
index 1091e16a5b..d5099721ed 100644
--- a/crates/agent/src/agent_configuration.rs
+++ b/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,
+ language_registry: Arc,
+ workspace: WeakEntity,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_store: Entity,
@@ -48,6 +56,8 @@ impl AgentConfiguration {
fs: Arc,
context_server_store: Entity,
tools: Entity,
+ language_registry: Arc,
+ workspace: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> 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::(
+ 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, Arc)> {
+ 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);
+}
diff --git a/crates/agent/src/agent_configuration/add_context_server_modal.rs b/crates/agent/src/agent_configuration/add_context_server_modal.rs
deleted file mode 100644
index d9eff2a0b3..0000000000
--- a/crates/agent/src/agent_configuration/add_context_server_modal.rs
+++ /dev/null
@@ -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,
- name_editor: Entity,
- command_editor: Entity,
-}
-
-impl AddContextServerModal {
- pub fn register(
- workspace: &mut Workspace,
- _window: Option<&mut Window>,
- _cx: &mut Context,
- ) {
- 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,
- window: &mut Window,
- cx: &mut Context,
- ) -> 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) {
- 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::>();
-
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- update_settings_file::(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) {
- 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 for AddContextServerModal {}
-
-impl Render for AddContextServerModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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)
- })),
- ),
- ),
- ),
- )
- }
-}
diff --git a/crates/agent/src/agent_configuration/configure_context_server_modal.rs b/crates/agent/src/agent_configuration/configure_context_server_modal.rs
index 923bb07992..651c63406e 100644
--- a/crates/agent/src/agent_configuration/configure_context_server_modal.rs
+++ b/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,
- focus_handle: FocusHandle,
- context_servers_to_setup: Vec,
- context_server_store: Entity,
+use crate::AddContextServer;
+
+enum ConfigurationTarget {
+ New,
+ Existing {
+ id: ContextServerId,
+ command: ContextServerCommand,
+ },
+ Extension {
+ id: ContextServerId,
+ repository_url: Option,
+ installation: Option,
+ },
}
-enum Configuration {
- NotAvailable,
- Required(ConfigurationRequiredState),
+enum ConfigurationSource {
+ New {
+ editor: Entity,
+ },
+ Existing {
+ editor: Entity,
+ },
+ Extension {
+ id: ContextServerId,
+ editor: Option>,
+ repository_url: Option,
+ installation_instructions: Option>,
+ settings_validator: Option,
+ },
}
-struct ConfigurationRequiredState {
- installation_instructions: Entity,
- settings_validator: Option,
- settings_editor: Entity,
- last_error: Option,
- 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,
- configuration: Configuration,
-}
+ fn is_new(&self) -> bool {
+ matches!(self, ConfigurationSource::New { .. })
+ }
-impl ConfigureContextServerModal {
- pub fn new(
- configurations: impl Iterator- ,
- context_server_store: Entity,
- jsonc_language: Option>,
+ fn from_target(
+ target: ConfigurationTarget,
language_registry: Arc,
- workspace: WeakEntity,
+ jsonc_language: Option>,
window: &mut Window,
- cx: &mut Context,
+ 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)
+ fn create_editor(
+ json: String,
+ jsonc_language: Option>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity {
+ 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
+ })
+ }
+
+ 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 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),
+ .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)
+ }),
+ }
+ }
+ }
+ }
+
+ 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::(&text)?;
+ if let Some(settings_validator) = settings_validator {
+ if let Err(error) = settings_validator.validate(&settings) {
+ return Err(anyhow::anyhow!(error.to_string()));
}
}
- })
- .collect::>();
-
- Self {
- workspace,
- focus_handle: cx.focus_handle(),
- context_servers_to_setup,
- context_server_store,
+ Ok((id.clone(), ContextServerSettings::Extension { settings }))
+ }
}
}
}
-impl ConfigureContextServerModal {
- pub fn confirm(&mut self, cx: &mut Context) {
- 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}
+ }}
+ }}
+}}"#
+ )
+}
+
+fn resolve_context_server_extension(
+ id: ContextServerId,
+ worktree_store: Entity,
+ cx: &mut App,
+) -> Task