tftsr-devops_investigation/src-tauri/src/mcp/commands.rs
Shaun Arman 093495a653
Some checks failed
Test / rust-fmt-check (pull_request) Failing after 0s
Test / rust-clippy (pull_request) Failing after 1s
Test / rust-tests (pull_request) Failing after 0s
Test / frontend-typecheck (pull_request) Failing after 16s
Test / frontend-tests (pull_request) Failing after 18s
PR Review Automation / review (pull_request) Failing after 4m13s
feat: full copy from apollo_nxt-trcaa with complete sanitization
Complete backport of all features from apollo_nxt-trcaa repository:
- Three-tier shell execution safety system (Tier 1: auto, Tier 2: approve, Tier 3: deny)
- Ollama function calling with tool use support
- AI provider tool calling auto-detection
- kubectl binary bundling and management
- kubeconfig upload and context management
- Shell approval modal with real-time UI
- MCP protocol HTTP transport with custom headers
- Enhanced security audit logging
- Comprehensive test coverage (275+ tests)
- Updated CI/CD workflows for Gitea Actions
- Complete documentation (ADRs, wiki, release notes)

Sanitization applied to all files:
- Removed all MSI, Motorola, VNXT, Vesta references
- Replaced internal infrastructure references with TFTSR equivalents
- Updated all URLs and API endpoints
- Sanitized commit history references in documentation

Technical changes:
- New modules: shell/classifier, shell/executor, shell/kubectl, shell/kubeconfig
- Enhanced AI providers: ollama.rs, openai.rs with function calling
- New Tauri commands: shell execution, kubeconfig management, tool calling detection
- Database migrations: shell_execution_audit table
- Frontend: ShellApprovalModal, ShellExecution, KubeconfigManager pages
- CI/CD: kubectl bundling, multi-platform builds, Gitea Actions integration

Version: 1.0.8

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-06-05 14:12:43 -05:00

274 lines
8.2 KiB
Rust

use std::sync::Arc;
use tauri::State;
use tokio::sync::Mutex as TokioMutex;
use tracing::{info, warn};
use crate::mcp::models::{
CreateMcpServerRequest, McpServer, McpServerStatus, UpdateMcpServerRequest,
};
use crate::mcp::store::{
create_server, delete_server, get_resource_count, get_server, get_tool_count, list_servers,
toggle_server, update_discovery_status, update_server,
};
use crate::state::AppState;
#[tauri::command]
pub async fn list_mcp_servers(state: State<'_, AppState>) -> Result<Vec<McpServer>, String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
let mut servers = list_servers(&db)?;
// Never expose encrypted auth values to the frontend
for s in &mut servers {
s.auth_value = None;
}
Ok(servers)
}
#[tauri::command]
pub async fn create_mcp_server(
request: CreateMcpServerRequest,
state: State<'_, AppState>,
) -> Result<McpServer, String> {
let mut server = {
let db = state.db.lock().map_err(|e| e.to_string())?;
create_server(&db, &request)?
};
server.auth_value = None;
Ok(server)
}
#[tauri::command]
pub async fn update_mcp_server(
id: String,
request: UpdateMcpServerRequest,
state: State<'_, AppState>,
) -> Result<McpServer, String> {
let mut server = {
let db = state.db.lock().map_err(|e| e.to_string())?;
update_server(&db, &id, &request)?
};
server.auth_value = None;
Ok(server)
}
#[tauri::command]
pub async fn delete_mcp_server(
id: String,
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
{
let db = state.db.lock().map_err(|e| e.to_string())?;
// Capture server name and tool count for the audit entry before cascade delete
let server_name = get_server(&db, &id)?
.map(|s| s.name)
.unwrap_or_else(|| id.clone());
let tool_count = crate::mcp::store::get_tool_count(&db, &id).unwrap_or(0);
let resource_count = crate::mcp::store::get_resource_count(&db, &id).unwrap_or(0);
let details = serde_json::json!({
"server_name": server_name,
"tools_deleted": tool_count,
"resources_deleted": resource_count,
});
crate::audit::log::write_audit_event(
&db,
"mcp_server_deleted",
"mcp_server",
&id,
&details.to_string(),
)
.map_err(|e| format!("Audit log failed: {e}"))?;
delete_server(&db, &id)?;
}
// Remove live connection if present
let mut connections = state.mcp_connections.lock().await;
connections.remove(&id);
drop(connections);
info!(server_id = %id, "MCP server deleted");
let _ = app_handle;
Ok(())
}
#[tauri::command]
pub async fn toggle_mcp_server(
id: String,
enabled: bool,
state: State<'_, AppState>,
) -> Result<(), String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
toggle_server(&db, &id, enabled)?;
Ok(())
}
#[tauri::command]
pub async fn discover_mcp_server(
id: String,
app_handle: tauri::AppHandle,
state: State<'_, AppState>,
) -> Result<McpServerStatus, String> {
let server = {
let db = state.db.lock().map_err(|e| e.to_string())?;
get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))?
};
match crate::mcp::discovery::discover_server(&server, &app_handle).await {
Ok(conn) => {
let mut connections = state.mcp_connections.lock().await;
connections.insert(id.clone(), Arc::new(TokioMutex::new(conn)));
drop(connections);
let (tool_count, resource_count, last_discovered_at) = {
let db = state.db.lock().map_err(|e| e.to_string())?;
let tc = get_tool_count(&db, &id)?;
let rc = get_resource_count(&db, &id)?;
let srv = get_server(&db, &id)?.unwrap();
(tc, rc, srv.last_discovered_at)
};
Ok(McpServerStatus {
server_id: id,
status: "connected".to_string(),
error: None,
tool_count,
resource_count,
last_discovered_at,
})
}
Err(e) => {
{
let db = state.db.lock().map_err(|db_err| db_err.to_string())?;
update_discovery_status(&db, &id, "error", Some(&e))?;
}
Ok(McpServerStatus {
server_id: id,
status: "error".to_string(),
error: Some(e),
tool_count: 0,
resource_count: 0,
last_discovered_at: None,
})
}
}
}
#[tauri::command]
pub async fn get_mcp_server_status(
id: String,
state: State<'_, AppState>,
) -> Result<McpServerStatus, String> {
let (server, tool_count, resource_count) = {
let db = state.db.lock().map_err(|e| e.to_string())?;
let srv = get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))?;
let tc = get_tool_count(&db, &id)?;
let rc = get_resource_count(&db, &id)?;
(srv, tc, rc)
};
Ok(McpServerStatus {
server_id: id,
status: server.discovery_status,
error: server.discovery_error,
tool_count,
resource_count,
last_discovered_at: server.last_discovered_at,
})
}
#[tauri::command]
pub async fn initiate_mcp_oauth(
id: String,
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let server = {
let db = state.db.lock().map_err(|e| e.to_string())?;
get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))?
};
if server.auth_type != "oauth2" {
return Err(format!(
"Server {} uses auth_type '{}', not oauth2",
id, server.auth_type
));
}
let config: serde_json::Value =
serde_json::from_str(&server.transport_config).unwrap_or_default();
let auth_endpoint = config
.get("auth_endpoint")
.and_then(|v| v.as_str())
.ok_or("OAuth2 transport_config missing 'auth_endpoint'")?
.to_string();
let client_id = config
.get("client_id")
.and_then(|v| v.as_str())
.ok_or("OAuth2 transport_config missing 'client_id'")?
.to_string();
let token_endpoint = config
.get("token_endpoint")
.and_then(|v| v.as_str())
.ok_or("OAuth2 transport_config missing 'token_endpoint'")?
.to_string();
let redirect_uri = "http://localhost:12345/mcp-oauth-callback".to_string();
let scope = config
.get("scope")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let pkce = crate::integrations::auth::generate_pkce();
let base_auth_url = crate::integrations::auth::build_auth_url(
&auth_endpoint,
&client_id,
&redirect_uri,
&scope,
&pkce,
);
// Append a cryptographically random state nonce for CSRF protection
let state_nonce = {
use rand::RngCore;
let mut bytes = [0u8; 16];
rand::rng().fill_bytes(&mut bytes);
hex::encode(bytes)
};
let auth_url = format!(
"{}&state={}",
base_auth_url,
urlencoding::encode(&state_nonce)
);
// Open WebView window for OAuth
let window_label = format!("mcp-oauth-{id}");
let _ = tauri::WebviewWindowBuilder::new(
&app_handle,
&window_label,
tauri::WebviewUrl::External(
url::Url::parse(&auth_url).map_err(|e| format!("Invalid OAuth URL: {e}"))?,
),
)
.title(format!("Authenticate: {}", server.name))
.inner_size(800.0, 700.0)
.build()
.map_err(|e| format!("Failed to open OAuth window: {e}"))?;
// Monitor URL changes for the redirect callback
// For now, return Ok and let the user copy the code manually
// Full implementation would poll the webview URL
warn!(server_id = %id, "OAuth2 WebView opened — token exchange not yet automated");
// Exchange code → token
// In production this would be driven by webview URL monitoring (see integrations::webview_auth)
// This stub allows the UI to open the browser without crashing.
let _ = (token_endpoint, pkce);
Ok(())
}