All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m28s
Test / frontend-tests (pull_request) Successful in 1m31s
Test / frontend-typecheck (pull_request) Successful in 1m34s
Test / rust-clippy (pull_request) Successful in 3m29s
PR Review Automation / review (pull_request) Successful in 4m34s
Test / rust-tests (pull_request) Successful in 4m56s
Add clearer placeholder and helper text to explain that encrypted environment variables are never displayed for security reasons. When editing an existing server, the encrypted_env field shows a placeholder explaining that leaving it blank will preserve existing values. Also apply cargo fmt formatting fixes to store.rs.
652 lines
21 KiB
Rust
652 lines
21 KiB
Rust
use rusqlite::{Connection, OptionalExtension};
|
|
use uuid::Uuid;
|
|
|
|
use crate::integrations::auth::{decrypt_token, encrypt_token};
|
|
use crate::mcp::models::{
|
|
CreateMcpServerRequest, McpResource, McpServer, McpTool, UpdateMcpServerRequest,
|
|
};
|
|
|
|
pub fn create_server(conn: &Connection, req: &CreateMcpServerRequest) -> Result<McpServer, String> {
|
|
let id = Uuid::now_v7().to_string();
|
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
|
|
let encrypted_auth = match &req.auth_value {
|
|
Some(v) if !v.is_empty() => Some(encrypt_token(v)?),
|
|
_ => None,
|
|
};
|
|
|
|
let encrypted_env = match &req.env_config {
|
|
Some(env_json) if !env_json.trim().is_empty() => Some(encrypt_token(env_json)?),
|
|
_ => None,
|
|
};
|
|
|
|
conn.execute(
|
|
"INSERT INTO mcp_servers
|
|
(id, name, url, transport_type, transport_config, auth_type, auth_value, enabled, env_config, created_at, updated_at)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10)",
|
|
rusqlite::params![
|
|
id,
|
|
req.name,
|
|
req.url,
|
|
req.transport_type,
|
|
req.transport_config,
|
|
req.auth_type,
|
|
encrypted_auth,
|
|
req.enabled as i32,
|
|
encrypted_env,
|
|
now,
|
|
],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
get_server(conn, &id)?.ok_or_else(|| "Server not found after insert".to_string())
|
|
}
|
|
|
|
pub fn get_server(conn: &Connection, id: &str) -> Result<Option<McpServer>, String> {
|
|
let row = conn
|
|
.query_row(
|
|
"SELECT id, name, url, transport_type, transport_config, auth_type, auth_value,
|
|
enabled, last_discovered_at, discovery_status, discovery_error,
|
|
created_at, updated_at, env_config
|
|
FROM mcp_servers WHERE id = ?1",
|
|
[id],
|
|
|row| {
|
|
Ok(McpServer {
|
|
id: row.get(0)?,
|
|
name: row.get(1)?,
|
|
url: row.get(2)?,
|
|
transport_type: row.get(3)?,
|
|
transport_config: row.get(4)?,
|
|
auth_type: row.get(5)?,
|
|
auth_value: row.get(6)?,
|
|
enabled: row.get::<_, i32>(7)? != 0,
|
|
last_discovered_at: row.get(8)?,
|
|
discovery_status: row.get(9)?,
|
|
discovery_error: row.get(10)?,
|
|
created_at: row.get(11)?,
|
|
updated_at: row.get(12)?,
|
|
env_config: row.get(13)?,
|
|
})
|
|
},
|
|
)
|
|
.optional()
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(row)
|
|
}
|
|
|
|
pub fn list_servers(conn: &Connection) -> Result<Vec<McpServer>, String> {
|
|
let mut stmt = conn
|
|
.prepare(
|
|
"SELECT id, name, url, transport_type, transport_config, auth_type, auth_value,
|
|
enabled, last_discovered_at, discovery_status, discovery_error,
|
|
created_at, updated_at, env_config
|
|
FROM mcp_servers ORDER BY created_at ASC",
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let rows = stmt
|
|
.query_map([], |row| {
|
|
Ok(McpServer {
|
|
id: row.get(0)?,
|
|
name: row.get(1)?,
|
|
url: row.get(2)?,
|
|
transport_type: row.get(3)?,
|
|
transport_config: row.get(4)?,
|
|
auth_type: row.get(5)?,
|
|
auth_value: row.get(6)?,
|
|
enabled: row.get::<_, i32>(7)? != 0,
|
|
last_discovered_at: row.get(8)?,
|
|
discovery_status: row.get(9)?,
|
|
discovery_error: row.get(10)?,
|
|
created_at: row.get(11)?,
|
|
updated_at: row.get(12)?,
|
|
env_config: row.get(13)?,
|
|
})
|
|
})
|
|
.map_err(|e| e.to_string())?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
Ok(rows)
|
|
}
|
|
|
|
pub fn update_server(
|
|
conn: &Connection,
|
|
id: &str,
|
|
req: &UpdateMcpServerRequest,
|
|
) -> Result<McpServer, String> {
|
|
let existing = get_server(conn, id)?.ok_or_else(|| format!("Server {id} not found"))?;
|
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
|
|
let new_encrypted_auth = match &req.auth_value {
|
|
Some(v) if !v.is_empty() => Some(encrypt_token(v)?),
|
|
Some(_) => None,
|
|
None => existing.auth_value.clone(),
|
|
};
|
|
|
|
let new_encrypted_env = match &req.env_config {
|
|
Some(env_json) if !env_json.trim().is_empty() => Some(encrypt_token(env_json)?),
|
|
Some(_) => None, // Empty string = clear env_config
|
|
None => existing.env_config.clone(), // No update requested
|
|
};
|
|
|
|
conn.execute(
|
|
"UPDATE mcp_servers SET
|
|
name = ?1, url = ?2, transport_type = ?3, transport_config = ?4,
|
|
auth_type = ?5, auth_value = ?6, enabled = ?7, env_config = ?8, updated_at = ?9
|
|
WHERE id = ?10",
|
|
rusqlite::params![
|
|
req.name.as_deref().unwrap_or(&existing.name),
|
|
req.url.as_deref().unwrap_or(&existing.url),
|
|
req.transport_type
|
|
.as_deref()
|
|
.unwrap_or(&existing.transport_type),
|
|
req.transport_config
|
|
.as_deref()
|
|
.unwrap_or(&existing.transport_config),
|
|
req.auth_type.as_deref().unwrap_or(&existing.auth_type),
|
|
new_encrypted_auth,
|
|
req.enabled
|
|
.map(|b| b as i32)
|
|
.unwrap_or(existing.enabled as i32),
|
|
new_encrypted_env,
|
|
now,
|
|
id,
|
|
],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
get_server(conn, id)?.ok_or_else(|| "Server not found after update".to_string())
|
|
}
|
|
|
|
pub fn delete_server(conn: &Connection, id: &str) -> Result<(), String> {
|
|
conn.execute("DELETE FROM mcp_servers WHERE id = ?1", [id])
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn toggle_server(conn: &Connection, id: &str, enabled: bool) -> Result<(), String> {
|
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
conn.execute(
|
|
"UPDATE mcp_servers SET enabled = ?1, updated_at = ?2 WHERE id = ?3",
|
|
rusqlite::params![enabled as i32, now, id],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_discovery_status(
|
|
conn: &Connection,
|
|
id: &str,
|
|
status: &str,
|
|
error: Option<&str>,
|
|
) -> Result<(), String> {
|
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
conn.execute(
|
|
"UPDATE mcp_servers SET discovery_status = ?1, discovery_error = ?2,
|
|
last_discovered_at = ?3, updated_at = ?3 WHERE id = ?4",
|
|
rusqlite::params![status, error, now, id],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn replace_tools(conn: &Connection, server_id: &str, tools: &[McpTool]) -> Result<(), String> {
|
|
conn.execute("DELETE FROM mcp_tools WHERE server_id = ?1", [server_id])
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
for tool in tools {
|
|
conn.execute(
|
|
"INSERT INTO mcp_tools (id, server_id, name, tool_key, description, parameters)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
rusqlite::params![
|
|
tool.id,
|
|
tool.server_id,
|
|
tool.name,
|
|
tool.tool_key,
|
|
tool.description,
|
|
tool.parameters,
|
|
],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn replace_resources(
|
|
conn: &Connection,
|
|
server_id: &str,
|
|
resources: &[McpResource],
|
|
) -> Result<(), String> {
|
|
conn.execute(
|
|
"DELETE FROM mcp_resources WHERE server_id = ?1",
|
|
[server_id],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
for res in resources {
|
|
conn.execute(
|
|
"INSERT INTO mcp_resources (id, server_id, uri, name, description)
|
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
rusqlite::params![res.id, res.server_id, res.uri, res.name, res.description],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_enabled_tools(conn: &Connection) -> Result<Vec<(McpTool, String)>, String> {
|
|
let mut stmt = conn
|
|
.prepare(
|
|
"SELECT t.id, t.server_id, t.name, t.tool_key, t.description, t.parameters, s.url
|
|
FROM mcp_tools t
|
|
JOIN mcp_servers s ON t.server_id = s.id
|
|
WHERE s.enabled = 1 AND s.discovery_status = 'connected'",
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let rows = stmt
|
|
.query_map([], |row| {
|
|
Ok((
|
|
McpTool {
|
|
id: row.get(0)?,
|
|
server_id: row.get(1)?,
|
|
name: row.get(2)?,
|
|
tool_key: row.get(3)?,
|
|
description: row.get(4)?,
|
|
parameters: row.get(5)?,
|
|
},
|
|
row.get::<_, String>(6)?,
|
|
))
|
|
})
|
|
.map_err(|e| e.to_string())?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
Ok(rows)
|
|
}
|
|
|
|
pub fn get_tool_by_key(conn: &Connection, tool_key: &str) -> Result<Option<McpTool>, String> {
|
|
conn.query_row(
|
|
"SELECT id, server_id, name, tool_key, description, parameters
|
|
FROM mcp_tools WHERE tool_key = ?1",
|
|
[tool_key],
|
|
|row| {
|
|
Ok(McpTool {
|
|
id: row.get(0)?,
|
|
server_id: row.get(1)?,
|
|
name: row.get(2)?,
|
|
tool_key: row.get(3)?,
|
|
description: row.get(4)?,
|
|
parameters: row.get(5)?,
|
|
})
|
|
},
|
|
)
|
|
.optional()
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
pub fn get_server_auth_value(conn: &Connection, server_id: &str) -> Result<Option<String>, String> {
|
|
let encrypted: Option<String> = conn
|
|
.query_row(
|
|
"SELECT auth_value FROM mcp_servers WHERE id = ?1",
|
|
[server_id],
|
|
|row| row.get(0),
|
|
)
|
|
.optional()
|
|
.map_err(|e| e.to_string())?
|
|
.flatten();
|
|
|
|
match encrypted {
|
|
Some(enc) => Ok(Some(decrypt_token(&enc)?)),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
pub fn get_tool_count(conn: &Connection, server_id: &str) -> Result<usize, String> {
|
|
conn.query_row(
|
|
"SELECT COUNT(*) FROM mcp_tools WHERE server_id = ?1",
|
|
[server_id],
|
|
|r| r.get::<_, i64>(0),
|
|
)
|
|
.map(|n| n as usize)
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
pub fn get_resource_count(conn: &Connection, server_id: &str) -> Result<usize, String> {
|
|
conn.query_row(
|
|
"SELECT COUNT(*) FROM mcp_resources WHERE server_id = ?1",
|
|
[server_id],
|
|
|r| r.get::<_, i64>(0),
|
|
)
|
|
.map(|n| n as usize)
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Decrypt and parse env_config from database, returning a HashMap.
|
|
/// Returns None if env_config is NULL, or an error if decryption/parsing fails.
|
|
pub fn get_server_env_config(
|
|
conn: &Connection,
|
|
server_id: &str,
|
|
) -> Result<Option<std::collections::HashMap<String, String>>, String> {
|
|
let encrypted: Option<String> = conn
|
|
.query_row(
|
|
"SELECT env_config FROM mcp_servers WHERE id = ?1",
|
|
[server_id],
|
|
|row| row.get(0),
|
|
)
|
|
.optional()
|
|
.map_err(|e| e.to_string())?
|
|
.flatten();
|
|
|
|
match encrypted {
|
|
Some(enc) => {
|
|
let decrypted = decrypt_token(&enc)?;
|
|
let parsed: std::collections::HashMap<String, String> =
|
|
serde_json::from_str(&decrypted)
|
|
.map_err(|e| format!("Failed to parse env_config JSON: {e}"))?;
|
|
Ok(Some(parsed))
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::db::migrations::run_migrations;
|
|
|
|
fn setup() -> Connection {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
run_migrations(&conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
fn make_req(name: &str) -> CreateMcpServerRequest {
|
|
CreateMcpServerRequest {
|
|
name: name.to_string(),
|
|
url: "http://localhost:8080/mcp".to_string(),
|
|
transport_type: "http".to_string(),
|
|
transport_config: "{}".to_string(),
|
|
auth_type: "none".to_string(),
|
|
auth_value: None,
|
|
enabled: true,
|
|
env_config: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_mcp_server_crud() {
|
|
let conn = setup();
|
|
|
|
// Create
|
|
let server = create_server(&conn, &make_req("Test Server")).unwrap();
|
|
assert_eq!(server.name, "Test Server");
|
|
assert_eq!(server.transport_type, "http");
|
|
assert_eq!(server.discovery_status, "pending");
|
|
assert!(server.enabled);
|
|
|
|
// Read
|
|
let fetched = get_server(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(fetched.id, server.id);
|
|
|
|
// List
|
|
let all = list_servers(&conn).unwrap();
|
|
assert_eq!(all.len(), 1);
|
|
|
|
// Update
|
|
let updated = update_server(
|
|
&conn,
|
|
&server.id,
|
|
&UpdateMcpServerRequest {
|
|
name: Some("Renamed".to_string()),
|
|
url: None,
|
|
transport_type: None,
|
|
transport_config: None,
|
|
auth_type: None,
|
|
auth_value: None,
|
|
enabled: None,
|
|
env_config: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(updated.name, "Renamed");
|
|
|
|
// Delete
|
|
delete_server(&conn, &server.id).unwrap();
|
|
assert!(get_server(&conn, &server.id).unwrap().is_none());
|
|
assert!(list_servers(&conn).unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_auth_value_encrypted_at_rest() {
|
|
let conn = setup();
|
|
|
|
let req = CreateMcpServerRequest {
|
|
name: "Secured".to_string(),
|
|
url: "http://localhost/mcp".to_string(),
|
|
transport_type: "http".to_string(),
|
|
transport_config: "{}".to_string(),
|
|
auth_type: "bearer".to_string(),
|
|
auth_value: Some("super-secret-token".to_string()),
|
|
enabled: true,
|
|
env_config: None,
|
|
};
|
|
let server = create_server(&conn, &req).unwrap();
|
|
|
|
// Raw DB value must NOT equal the plaintext token
|
|
let raw: Option<String> = conn
|
|
.query_row(
|
|
"SELECT auth_value FROM mcp_servers WHERE id = ?1",
|
|
[&server.id],
|
|
|r| r.get(0),
|
|
)
|
|
.unwrap();
|
|
let raw = raw.unwrap();
|
|
assert_ne!(
|
|
raw, "super-secret-token",
|
|
"auth_value must be encrypted in DB"
|
|
);
|
|
|
|
// Decrypted value must match original
|
|
let decrypted = get_server_auth_value(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(decrypted, "super-secret-token");
|
|
}
|
|
|
|
#[test]
|
|
fn test_disabled_server_excluded_from_tools() {
|
|
let conn = setup();
|
|
|
|
let mut req = make_req("Disabled Server");
|
|
req.enabled = false;
|
|
let server = create_server(&conn, &req).unwrap();
|
|
|
|
// Mark connected and add a tool
|
|
update_discovery_status(&conn, &server.id, "connected", None).unwrap();
|
|
let tool = McpTool {
|
|
id: Uuid::now_v7().to_string(),
|
|
server_id: server.id.clone(),
|
|
name: "echo".to_string(),
|
|
tool_key: "mcp_disabled_server_echo".to_string(),
|
|
description: None,
|
|
parameters: "{}".to_string(),
|
|
};
|
|
replace_tools(&conn, &server.id, &[tool]).unwrap();
|
|
|
|
let tools = get_enabled_tools(&conn).unwrap();
|
|
assert!(tools.is_empty(), "disabled server tools should be excluded");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_discovery_status() {
|
|
let conn = setup();
|
|
|
|
let server = create_server(&conn, &make_req("Status Test")).unwrap();
|
|
assert_eq!(server.discovery_status, "pending");
|
|
|
|
update_discovery_status(&conn, &server.id, "connected", None).unwrap();
|
|
let updated = get_server(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(updated.discovery_status, "connected");
|
|
assert!(updated.last_discovered_at.is_some());
|
|
|
|
update_discovery_status(&conn, &server.id, "error", Some("connection refused")).unwrap();
|
|
let errored = get_server(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(errored.discovery_status, "error");
|
|
assert_eq!(
|
|
errored.discovery_error.as_deref(),
|
|
Some("connection refused")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cascade_delete_clears_tools() {
|
|
let conn = setup();
|
|
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
|
|
|
|
let server = create_server(&conn, &make_req("Cascade Test")).unwrap();
|
|
let tool = McpTool {
|
|
id: Uuid::now_v7().to_string(),
|
|
server_id: server.id.clone(),
|
|
name: "ping".to_string(),
|
|
tool_key: "mcp_cascade_test_ping".to_string(),
|
|
description: None,
|
|
parameters: "{}".to_string(),
|
|
};
|
|
replace_tools(&conn, &server.id, &[tool]).unwrap();
|
|
|
|
let count = get_tool_count(&conn, &server.id).unwrap();
|
|
assert_eq!(count, 1);
|
|
|
|
delete_server(&conn, &server.id).unwrap();
|
|
|
|
// After delete, count should be 0 (cascade)
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM mcp_tools", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert_eq!(count, 0, "cascade delete should clear mcp_tools");
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_config_encrypted_at_rest() {
|
|
let conn = setup();
|
|
|
|
let req = CreateMcpServerRequest {
|
|
name: "Env Test".to_string(),
|
|
url: "".to_string(),
|
|
transport_type: "stdio".to_string(),
|
|
transport_config: r#"{"command":"/usr/bin/test","args":[]}"#.to_string(),
|
|
auth_type: "none".to_string(),
|
|
auth_value: None,
|
|
enabled: true,
|
|
env_config: Some(r#"{"API_KEY":"secret123","DEBUG":"1"}"#.to_string()),
|
|
};
|
|
let server = create_server(&conn, &req).unwrap();
|
|
|
|
// Raw DB value must be encrypted (not equal to plaintext)
|
|
let raw: Option<String> = conn
|
|
.query_row(
|
|
"SELECT env_config FROM mcp_servers WHERE id = ?1",
|
|
[&server.id],
|
|
|r| r.get(0),
|
|
)
|
|
.unwrap();
|
|
let raw = raw.unwrap();
|
|
assert_ne!(
|
|
raw, r#"{"API_KEY":"secret123","DEBUG":"1"}"#,
|
|
"env_config should be encrypted at rest"
|
|
);
|
|
|
|
// Decrypted value must match original
|
|
let env_map = get_server_env_config(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(env_map.get("API_KEY").unwrap(), "secret123");
|
|
assert_eq!(env_map.get("DEBUG").unwrap(), "1");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_env_config() {
|
|
let conn = setup();
|
|
|
|
let server = create_server(&conn, &make_req("Env Update")).unwrap();
|
|
assert!(server.env_config.is_none());
|
|
|
|
let updated = update_server(
|
|
&conn,
|
|
&server.id,
|
|
&UpdateMcpServerRequest {
|
|
name: None,
|
|
url: None,
|
|
transport_type: None,
|
|
transport_config: None,
|
|
auth_type: None,
|
|
auth_value: None,
|
|
enabled: None,
|
|
env_config: Some(r#"{"NEW_VAR":"value"}"#.to_string()),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(updated.env_config.is_some());
|
|
let env_map = get_server_env_config(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(env_map.get("NEW_VAR").unwrap(), "value");
|
|
}
|
|
|
|
#[test]
|
|
fn test_clear_env_config_with_empty_string() {
|
|
let conn = setup();
|
|
|
|
let mut req = make_req("Clear Env");
|
|
req.env_config = Some(r#"{"KEY":"val"}"#.to_string());
|
|
let server = create_server(&conn, &req).unwrap();
|
|
assert!(server.env_config.is_some());
|
|
|
|
let updated = update_server(
|
|
&conn,
|
|
&server.id,
|
|
&UpdateMcpServerRequest {
|
|
name: None,
|
|
url: None,
|
|
transport_type: None,
|
|
transport_config: None,
|
|
auth_type: None,
|
|
auth_value: None,
|
|
enabled: None,
|
|
env_config: Some("".to_string()), // Clear
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(updated.env_config.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_config_none_preserves_existing() {
|
|
let conn = setup();
|
|
|
|
let mut req = make_req("Preserve Env");
|
|
req.env_config = Some(r#"{"ORIGINAL":"value"}"#.to_string());
|
|
let server = create_server(&conn, &req).unwrap();
|
|
|
|
// Update without touching env_config
|
|
let updated = update_server(
|
|
&conn,
|
|
&server.id,
|
|
&UpdateMcpServerRequest {
|
|
name: Some("New Name".to_string()),
|
|
url: None,
|
|
transport_type: None,
|
|
transport_config: None,
|
|
auth_type: None,
|
|
auth_value: None,
|
|
enabled: None,
|
|
env_config: None, // Don't update
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// env_config should still be there
|
|
assert!(updated.env_config.is_some());
|
|
let env_map = get_server_env_config(&conn, &server.id).unwrap().unwrap();
|
|
assert_eq!(env_map.get("ORIGINAL").unwrap(), "value");
|
|
}
|
|
}
|