agent: Don't connect to MCP servers when AI is globally disabled (#47857)

Oliver Azevedo Barnes and Bennet Bo Fenner created

Closes #46846

When `disable_ai: true` is set in user settings, Zed was still
connecting to configured MCP (context) servers and sending
initialization requests. This change adds checks for `DisableAiSettings`
in `ContextServerStore` to:

- Skip server connections when AI is disabled
- Disconnect from running servers when AI becomes disabled
- Connect to servers when AI is re-enabled
- Prevent registry changes from triggering connections while AI is
disabled

The fix tracks `ai_disabled` state to detect transitions and properly
manage server connections when AI is toggled.

Release Notes:

- Fixed Zed connecting to MCP servers when AI is disabled.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>

Change summary

crates/project/src/context_server_store.rs               |  31 ++
crates/project/tests/integration/context_server_store.rs | 113 +++++++++
2 files changed, 138 insertions(+), 6 deletions(-)

Detailed changes

crates/project/src/context_server_store.rs 🔗

@@ -222,6 +222,7 @@ pub struct ContextServerStore {
     update_servers_task: Option<Task<Result<()>>>,
     context_server_factory: Option<ContextServerFactory>,
     needs_server_update: bool,
+    ai_disabled: bool,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -377,23 +378,42 @@ impl ContextServerStore {
         cx: &mut Context<Self>,
     ) -> Self {
         let mut subscriptions = vec![cx.observe_global::<SettingsStore>(move |this, cx| {
+            let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
+            let ai_was_disabled = this.ai_disabled;
+            this.ai_disabled = ai_disabled;
+
             let settings =
                 &Self::resolve_project_settings(&this.worktree_store, cx).context_servers;
-            if &this.context_server_settings == settings {
+            let settings_changed = &this.context_server_settings != settings;
+
+            if settings_changed {
+                this.context_server_settings = settings.clone();
+            }
+
+            // When AI is disabled, stop all running servers
+            if ai_disabled {
+                let server_ids: Vec<_> = this.servers.keys().cloned().collect();
+                for id in server_ids {
+                    this.stop_server(&id, cx).log_err();
+                }
                 return;
             }
-            this.context_server_settings = settings.clone();
-            if maintain_server_loop {
+
+            // Trigger updates if AI was re-enabled or settings changed
+            if maintain_server_loop && (ai_was_disabled || settings_changed) {
                 this.available_context_servers_changed(cx);
             }
         })];
 
         if maintain_server_loop {
             subscriptions.push(cx.observe(&registry, |this, _registry, cx| {
-                this.available_context_servers_changed(cx);
+                if !DisableAiSettings::get_global(cx).disable_ai {
+                    this.available_context_servers_changed(cx);
+                }
             }));
         }
 
+        let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
         let mut this = Self {
             state,
             _subscriptions: subscriptions,
@@ -404,12 +424,13 @@ impl ContextServerStore {
             project: weak_project,
             registry,
             needs_server_update: false,
+            ai_disabled,
             servers: HashMap::default(),
             server_ids: Default::default(),
             update_servers_task: None,
             context_server_factory,
         };
-        if maintain_server_loop {
+        if maintain_server_loop && !DisableAiSettings::get_global(cx).disable_ai {
             this.available_context_servers_changed(cx);
         }
         this

crates/project/tests/integration/context_server_store.rs 🔗

@@ -8,10 +8,11 @@ use project::context_server_store::*;
 use project::project_settings::ContextServerSettings;
 use project::worktree_store::WorktreeStore;
 use project::{
-    FakeFs, Project, context_server_store::registry::ContextServerDescriptor,
+    DisableAiSettings, FakeFs, Project, context_server_store::registry::ContextServerDescriptor,
     project_settings::ProjectSettings,
 };
 use serde_json::json;
+use settings::settings_content::SaturatingBool;
 use settings::{ContextServerCommand, Settings, SettingsStore};
 use std::sync::Arc;
 use std::{cell::RefCell, path::PathBuf, rc::Rc};
@@ -553,6 +554,116 @@ async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) {
     }
 }
 
+#[gpui::test]
+async fn test_context_server_respects_disable_ai(cx: &mut TestAppContext) {
+    const SERVER_1_ID: &str = "mcp-1";
+
+    let server_1_id = ContextServerId(SERVER_1_ID.into());
+
+    // Set up SettingsStore with disable_ai: true in user settings BEFORE creating project
+    cx.update(|cx| {
+        let settings_store = SettingsStore::test(cx);
+        cx.set_global(settings_store);
+        DisableAiSettings::register(cx);
+        // Set disable_ai via user settings (not override_global) so it persists through recompute_values
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings(cx, |content| {
+                content.project.disable_ai = Some(SaturatingBool(true));
+            });
+        });
+    });
+
+    // Now create the project (ContextServerStore will see disable_ai = true)
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(path!("/test"), json!({"code.rs": ""})).await;
+    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
+
+    let executor = cx.executor();
+    let store = project.read_with(cx, |project, _| project.context_server_store());
+    store.update(cx, |store, _| {
+        store.set_context_server_factory(Box::new(move |id, _| {
+            Arc::new(ContextServer::new(
+                id.clone(),
+                Arc::new(create_fake_transport(id.0.to_string(), executor.clone())),
+            ))
+        }));
+    });
+
+    set_context_server_configuration(
+        vec![(
+            server_1_id.0.clone(),
+            settings::ContextServerSettingsContent::Stdio {
+                enabled: true,
+                remote: false,
+                command: ContextServerCommand {
+                    path: "somebinary".into(),
+                    args: vec!["arg".to_string()],
+                    env: None,
+                    timeout: None,
+                },
+            },
+        )],
+        cx,
+    );
+
+    cx.run_until_parked();
+
+    // Verify that no server started because AI is disabled
+    cx.update(|cx| {
+        assert_eq!(
+            store.read(cx).status_for_server(&server_1_id),
+            None,
+            "Server should not start when disable_ai is true"
+        );
+    });
+
+    // Enable AI and verify server starts
+    {
+        let _server_events = assert_server_events(
+            &store,
+            vec![
+                (server_1_id.clone(), ContextServerStatus::Starting),
+                (server_1_id.clone(), ContextServerStatus::Running),
+            ],
+            cx,
+        );
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings(cx, |content| {
+                    content.project.disable_ai = Some(SaturatingBool(false));
+                });
+            });
+        });
+        cx.run_until_parked();
+    }
+
+    // Disable AI again and verify server stops
+    {
+        let _server_events = assert_server_events(
+            &store,
+            vec![(server_1_id.clone(), ContextServerStatus::Stopped)],
+            cx,
+        );
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings(cx, |content| {
+                    content.project.disable_ai = Some(SaturatingBool(true));
+                });
+            });
+        });
+        cx.run_until_parked();
+    }
+
+    // Verify server is stopped
+    cx.update(|cx| {
+        assert_eq!(
+            store.read(cx).status_for_server(&server_1_id),
+            Some(ContextServerStatus::Stopped),
+            "Server should be stopped when disable_ai is true"
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_server_ids_includes_disabled_servers(cx: &mut TestAppContext) {
     const ENABLED_SERVER_ID: &str = "enabled-server";