Skip to content

Commit 1c337c3

Browse files
committed
feat: update TUI MCP Manager Modal to support remote registry
- Add RegistryEntry and RegistrySource types in registry.rs - Add From<RegistryServer> impl to convert remote entries - Add get_local_registry_entries() and get_remote_server_config() - Add RegistryLoadState enum in types.rs - Update SelectFromRegistry mode to include entries and load_state - Update handlers to work with new RegistryEntry structure - Update rendering to display category and required env indicators - Re-export remote registry types for future async loading
1 parent a84da11 commit 1c337c3

File tree

5 files changed

+184
-34
lines changed

5 files changed

+184
-34
lines changed

src/cortex-tui/src/modal/mcp_manager/handlers.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
//!
33
//! Contains all key event handling for the MCP Manager modal.
44
5-
use super::registry::{get_registry_server_config, get_registry_servers};
5+
use super::registry::{get_local_registry_entries, get_registry_server_config};
66
use super::state::McpManagerModal;
77
use super::types::{
88
AddHttpServerFocus, AddStdioServerFocus, McpMode, McpServerSource, McpStatus,
9+
RegistryLoadState,
910
};
1011
use crate::modal::{ModalAction, ModalResult};
1112
use crate::widgets::selection_list::SelectionResult;
@@ -95,10 +96,12 @@ impl McpManagerModal {
9596
};
9697
}
9798
McpServerSource::Registry => {
98-
// Go directly to registry selection
99+
// Go to registry selection with local entries preloaded
99100
self.mode = McpMode::SelectFromRegistry {
100101
selected: 0,
101102
search_query: String::new(),
103+
entries: get_local_registry_entries(),
104+
load_state: RegistryLoadState::Loaded,
102105
};
103106
}
104107
}
@@ -300,18 +303,25 @@ impl McpManagerModal {
300303
if let McpMode::SelectFromRegistry {
301304
ref mut selected,
302305
ref mut search_query,
306+
ref entries,
307+
load_state: _,
303308
} = self.mode
304309
{
305-
let registry_servers = get_registry_servers();
310+
// Filter entries based on search
306311
let filtered: Vec<_> = if search_query.is_empty() {
307-
registry_servers.iter().collect()
312+
entries.iter().collect()
308313
} else {
309314
let query_lower = search_query.to_lowercase();
310-
registry_servers
315+
entries
311316
.iter()
312-
.filter(|(name, desc)| {
313-
name.to_lowercase().contains(&query_lower)
314-
|| desc.to_lowercase().contains(&query_lower)
317+
.filter(|e| {
318+
e.name.to_lowercase().contains(&query_lower)
319+
|| e.description.to_lowercase().contains(&query_lower)
320+
|| e.tags.iter().any(|t| t.to_lowercase().contains(&query_lower))
321+
|| e.category
322+
.as_ref()
323+
.map(|c| c.to_lowercase().contains(&query_lower))
324+
.unwrap_or(false)
315325
})
316326
.collect()
317327
};
@@ -330,11 +340,10 @@ impl McpManagerModal {
330340
ModalResult::Continue
331341
}
332342
KeyCode::Enter => {
333-
if let Some((name, _)) = filtered.get(*selected) {
334-
// For registry servers, we use predefined commands
335-
let (command, args) = get_registry_server_config(name);
343+
if let Some(entry) = filtered.get(*selected) {
344+
let (command, args) = get_registry_server_config(&entry.name);
336345
return ModalResult::Action(ModalAction::AddMcpServer {
337-
name: name.to_string(),
346+
name: entry.name.clone(),
338347
command,
339348
args,
340349
});

src/cortex-tui/src/modal/mcp_manager/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ impl Modal for McpManagerModal {
5959
McpMode::SelectFromRegistry {
6060
selected: _,
6161
search_query,
62+
entries: _,
63+
load_state: _,
6264
} => {
6365
search_query.push_str(text);
6466
true

src/cortex-tui/src/modal/mcp_manager/registry.rs

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,67 @@
11
//! MCP Registry
22
//!
3-
//! Contains predefined MCP server configurations from the registry.
3+
//! Provides access to MCP server configurations from both local fallbacks
4+
//! and the remote registry at registry.cortex.foundation/mcp
45
5-
/// Get list of available servers from registry
6+
use cortex_engine::mcp::RegistryServer;
7+
8+
/// Registry entry for display in the TUI
9+
#[derive(Debug, Clone)]
10+
pub struct RegistryEntry {
11+
/// Server name
12+
pub name: String,
13+
/// Description
14+
pub description: String,
15+
/// Category
16+
pub category: Option<String>,
17+
/// Vendor
18+
pub vendor: Option<String>,
19+
/// Tags for search
20+
pub tags: Vec<String>,
21+
/// Whether stdio transport is available
22+
pub has_stdio: bool,
23+
/// Whether HTTP transport is available
24+
pub has_http: bool,
25+
/// Required environment variables
26+
pub required_env: Vec<String>,
27+
/// Source (local or remote)
28+
pub source: RegistrySource,
29+
}
30+
31+
/// Source of the registry entry
32+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33+
pub enum RegistrySource {
34+
/// Local fallback
35+
Local,
36+
/// Remote registry
37+
Remote,
38+
}
39+
40+
impl From<RegistryServer> for RegistryEntry {
41+
fn from(server: RegistryServer) -> Self {
42+
let required_env = server
43+
.install
44+
.stdio
45+
.as_ref()
46+
.map(|s| s.required_env.clone())
47+
.unwrap_or_default();
48+
49+
Self {
50+
name: server.name,
51+
description: server.description,
52+
category: server.category,
53+
vendor: server.vendor,
54+
tags: server.tags,
55+
has_stdio: server.install.stdio.is_some(),
56+
has_http: server.install.http.is_some(),
57+
required_env,
58+
source: RegistrySource::Remote,
59+
}
60+
}
61+
}
62+
63+
/// Get list of available servers from local fallback registry
64+
/// Returns (name, description) tuples for backwards compatibility
665
pub fn get_registry_servers() -> Vec<(&'static str, &'static str)> {
766
vec![
867
("filesystem", "File system operations and management"),
@@ -20,7 +79,7 @@ pub fn get_registry_servers() -> Vec<(&'static str, &'static str)> {
2079
]
2180
}
2281

23-
/// Get the command and args for a registry server
82+
/// Get the command and args for a local registry server
2483
pub fn get_registry_server_config(name: &str) -> (String, Vec<String>) {
2584
match name {
2685
"filesystem" => (
@@ -99,3 +158,49 @@ pub fn get_registry_server_config(name: &str) -> (String, Vec<String>) {
99158
_ => ("npx".to_string(), vec!["-y".to_string(), name.to_string()]),
100159
}
101160
}
161+
162+
/// Get registry entries as RegistryEntry structs
163+
pub fn get_local_registry_entries() -> Vec<RegistryEntry> {
164+
get_registry_servers()
165+
.into_iter()
166+
.map(|(name, description)| {
167+
// Determine required env vars based on server
168+
let required_env = match name {
169+
"github" => vec!["GITHUB_TOKEN".to_string()],
170+
"brave-search" => vec!["BRAVE_API_KEY".to_string()],
171+
"google-maps" => vec!["GOOGLE_MAPS_API_KEY".to_string()],
172+
"slack" => vec!["SLACK_TOKEN".to_string()],
173+
"postgres" => vec!["DATABASE_URL".to_string()],
174+
_ => vec![],
175+
};
176+
177+
RegistryEntry {
178+
name: name.to_string(),
179+
description: description.to_string(),
180+
category: None,
181+
vendor: None,
182+
tags: vec![],
183+
has_stdio: true,
184+
has_http: false,
185+
required_env,
186+
source: RegistrySource::Local,
187+
}
188+
})
189+
.collect()
190+
}
191+
192+
/// Convert a RegistryServer from remote to command/args for installation
193+
pub fn get_remote_server_config(server: &RegistryServer) -> Option<(String, Vec<String>)> {
194+
server
195+
.install
196+
.stdio
197+
.as_ref()
198+
.map(|stdio| (stdio.command.clone(), stdio.args.clone()))
199+
}
200+
201+
// Re-export types for convenience (allow unused imports since they're for API exposure)
202+
#[allow(unused_imports)]
203+
pub use cortex_engine::mcp::{
204+
HttpConfig as RemoteHttpConfig, McpRegistryClient, RegistryInstallConfig,
205+
RegistryServer as RemoteRegistryServer, StdioConfig as RemoteStdioConfig,
206+
};

src/cortex-tui/src/modal/mcp_manager/rendering.rs

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! Contains all rendering methods for the MCP Manager modal.
44
5-
use super::registry::get_registry_servers;
5+
use super::registry::RegistryEntry;
66
use super::state::McpManagerModal;
77
use super::types::{AddHttpServerFocus, AddStdioServerFocus, McpMode, McpStatus};
88
use crate::modal::render_search_bar;
@@ -507,6 +507,8 @@ impl McpManagerModal {
507507
if let McpMode::SelectFromRegistry {
508508
selected,
509509
ref search_query,
510+
ref entries,
511+
load_state: _,
510512
} = self.mode
511513
{
512514
Clear.render(area, buf);
@@ -548,21 +550,25 @@ impl McpManagerModal {
548550

549551
// Render server list
550552
let list_area = chunks[2];
551-
let registry_servers = get_registry_servers();
552-
let filtered: Vec<_> = if search_query.is_empty() {
553-
registry_servers.iter().collect()
553+
let filtered: Vec<&RegistryEntry> = if search_query.is_empty() {
554+
entries.iter().collect()
554555
} else {
555556
let query_lower = search_query.to_lowercase();
556-
registry_servers
557+
entries
557558
.iter()
558-
.filter(|(name, desc)| {
559-
name.to_lowercase().contains(&query_lower)
560-
|| desc.to_lowercase().contains(&query_lower)
559+
.filter(|e| {
560+
e.name.to_lowercase().contains(&query_lower)
561+
|| e.description.to_lowercase().contains(&query_lower)
562+
|| e.tags.iter().any(|t| t.to_lowercase().contains(&query_lower))
563+
|| e.category
564+
.as_ref()
565+
.map(|c| c.to_lowercase().contains(&query_lower))
566+
.unwrap_or(false)
561567
})
562568
.collect()
563569
};
564570

565-
for (i, (name, desc)) in filtered.iter().enumerate() {
571+
for (i, entry) in filtered.iter().enumerate() {
566572
if i as u16 >= list_area.height {
567573
break;
568574
}
@@ -584,18 +590,32 @@ impl McpManagerModal {
584590
} else {
585591
Style::default().fg(TEXT)
586592
};
587-
buf.set_string(list_area.x + 3, y, name, name_style);
588-
589-
// Description (on same line, after name)
590-
let desc_x = list_area.x + 3 + name.len() as u16 + 2;
591-
if desc_x < list_area.x + list_area.width {
592-
let max_desc_len = (list_area.x + list_area.width - desc_x) as usize;
593-
let truncated_desc = if desc.len() > max_desc_len {
594-
format!("{}...", &desc[..max_desc_len.saturating_sub(3)])
593+
buf.set_string(list_area.x + 3, y, &entry.name, name_style);
594+
595+
// Category indicator if available
596+
let mut desc_start_x = list_area.x + 3 + entry.name.len() as u16 + 1;
597+
if let Some(ref category) = entry.category {
598+
let cat_text = format!("[{}]", category);
599+
buf.set_string(desc_start_x, y, &cat_text, Style::default().fg(WARNING));
600+
desc_start_x += cat_text.len() as u16 + 1;
601+
}
602+
603+
// Required env indicator
604+
if !entry.required_env.is_empty() {
605+
let env_indicator = "●";
606+
buf.set_string(desc_start_x, y, env_indicator, Style::default().fg(WARNING));
607+
desc_start_x += 2;
608+
}
609+
610+
// Description (on same line, after name and indicators)
611+
if desc_start_x < list_area.x + list_area.width {
612+
let max_desc_len = (list_area.x + list_area.width - desc_start_x) as usize;
613+
let truncated_desc = if entry.description.len() > max_desc_len {
614+
format!("{}...", &entry.description[..max_desc_len.saturating_sub(3)])
595615
} else {
596-
desc.to_string()
616+
entry.description.clone()
597617
};
598-
buf.set_string(desc_x, y, &truncated_desc, Style::default().fg(TEXT_DIM));
618+
buf.set_string(desc_start_x, y, &truncated_desc, Style::default().fg(TEXT_DIM));
599619
}
600620
}
601621

src/cortex-tui/src/modal/mcp_manager/types.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ pub enum McpTransportType {
7878
Http,
7979
}
8080

81+
/// Registry loading state
82+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83+
pub enum RegistryLoadState {
84+
#[default]
85+
NotLoaded,
86+
Loading,
87+
Loaded,
88+
Error,
89+
}
90+
8191
// ============================================================================
8292
// MODE ENUMS
8393
// ============================================================================
@@ -113,6 +123,10 @@ pub enum McpMode {
113123
SelectFromRegistry {
114124
selected: usize,
115125
search_query: String,
126+
/// Cached registry entries (if loaded)
127+
entries: Vec<super::registry::RegistryEntry>,
128+
/// Loading state
129+
load_state: RegistryLoadState,
116130
},
117131
/// Confirming deletion
118132
ConfirmDelete { server_name: String },

0 commit comments

Comments
 (0)