Skip to content

Commit 4bff3ba

Browse files
authored
Expand TUI MCP catalog and discovery (#14500)
## Description Follows corrective PR #14499, which has merged. Expands the TUI `/mcp` experience so users can discover and explicitly enable MCP servers from the same sources represented by Warp's broader MCP catalog. - Shows TUI-local installations, synced templates, gallery entries, Warp-global definitions, third-party global definitions, and current-repository definitions in one searchable list. - Labels entries with source and scope, preserves same-named file definitions from distinct providers, and reports diagnostics for multiple config files. - Represents uninstalled synced and gallery entries as `available`; catalog refresh never installs, starts, or initiates OAuth for them. - Adds a TUI-native enable flow for template variables and final confirmation. Installation and startup happen only after confirmation, and cancellation leaves catalog/runtime state unchanged. - Detects Claude, Codex, `.agents`, and current-repository MCP configs in place without copying them into Warp's global config. - Keeps third-party and repository definitions offline by default in TUI while preserving existing Warp-global activation and GUI auto-start behavior. ## Linked Issue N/A ## Testing - [x] I have manually tested my changes locally with `./script/run-tui` - [x] Live `/mcp` verification in a real terminal: - Confirmed synced entries render as `available` with `Enter to enable`. - Confirmed enabling opens a separate `Enable and start` confirmation screen. - Canceled the flow and verified counts remained `2 connected · 1 failed · 1 offline · 26 available`. - Confirmed a current-repository Claude MCP appears as offline and is not auto-started. - [x] `cargo nextest run -p warp_tui` (823 passed) - [x] Focused `warp` MCP tests (29 passed; 6016 skipped) - [x] `cargo check -p warp_tui` - [x] Verified this child reapplies the exact parent-revert delta and its tip is tree-identical to `origin/master`. - [x] `./script/format --check` - [x] `cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warnings` - [x] `cargo clippy -p warp --all-targets --tests -- -D warnings` - [x] `cargo clippy -p warp_completer --all-targets --tests -- -D warnings` - [x] `git diff --check` The full workspace test suite was not rerun for this stack reconstruction. ### Screenshots / Videos N/A — the interaction was validated directly in the terminal with `./script/run-tui`. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode - [Agent conversation](https://staging.warp.dev/conversation/1592c723-6dbc-49fa-a6c8-1b0cf9699f1d) CHANGELOG-IMPROVEMENT: Expanded TUI `/mcp` discovery and added explicit opt-in installation for synced and gallery MCP servers.
1 parent ddadcee commit 4bff3ba

28 files changed

Lines changed: 2342 additions & 330 deletions

app/src/ai/mcp/file_based_manager.rs

Lines changed: 117 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -298,20 +298,13 @@ impl FileBasedMCPManager {
298298
/// global, even if they also happen to be referenced from a global location (in which
299299
/// case this returns `true` due to the global reference).
300300
fn is_global_server(&self, hash: u64) -> bool {
301-
let home_dir = dirs::home_dir();
302301
self.file_based_servers_by_root
303302
.iter()
304303
.any(|(root_path, provider_map)| {
305304
provider_map.iter().any(|(provider, hashes)| {
306-
if !hashes.contains(&hash) {
307-
return false;
308-
}
309-
match provider {
310-
MCPProvider::Warp => Self::is_global_warp_root(root_path),
311-
MCPProvider::Claude | MCPProvider::Codex | MCPProvider::Agents => {
312-
home_dir.as_ref().is_some_and(|home| root_path == home)
313-
}
314-
}
305+
hashes.contains(&hash)
306+
&& Self::scope_for_source(root_path, *provider)
307+
== FileBasedMCPServerScope::Global
315308
})
316309
})
317310
}
@@ -332,6 +325,28 @@ impl FileBasedMCPManager {
332325
fn is_global_warp_root(root_path: &Path) -> bool {
333326
warp_managed_mcp_config_path().is_some_and(|path| root_path == path.root_path.as_path())
334327
}
328+
329+
fn scope_for_source(root_path: &Path, provider: MCPProvider) -> FileBasedMCPServerScope {
330+
match provider {
331+
MCPProvider::Warp => {
332+
if Self::is_global_warp_root(root_path) {
333+
FileBasedMCPServerScope::Global
334+
} else {
335+
FileBasedMCPServerScope::Project
336+
}
337+
}
338+
MCPProvider::Claude | MCPProvider::Codex | MCPProvider::Agents => {
339+
if dirs::home_dir()
340+
.as_ref()
341+
.is_some_and(|home| root_path == home)
342+
{
343+
FileBasedMCPServerScope::Global
344+
} else {
345+
FileBasedMCPServerScope::Project
346+
}
347+
}
348+
}
349+
}
335350
fn auto_start_decision(&self, hash: u64, file_based_mcp_enabled: bool) -> AutoStartDecision {
336351
let server_type = if self.is_global_warp_server(hash) {
337352
FileBasedMCPServerType::GlobalWarp
@@ -342,7 +357,9 @@ impl FileBasedMCPManager {
342357
};
343358
let should_autostart = match server_type {
344359
FileBasedMCPServerType::GlobalWarp => true,
345-
FileBasedMCPServerType::GlobalThirdParty => file_based_mcp_enabled,
360+
FileBasedMCPServerType::GlobalThirdParty => {
361+
!self.defer_global_warp_autostart && file_based_mcp_enabled
362+
}
346363
FileBasedMCPServerType::ProjectScoped => false,
347364
};
348365

@@ -460,6 +477,12 @@ impl FileBasedMCPManager {
460477
}
461478

462479
fn handle_file_based_mcp_enabled_change(&mut self, ctx: &mut ModelContext<Self>) {
480+
// The setting is GUI-only. TUI-discovered third-party servers always
481+
// require an explicit start action, even if a value is loaded into the
482+
// shared model by tests or future settings migrations.
483+
if self.defer_global_warp_autostart {
484+
return;
485+
}
463486
// Only global third-party servers are affected by the toggle:
464487
// - Global Warp servers always spawn regardless of the toggle.
465488
// - Project-scoped servers (any provider) are never auto-spawned and their
@@ -497,12 +520,21 @@ impl FileBasedMCPManager {
497520
.find(|(_, server)| server.uuid() == installation_uuid)
498521
.map(|(hash, _)| *hash)
499522
}
523+
/// Returns owned snapshots of every current file-config diagnostic.
500524
#[cfg(any(feature = "tui", test))]
501-
pub fn config_diagnostic(&self, config_path: &Path) -> Option<&FileMCPConfigDiagnostic> {
502-
self.config_diagnostics_by_path.get(config_path)
525+
pub fn config_diagnostics(&self) -> Vec<FileMCPConfigDiagnostic> {
526+
self.config_diagnostics_by_path
527+
.values()
528+
.cloned()
529+
.sorted_by(|left, right| {
530+
left.config_path.cmp(&right.config_path).then_with(|| {
531+
provider_sort_key(left.provider).cmp(&provider_sort_key(right.provider))
532+
})
533+
})
534+
.collect()
503535
}
504536

505-
#[cfg(feature = "tui")]
537+
#[cfg(any(feature = "tui", test))]
506538
pub fn global_warp_servers(&self) -> Vec<&TemplatableMCPServerInstallation> {
507539
self.file_based_servers
508540
.iter()
@@ -511,17 +543,7 @@ impl FileBasedMCPManager {
511543
.collect()
512544
}
513545

514-
#[cfg(feature = "tui")]
515-
pub fn global_warp_installation_by_hash(
516-
&self,
517-
hash: u64,
518-
) -> Option<&TemplatableMCPServerInstallation> {
519-
self.is_global_warp_server(hash)
520-
.then(|| self.file_based_servers.get(&hash))
521-
.flatten()
522-
}
523-
524-
#[cfg(feature = "tui")]
546+
#[cfg(any(feature = "tui", test))]
525547
pub fn activate_global_warp_servers(&mut self, ctx: &mut ModelContext<Self>) {
526548
if self.global_warp_servers_activated {
527549
return;
@@ -541,6 +563,46 @@ impl FileBasedMCPManager {
541563
pub fn file_based_servers(&self) -> Vec<&TemplatableMCPServerInstallation> {
542564
self.file_based_servers.values().collect()
543565
}
566+
/// Returns owned file-based installations with every config source that
567+
/// currently references each installation.
568+
#[cfg(any(feature = "tui", test))]
569+
pub fn file_based_servers_with_sources(&self) -> Vec<FileBasedMCPServerWithSources> {
570+
self.file_based_servers
571+
.iter()
572+
.sorted_by_key(|(hash, _)| **hash)
573+
.map(|(hash, installation)| {
574+
let mut sources = self
575+
.file_based_servers_by_root
576+
.iter()
577+
.flat_map(|(root_path, provider_map)| {
578+
provider_map
579+
.iter()
580+
.filter(|(_, hashes)| hashes.contains(hash))
581+
.map(|(provider, _)| FileBasedMCPServerSource {
582+
provider: *provider,
583+
root_path: root_path.clone(),
584+
scope: Self::scope_for_source(root_path, *provider),
585+
})
586+
})
587+
.collect_vec();
588+
sources.sort_by(|left, right| {
589+
left.root_path.cmp(&right.root_path).then_with(|| {
590+
provider_sort_key(left.provider).cmp(&provider_sort_key(right.provider))
591+
})
592+
});
593+
FileBasedMCPServerWithSources {
594+
installation: installation.clone(),
595+
sources,
596+
}
597+
})
598+
.collect()
599+
}
600+
601+
/// Returns a file-based installation by its stable content hash.
602+
#[cfg(any(feature = "tui", test))]
603+
pub fn installation_by_hash(&self, hash: u64) -> Option<&TemplatableMCPServerInstallation> {
604+
self.file_based_servers.get(&hash)
605+
}
544606

545607
/// Returns the installation with the given UUID, if any.
546608
pub fn get_installation_by_uuid(
@@ -608,6 +670,36 @@ impl FileBasedMCPManager {
608670
}
609671
}
610672

673+
#[cfg(any(feature = "tui", test))]
674+
fn provider_sort_key(provider: MCPProvider) -> u8 {
675+
match provider {
676+
MCPProvider::Warp => 0,
677+
MCPProvider::Claude => 1,
678+
MCPProvider::Codex => 2,
679+
MCPProvider::Agents => 3,
680+
}
681+
}
682+
683+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
684+
pub enum FileBasedMCPServerScope {
685+
Global,
686+
Project,
687+
}
688+
689+
#[cfg(any(feature = "tui", test))]
690+
#[derive(Clone, Debug, Eq, PartialEq)]
691+
pub struct FileBasedMCPServerSource {
692+
pub provider: MCPProvider,
693+
pub root_path: PathBuf,
694+
pub scope: FileBasedMCPServerScope,
695+
}
696+
697+
#[cfg(any(feature = "tui", test))]
698+
#[derive(Clone, Debug)]
699+
pub struct FileBasedMCPServerWithSources {
700+
pub installation: TemplatableMCPServerInstallation,
701+
pub sources: Vec<FileBasedMCPServerSource>,
702+
}
611703
struct AutoStartDecision {
612704
should_autostart: bool,
613705
server_type: FileBasedMCPServerType,

0 commit comments

Comments
 (0)