mcp: Fix source property showing up as undefined in settings (#43417)

Follow up to #39021.

<img width="576" height="141" alt="image"
src="https://github.com/user-attachments/assets/c89885a4-e664-4614-9bb0-86442dff34ee"
/>

- Add migration to remove `source` tag because `ContextServerSettings`
is now untagged
- Fix typos in context server modal
- PR seems to have removed the `test_action_namespaces` test, which I
brought back in this PR

Release Notes:

- Fixed an issue where the `source` property of MCP settings would show
up as unrecognised
This commit is contained in:
Bennet Bo Fenner
2025-11-25 16:03:21 +00:00
committed by GitHub
parent 9cae39449a
commit ab80ef1845
12 changed files with 237 additions and 45 deletions
+1 -1
View File
@@ -2553,7 +2553,7 @@ fn setup_context_server(
let mut settings = ProjectSettings::get_global(cx).clone();
settings.context_servers.insert(
name.into(),
project::project_settings::ContextServerSettings::Custom {
project::project_settings::ContextServerSettings::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -182,7 +182,7 @@ impl ConfigurationSource {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: true,
command,
},
@@ -403,7 +403,7 @@ impl ConfigureContextServerModal {
window.spawn(cx, async move |cx| {
let target = match settings {
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: _,
command,
} => Some(ConfigurationTarget::Existing {
@@ -635,7 +635,6 @@ impl ConfigureContextServerModal {
}
fn render_modal_content(&self, cx: &App) -> AnyElement {
// All variants now use single editor approach
let editor = match &self.source {
ConfigurationSource::New { editor, .. } => editor,
ConfigurationSource::Existing { editor, .. } => editor,
@@ -712,12 +711,12 @@ impl ConfigureContextServerModal {
)
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
let label = if *is_http {
"Run command"
"Configure Local"
} else {
"Connect via HTTP"
"Configure Remote"
};
let tooltip = if *is_http {
"Configure an MCP serevr that runs on stdin/stdout."
"Configure an MCP server that runs on stdin/stdout."
} else {
"Configure an MCP server that you connect to over HTTP"
};
@@ -972,7 +972,7 @@ impl ExtensionImports for WasmState {
});
match settings {
project::project_settings::ContextServerSettings::Custom {
project::project_settings::ContextServerSettings::Stdio {
enabled: _,
command,
} => Ok(serde_json::to_string(&settings::ContextServerSettings {
+6
View File
@@ -147,3 +147,9 @@ pub(crate) mod m_2025_11_20 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_11_25 {
mod settings;
pub(crate) use settings::remove_context_server_source;
}
@@ -0,0 +1,17 @@
use anyhow::Result;
use serde_json::Value;
pub fn remove_context_server_source(settings: &mut Value) -> Result<()> {
if let Some(obj) = settings.as_object_mut() {
if let Some(context_servers) = obj.get_mut("context_servers") {
if let Some(servers) = context_servers.as_object_mut() {
for (_, server) in servers.iter_mut() {
if let Some(server_obj) = server.as_object_mut() {
server_obj.remove("source");
}
}
}
}
}
Ok(())
}
+49 -6
View File
@@ -223,6 +223,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_11_20::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_11_20,
),
MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
];
run_migrations(text, migrations)
}
@@ -1334,7 +1335,6 @@ mod tests {
r#"{
"context_servers": {
"some-mcp-server": {
"source": "custom",
"command": {
"path": "npx",
"args": [
@@ -1354,7 +1354,6 @@ mod tests {
r#"{
"context_servers": {
"some-mcp-server": {
"source": "custom",
"command": "npx",
"args": [
"-y",
@@ -1376,7 +1375,6 @@ mod tests {
r#"{
"context_servers": {
"server-with-extras": {
"source": "custom",
"command": {
"path": "/usr/bin/node",
"args": ["server.js"]
@@ -1389,7 +1387,6 @@ mod tests {
r#"{
"context_servers": {
"server-with-extras": {
"source": "custom",
"command": "/usr/bin/node",
"args": ["server.js"],
"settings": {}
@@ -1404,7 +1401,6 @@ mod tests {
r#"{
"context_servers": {
"simple-server": {
"source": "custom",
"command": {
"path": "simple-mcp-server"
}
@@ -1415,7 +1411,6 @@ mod tests {
r#"{
"context_servers": {
"simple-server": {
"source": "custom",
"command": "simple-mcp-server"
}
}
@@ -2311,4 +2306,52 @@ mod tests {
),
);
}
#[test]
fn test_remove_context_server_source() {
assert_migrate_settings(
&r#"
{
"context_servers": {
"extension_server": {
"source": "extension",
"settings": {
"foo": "bar"
}
},
"custom_server": {
"source": "custom",
"command": "foo",
"args": ["bar"],
"env": {
"FOO": "BAR"
}
},
}
}
"#
.unindent(),
Some(
&r#"
{
"context_servers": {
"extension_server": {
"settings": {
"foo": "bar"
}
},
"custom_server": {
"command": "foo",
"args": ["bar"],
"env": {
"FOO": "BAR"
}
},
}
}
"#
.unindent(),
),
);
}
}
+7 -7
View File
@@ -122,7 +122,7 @@ impl ContextServerConfiguration {
cx: &AsyncApp,
) -> Option<Self> {
match settings {
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: _,
command,
} => Some(ContextServerConfiguration::Custom { command }),
@@ -1003,7 +1003,7 @@ mod tests {
),
(
server_2_id.0.clone(),
settings::ContextServerSettingsContent::Custom {
settings::ContextServerSettingsContent::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -1044,7 +1044,7 @@ mod tests {
),
(
server_2_id.0.clone(),
settings::ContextServerSettingsContent::Custom {
settings::ContextServerSettingsContent::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -1127,7 +1127,7 @@ mod tests {
json!({"code.rs": ""}),
vec![(
SERVER_1_ID.into(),
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -1180,7 +1180,7 @@ mod tests {
set_context_server_configuration(
vec![(
server_1_id.0.clone(),
settings::ContextServerSettingsContent::Custom {
settings::ContextServerSettingsContent::Stdio {
enabled: false,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -1209,7 +1209,7 @@ mod tests {
set_context_server_configuration(
vec![(
server_1_id.0.clone(),
settings::ContextServerSettingsContent::Custom {
settings::ContextServerSettingsContent::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
@@ -1328,7 +1328,7 @@ mod tests {
}
fn dummy_server_settings() -> ContextServerSettings {
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
+19 -19
View File
@@ -117,7 +117,7 @@ pub struct GlobalLspSettings {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum ContextServerSettings {
Custom {
Stdio {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
enabled: bool,
@@ -125,16 +125,6 @@ pub enum ContextServerSettings {
#[serde(flatten)]
command: ContextServerCommand,
},
Extension {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
enabled: bool,
/// The settings for this context server specified by the extension.
///
/// Consult the documentation for the context server to see what settings
/// are supported.
settings: serde_json::Value,
},
Http {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
@@ -145,13 +135,23 @@ pub enum ContextServerSettings {
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
headers: HashMap<String, String>,
},
Extension {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
enabled: bool,
/// The settings for this context server specified by the extension.
///
/// Consult the documentation for the context server to see what settings
/// are supported.
settings: serde_json::Value,
},
}
impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
fn from(value: settings::ContextServerSettingsContent) -> Self {
match value {
settings::ContextServerSettingsContent::Custom { enabled, command } => {
ContextServerSettings::Custom { enabled, command }
settings::ContextServerSettingsContent::Stdio { enabled, command } => {
ContextServerSettings::Stdio { enabled, command }
}
settings::ContextServerSettingsContent::Extension { enabled, settings } => {
ContextServerSettings::Extension { enabled, settings }
@@ -171,8 +171,8 @@ impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
fn into(self) -> settings::ContextServerSettingsContent {
match self {
ContextServerSettings::Custom { enabled, command } => {
settings::ContextServerSettingsContent::Custom { enabled, command }
ContextServerSettings::Stdio { enabled, command } => {
settings::ContextServerSettingsContent::Stdio { enabled, command }
}
ContextServerSettings::Extension { enabled, settings } => {
settings::ContextServerSettingsContent::Extension { enabled, settings }
@@ -200,17 +200,17 @@ impl ContextServerSettings {
pub fn enabled(&self) -> bool {
match self {
ContextServerSettings::Custom { enabled, .. } => *enabled,
ContextServerSettings::Extension { enabled, .. } => *enabled,
ContextServerSettings::Stdio { enabled, .. } => *enabled,
ContextServerSettings::Http { enabled, .. } => *enabled,
ContextServerSettings::Extension { enabled, .. } => *enabled,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
match self {
ContextServerSettings::Custom { enabled: e, .. } => *e = enabled,
ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
ContextServerSettings::Stdio { enabled: e, .. } => *e = enabled,
ContextServerSettings::Http { enabled: e, .. } => *e = enabled,
ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
}
}
}
@@ -192,7 +192,7 @@ pub struct SessionSettingsContent {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)]
#[serde(untagged, rename_all = "snake_case")]
pub enum ContextServerSettingsContent {
Custom {
Stdio {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
enabled: bool,
@@ -225,7 +225,7 @@ pub enum ContextServerSettingsContent {
impl ContextServerSettingsContent {
pub fn set_enabled(&mut self, enabled: bool) {
match self {
ContextServerSettingsContent::Custom {
ContextServerSettingsContent::Stdio {
enabled: custom_enabled,
..
} => {
+1 -1
View File
@@ -568,7 +568,7 @@ impl VsCodeSettings {
.filter_map(|(k, v)| {
Some((
k.clone().into(),
ContextServerSettingsContent::Custom {
ContextServerSettingsContent::Stdio {
enabled: true,
command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
.ok()
+127
View File
@@ -4686,6 +4686,133 @@ mod tests {
});
}
/// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
/// and let you know when introducing a new namespace.
#[gpui::test]
async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
use itertools::Itertools;
init_keymap_test(cx);
cx.update(|cx| {
let all_actions = cx.all_action_names();
let mut actions_without_namespace = Vec::new();
let all_namespaces = all_actions
.iter()
.filter_map(|action_name| {
let namespace = action_name
.split("::")
.collect::<Vec<_>>()
.into_iter()
.rev()
.skip(1)
.rev()
.join("::");
if namespace.is_empty() {
actions_without_namespace.push(*action_name);
}
if &namespace == "test_only" || &namespace == "stories" {
None
} else {
Some(namespace)
}
})
.sorted()
.dedup()
.collect::<Vec<_>>();
assert_eq!(actions_without_namespace, Vec::<&str>::new());
let expected_namespaces = vec![
"action",
"activity_indicator",
"agent",
#[cfg(not(target_os = "macos"))]
"app_menu",
"assistant",
"assistant2",
"auto_update",
"bedrock",
"branches",
"buffer_search",
"channel_modal",
"cli",
"client",
"collab",
"collab_panel",
"command_palette",
"console",
"context_server",
"copilot",
"debug_panel",
"debugger",
"dev",
"diagnostics",
"edit_prediction",
"editor",
"feedback",
"file_finder",
"git",
"git_onboarding",
"git_panel",
"go_to_line",
"icon_theme_selector",
"journal",
"keymap_editor",
"keystroke_input",
"language_selector",
"line_ending_selector",
"lsp_tool",
"markdown",
"menu",
"notebook",
"notification_panel",
"onboarding",
"outline",
"outline_panel",
"pane",
"panel",
"picker",
"project_panel",
"project_search",
"project_symbols",
"projects",
"repl",
"rules_library",
"search",
"settings_editor",
"settings_profile_selector",
"snippets",
"stash_picker",
"supermaven",
"svg",
"syntax_tree_view",
"tab_switcher",
"task",
"terminal",
"terminal_panel",
"theme_selector",
"toast",
"toolchain",
"variable_list",
"vim",
"window",
"workspace",
"zed",
"zed_actions",
"zed_predict_onboarding",
"zeta",
];
assert_eq!(
all_namespaces,
expected_namespaces
.into_iter()
.map(|namespace| namespace.to_string())
.sorted()
.collect::<Vec<_>>()
);
});
}
#[gpui::test]
fn test_bundled_settings_and_themes(cx: &mut App) {
cx.text_system()
+2 -2
View File
@@ -40,12 +40,12 @@ You can connect them by adding their commands directly to your `settings.json`,
```json [settings]
{
"context_servers": {
"run-command": {
"local-mcp-server": {
"command": "some-command",
"args": ["arg-1", "arg-2"],
"env": {}
},
"over-http": {
"remote-mcp-server": {
"url": "custom",
"headers": { "Authorization": "Bearer <token>" }
}