feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
use rmcp::transport::TokioChildProcess;
|
2026-06-01 13:25:49 +00:00
|
|
|
use std::collections::HashMap;
|
feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
2026-06-01 13:25:49 +00:00
|
|
|
/// Build a stdio transport from a command path, argument list, and environment variables.
|
feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
/// Rejects relative paths to prevent path traversal.
|
2026-06-01 17:16:11 +00:00
|
|
|
/// Validates environment variable names to block known privilege escalation vectors.
|
2026-06-01 13:25:49 +00:00
|
|
|
pub fn build_stdio_transport(
|
|
|
|
|
command: &str,
|
|
|
|
|
args: &[String],
|
|
|
|
|
env: HashMap<String, String>,
|
|
|
|
|
) -> Result<TokioChildProcess, String> {
|
feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
if !Path::new(command).is_absolute() {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"stdio command must be an absolute path, got: {command}"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 17:16:11 +00:00
|
|
|
// Validate env var names to block dangerous variables that could be used for privilege escalation
|
|
|
|
|
const DANGEROUS_ENV_VARS: &[&str] = &[
|
|
|
|
|
"LD_PRELOAD",
|
|
|
|
|
"LD_LIBRARY_PATH",
|
|
|
|
|
"DYLD_INSERT_LIBRARIES",
|
|
|
|
|
"DYLD_LIBRARY_PATH",
|
|
|
|
|
"DYLD_FRAMEWORK_PATH",
|
|
|
|
|
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for key in env.keys() {
|
|
|
|
|
let upper_key = key.to_uppercase();
|
|
|
|
|
if DANGEROUS_ENV_VARS.contains(&upper_key.as_str()) {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"Dangerous environment variable '{key}' is not allowed for security reasons. \
|
|
|
|
|
This variable could be used for privilege escalation attacks."
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
let mut cmd = Command::new(command);
|
|
|
|
|
cmd.args(args);
|
|
|
|
|
|
2026-06-01 13:25:49 +00:00
|
|
|
// Apply environment variables
|
|
|
|
|
for (key, value) in env {
|
|
|
|
|
cmd.env(key, value);
|
|
|
|
|
}
|
|
|
|
|
|
feat(mcp): add MCP Server Support with TDD implementation
Adds full Model Context Protocol (MCP) server management, enabling the
AI assistant to discover and call tools from external MCP servers during
triage conversations.
Backend (Rust):
- rmcp 1.7.0 dependency (client + stdio + Streamable HTTP transports)
- Migration 018: mcp_servers, mcp_tools, mcp_resources tables with
CHECK constraints for transport_type, auth_type, discovery_status
- src/mcp/ module: models, store, client, adapter, discovery, commands,
transport/{stdio,http}
- AppState gains mcp_connections: Arc<TokioMutex<HashMap<...>>>
- .setup() hook auto-discovers enabled servers at startup
- 8 new Tauri commands wired into invoke_handler
- execute_mcp_tool_call: PII scan + mandatory audit_log before execution
- Auth values encrypted at rest via integrations::auth::encrypt_token();
scrubbed before any frontend response
Frontend:
- MCPServers.tsx settings page (/settings/mcp) with server list,
status badges, Discover Now, Add/Edit modal, enable/disable toggle
- tauriCommands.ts: McpServer, McpTool, McpServerStatus types + 8 cmds
- App.tsx: Plug icon, /settings/mcp route, sidebar nav entry
Tests (TDD): 15 new tests, all green
- 5 migration tests (written before migration, red → green)
- 5 store CRUD + encryption tests
- 5 adapter sanitization + conversion tests
Verification: 185/185 Rust, 94/94 Vitest, clippy -D warnings: 0
2026-05-23 21:23:48 +00:00
|
|
|
TokioChildProcess::new(cmd).map_err(|e| format!("Failed to spawn stdio process: {e}"))
|
|
|
|
|
}
|
2026-06-01 17:16:11 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_rejects_relative_path() {
|
|
|
|
|
let result = build_stdio_transport("./mcp-server", &[], HashMap::new());
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
if let Err(e) = result {
|
|
|
|
|
assert!(e.contains("absolute path"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_rejects_dangerous_env_vars() {
|
|
|
|
|
let dangerous_vars = vec![
|
|
|
|
|
"LD_PRELOAD",
|
|
|
|
|
"LD_LIBRARY_PATH",
|
|
|
|
|
"DYLD_INSERT_LIBRARIES",
|
|
|
|
|
"DYLD_LIBRARY_PATH",
|
|
|
|
|
"DYLD_FRAMEWORK_PATH",
|
|
|
|
|
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for var in dangerous_vars {
|
|
|
|
|
let mut env = HashMap::new();
|
|
|
|
|
env.insert(var.to_string(), "malicious.so".to_string());
|
|
|
|
|
|
|
|
|
|
let result = build_stdio_transport("/usr/bin/test", &[], env);
|
|
|
|
|
assert!(result.is_err(), "Should reject {}", var);
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
assert!(
|
|
|
|
|
err.contains("Dangerous environment variable"),
|
|
|
|
|
"Error should mention dangerous variable: {}",
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_rejects_dangerous_env_vars_case_insensitive() {
|
|
|
|
|
let mut env = HashMap::new();
|
|
|
|
|
env.insert("ld_preload".to_string(), "malicious.so".to_string());
|
|
|
|
|
|
|
|
|
|
let result = build_stdio_transport("/usr/bin/test", &[], env);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
assert!(err.contains("Dangerous environment variable"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_allows_safe_env_vars() {
|
2026-06-01 17:41:26 +00:00
|
|
|
// Test that safe env vars pass validation (validation happens before spawn)
|
|
|
|
|
let safe_vars = vec![
|
|
|
|
|
("DEBUG", "1"),
|
|
|
|
|
("API_KEY", "secret123"),
|
|
|
|
|
("PATH", "/usr/bin"),
|
|
|
|
|
("HOME", "/home/user"),
|
|
|
|
|
("GITHUB_PERSONAL_ACCESS_TOKEN", "ghp_token"),
|
|
|
|
|
("LOG_LEVEL", "info"),
|
|
|
|
|
];
|
2026-06-01 17:16:11 +00:00
|
|
|
|
2026-06-01 17:41:26 +00:00
|
|
|
for (key, value) in safe_vars {
|
|
|
|
|
let mut env = HashMap::new();
|
|
|
|
|
env.insert(key.to_string(), value.to_string());
|
2026-06-01 17:16:11 +00:00
|
|
|
|
2026-06-01 17:41:26 +00:00
|
|
|
// This will fail to spawn since /usr/bin/nonexistent doesn't exist,
|
|
|
|
|
// but if validation passed, error won't mention "Dangerous environment variable"
|
|
|
|
|
let result = build_stdio_transport("/usr/bin/nonexistent", &[], env);
|
|
|
|
|
|
|
|
|
|
// Validation passes (doesn't reject env var), spawn fails (command doesn't exist)
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
assert!(
|
|
|
|
|
!err.contains("Dangerous environment variable"),
|
|
|
|
|
"Should not reject safe env var '{}', got: {}",
|
|
|
|
|
key,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-01 17:16:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|