Skip to content

Commit 1358370

Browse files
echobtfactorydroid
andauthored
feat(mcp): add multi-step flow for adding MCP servers (#240)
Implement a guided multi-step process when adding MCP servers: Step 1: Choose source - Custom Server (manual configuration) - From Registry (placeholder for future integration) Step 2: Choose transport type (for Custom) - stdio (local process with command/args) - HTTP (remote server with URL/API key) Step 3: Fill appropriate form - stdio form: name, command, args - HTTP form: name, URL, API key (optional) Changes: - Add build_mcp_source_selector() for source selection - Add build_mcp_transport_selector() for transport type selection - Add build_mcp_stdio_form() and build_mcp_http_form() - Add build_mcp_registry_browser() placeholder - Wire up multi-step navigation in event_loop.rs - Update form handlers for both server types Co-authored-by: Droid Agent <droid@factory.ai>
1 parent f6f55c3 commit 1358370

3 files changed

Lines changed: 186 additions & 16 deletions

File tree

cortex-tui/src/interactive/builders/mcp.rs

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,55 @@ pub fn build_mcp_selector(servers: &[McpServerInfo]) -> InteractiveState {
7676
])
7777
}
7878

79-
/// Build an inline form for adding a new MCP server.
80-
/// This form is displayed within the MCP panel, not as a separate modal.
81-
pub fn build_mcp_add_server_form() -> InlineFormState {
82-
InlineFormState::new("Add MCP Server", "mcp-add")
79+
/// Build a selector for choosing MCP server source (Custom or Registry).
80+
/// This is the first step when adding an MCP server.
81+
pub fn build_mcp_source_selector() -> InteractiveState {
82+
let items = vec![
83+
InteractiveItem::new("custom", "Custom Server")
84+
.with_description("Configure a server with command/URL manually")
85+
.with_shortcut('c'),
86+
InteractiveItem::new("registry", "From Registry")
87+
.with_description("Browse and install from MCP server registry")
88+
.with_shortcut('r'),
89+
];
90+
91+
InteractiveState::new(
92+
"Add MCP Server",
93+
items,
94+
InteractiveAction::Custom("mcp-source".to_string()),
95+
)
96+
.with_hints(vec![
97+
("Enter".to_string(), "select".to_string()),
98+
("Esc".to_string(), "back".to_string()),
99+
])
100+
}
101+
102+
/// Build a selector for choosing MCP transport type (stdio or HTTP).
103+
/// This is shown after selecting "Custom Server".
104+
pub fn build_mcp_transport_selector() -> InteractiveState {
105+
let items = vec![
106+
InteractiveItem::new("stdio", "stdio (Local Process)")
107+
.with_description("Run a local command (npx, uvx, binary)")
108+
.with_shortcut('s'),
109+
InteractiveItem::new("http", "HTTP (Remote Server)")
110+
.with_description("Connect to a remote MCP server via HTTP/SSE")
111+
.with_shortcut('h'),
112+
];
113+
114+
InteractiveState::new(
115+
"Transport Type",
116+
items,
117+
InteractiveAction::Custom("mcp-transport".to_string()),
118+
)
119+
.with_hints(vec![
120+
("Enter".to_string(), "select".to_string()),
121+
("Esc".to_string(), "back".to_string()),
122+
])
123+
}
124+
125+
/// Build an inline form for adding a stdio MCP server.
126+
pub fn build_mcp_stdio_form() -> InlineFormState {
127+
InlineFormState::new("Add stdio Server", "mcp-add-stdio")
83128
.with_field(
84129
InlineFormField::new("name", "Name")
85130
.required()
@@ -93,6 +138,45 @@ pub fn build_mcp_add_server_form() -> InlineFormState {
93138
.with_field(InlineFormField::new("args", "Args").with_placeholder("arg1 arg2 ..."))
94139
}
95140

141+
/// Build an inline form for adding an HTTP MCP server.
142+
pub fn build_mcp_http_form() -> InlineFormState {
143+
InlineFormState::new("Add HTTP Server", "mcp-add-http")
144+
.with_field(
145+
InlineFormField::new("name", "Name")
146+
.required()
147+
.with_placeholder("server-name"),
148+
)
149+
.with_field(
150+
InlineFormField::new("url", "URL")
151+
.required()
152+
.with_placeholder("https://api.example.com/mcp"),
153+
)
154+
.with_field(InlineFormField::new("api_key", "API Key").with_placeholder("optional"))
155+
}
156+
157+
/// Build a selector for browsing MCP registry (placeholder).
158+
pub fn build_mcp_registry_browser() -> InteractiveState {
159+
// TODO: Fetch real registry data
160+
let items = vec![
161+
InteractiveItem::new("__coming_soon__", "Registry Coming Soon")
162+
.with_description("MCP registry integration is under development")
163+
.with_disabled(true),
164+
];
165+
166+
InteractiveState::new(
167+
"MCP Registry",
168+
items,
169+
InteractiveAction::Custom("mcp-registry".to_string()),
170+
)
171+
.with_hints(vec![("Esc".to_string(), "back".to_string())])
172+
}
173+
174+
/// Build an inline form for adding a new MCP server (legacy - kept for compatibility).
175+
/// This form is displayed within the MCP panel, not as a separate modal.
176+
pub fn build_mcp_add_server_form() -> InlineFormState {
177+
build_mcp_stdio_form()
178+
}
179+
96180
/// Build an interactive state for MCP server actions (for a specific server).
97181
pub fn build_mcp_server_actions(server: &McpServerInfo) -> InteractiveState {
98182
let mut items = Vec::new();

cortex-tui/src/interactive/builders/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ pub use files::{build_context_list, build_context_remove, build_file_browser};
2828
pub use login::{
2929
LoginFlowState, LoginStatus, build_already_logged_in_selector, build_login_selector,
3030
};
31-
pub use mcp::{build_mcp_add_server_form, build_mcp_selector};
31+
pub use mcp::{
32+
build_mcp_add_server_form, build_mcp_http_form, build_mcp_registry_browser, build_mcp_selector,
33+
build_mcp_source_selector, build_mcp_stdio_form, build_mcp_transport_selector,
34+
};
3235
pub use model::build_model_selector;
3336
// pub use provider::build_provider_selector; // REMOVED (single Cortex provider)
3437
pub use resume_picker::build_resume_picker;

cortex-tui/src/runner/event_loop.rs

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6009,8 +6009,8 @@ impl EventLoop {
60096009
values: std::collections::HashMap<String, String>,
60106010
) {
60116011
match action_id {
6012-
"mcp-add" => {
6013-
// Add new MCP server from inline form
6012+
"mcp-add" | "mcp-add-stdio" => {
6013+
// Add new stdio MCP server from inline form
60146014
let name = values.get("name").cloned().unwrap_or_default();
60156015
let command = values.get("command").cloned().unwrap_or_default();
60166016
let args_str = values.get("args").cloned().unwrap_or_default();
@@ -6040,16 +6040,50 @@ impl EventLoop {
60406040
self.app_state.mcp_servers.push(server_info);
60416041
self.app_state
60426042
.toasts
6043-
.success(&format!("Added MCP server: {}", name));
6043+
.success(&format!("Added stdio MCP server: {}", name));
60446044

60456045
// TODO: Actually register with MCP integration
60466046
tracing::info!(
6047-
"Added MCP server: {} (command: {}, args: {:?})",
6047+
"Added stdio MCP server: {} (command: {}, args: {:?})",
60486048
name,
60496049
command,
60506050
args
60516051
);
60526052
}
6053+
"mcp-add-http" => {
6054+
// Add new HTTP MCP server from inline form
6055+
let name = values.get("name").cloned().unwrap_or_default();
6056+
let url = values.get("url").cloned().unwrap_or_default();
6057+
let api_key = values.get("api_key").cloned().unwrap_or_default();
6058+
6059+
if name.is_empty() || url.is_empty() {
6060+
self.app_state.toasts.error("Name and URL are required");
6061+
return;
6062+
}
6063+
6064+
// Create new server info
6065+
let server_info = crate::modal::mcp_manager::McpServerInfo {
6066+
name: name.clone(),
6067+
status: crate::modal::mcp_manager::McpStatus::Stopped,
6068+
tool_count: 0,
6069+
error: None,
6070+
requires_auth: !api_key.is_empty(),
6071+
};
6072+
6073+
// Add to app state
6074+
self.app_state.mcp_servers.push(server_info);
6075+
self.app_state
6076+
.toasts
6077+
.success(&format!("Added HTTP MCP server: {}", name));
6078+
6079+
// TODO: Actually register with MCP integration (store URL and API key)
6080+
tracing::info!(
6081+
"Added HTTP MCP server: {} (url: {}, has_api_key: {})",
6082+
name,
6083+
url,
6084+
!api_key.is_empty()
6085+
);
6086+
}
60536087
_ => {
60546088
tracing::warn!("Unhandled inline form submission: {}", action_id);
60556089
}
@@ -6210,13 +6244,11 @@ impl EventLoop {
62106244
// Handle MCP server selection
62116245
match item_id.as_str() {
62126246
"__add__" => {
6213-
// Open inline form for adding MCP server (stays in same panel)
6214-
if let Some(state) = self.app_state.input_mode.interactive_mut() {
6215-
let form =
6216-
crate::interactive::builders::mcp::build_mcp_add_server_form();
6217-
state.open_form(form);
6218-
}
6219-
return true; // Stay in interactive mode with form
6247+
// Step 1: Show source selector (Custom vs Registry)
6248+
let source_selector =
6249+
crate::interactive::builders::mcp::build_mcp_source_selector();
6250+
self.app_state.enter_interactive_mode(source_selector);
6251+
return true; // Stay in interactive mode
62206252
}
62216253
"__tools__" => {
62226254
// Show all MCP tools
@@ -6578,6 +6610,57 @@ impl EventLoop {
65786610
_ => {}
65796611
}
65806612
}
6613+
"mcp-source" => {
6614+
// Step 1 result: Custom vs Registry
6615+
match item_id.as_str() {
6616+
"custom" => {
6617+
// Step 2: Show transport selector (stdio vs HTTP)
6618+
let transport_selector =
6619+
crate::interactive::builders::mcp::build_mcp_transport_selector(
6620+
);
6621+
self.app_state.enter_interactive_mode(transport_selector);
6622+
return true;
6623+
}
6624+
"registry" => {
6625+
// Show registry browser (placeholder for now)
6626+
let registry_browser =
6627+
crate::interactive::builders::mcp::build_mcp_registry_browser();
6628+
self.app_state.enter_interactive_mode(registry_browser);
6629+
return true;
6630+
}
6631+
_ => {}
6632+
}
6633+
}
6634+
"mcp-transport" => {
6635+
// Step 2 result: stdio vs HTTP
6636+
match item_id.as_str() {
6637+
"stdio" => {
6638+
// Show stdio form
6639+
if let Some(state) = self.app_state.input_mode.interactive_mut() {
6640+
let form =
6641+
crate::interactive::builders::mcp::build_mcp_stdio_form();
6642+
state.open_form(form);
6643+
}
6644+
return true;
6645+
}
6646+
"http" => {
6647+
// Show HTTP form
6648+
if let Some(state) = self.app_state.input_mode.interactive_mut() {
6649+
let form =
6650+
crate::interactive::builders::mcp::build_mcp_http_form();
6651+
state.open_form(form);
6652+
}
6653+
return true;
6654+
}
6655+
_ => {}
6656+
}
6657+
}
6658+
"mcp-registry" => {
6659+
// Registry browser - nothing selectable for now
6660+
self.app_state
6661+
.toasts
6662+
.info("Registry integration coming soon");
6663+
}
65816664
_ if custom_action.starts_with("mcp:") => {
65826665
// Handle MCP server actions - format: "mcp:<server_name>"
65836666
let server_name = custom_action.strip_prefix("mcp:").unwrap_or("");

0 commit comments

Comments
 (0)