301 lines
8.6 KiB
Rust
301 lines
8.6 KiB
Rust
use serde_json::{json, Map, Value};
|
|
use galaxy_cli::mcp::MCPSpec;
|
|
|
|
use super::build_mcp_servers_from_specs;
|
|
|
|
fn build(specs: Vec<MCPSpec>) -> Map<String, Value> {
|
|
build_mcp_servers_from_specs(&specs)
|
|
.expect("builder should not error")
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[test]
|
|
fn empty_specs_returns_none() {
|
|
assert!(build_mcp_servers_from_specs(&[]).unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn uuid_spec_is_coerced_to_warp_id() {
|
|
let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
|
|
let servers = build(vec![MCPSpec::Uuid(uuid)]);
|
|
|
|
let entry = servers.get(&uuid.to_string()).unwrap();
|
|
assert_eq!(
|
|
entry["warp_id"].as_str(),
|
|
Some("550e8400-e29b-41d4-a716-446655440000")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wrapper_mcp_servers_is_unpacked() {
|
|
let spec = json!({
|
|
"mcpServers": {
|
|
"github": { "command": "npx", "args": ["-y", "server"] }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("github"));
|
|
assert_eq!(servers["github"]["command"].as_str(), Some("npx"));
|
|
assert!(servers.get("mcpServers").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn wrapper_mcp_servers_snake_case_is_unpacked() {
|
|
let spec = json!({
|
|
"mcp_servers": {
|
|
"s": { "url": "https://example.com/mcp" }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("s"));
|
|
assert_eq!(
|
|
servers["s"]["url"].as_str(),
|
|
Some("https://example.com/mcp")
|
|
);
|
|
assert!(servers.get("mcp_servers").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn wrapper_servers_is_unpacked() {
|
|
let spec = json!({
|
|
"servers": {
|
|
"s": { "command": "python", "args": ["mcp.py"] }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("s"));
|
|
assert_eq!(servers["s"]["command"].as_str(), Some("python"));
|
|
assert!(servers.get("servers").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn wrapper_mcp_dot_servers_is_unpacked() {
|
|
let spec = json!({
|
|
"mcp": {
|
|
"servers": {
|
|
"s": { "url": "https://example.com/mcp" }
|
|
}
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("s"));
|
|
assert_eq!(
|
|
servers["s"]["url"].as_str(),
|
|
Some("https://example.com/mcp")
|
|
);
|
|
assert!(servers.get("mcp").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn plain_map_is_accepted() {
|
|
let spec = json!({
|
|
"github": { "command": "npx", "args": [] },
|
|
"remote": { "url": "https://example.com/mcp" }
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("github"));
|
|
assert!(servers.contains_key("remote"));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_outer_braces_is_accepted() {
|
|
// Emulate copying docs that omit the top-level `{}`.
|
|
let full = json!({
|
|
"mcpServers": {
|
|
"s": { "command": "npx", "args": [] }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let inner = &full[1..full.len() - 1];
|
|
let spec = format!(" {inner} ");
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("s"));
|
|
assert_eq!(servers["s"]["command"].as_str(), Some("npx"));
|
|
}
|
|
|
|
#[test]
|
|
fn single_server_shorthand_command_is_wrapped() {
|
|
let spec = json!({ "command": "npx", "args": [] }).to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert_eq!(servers.len(), 1);
|
|
let (_name, config) = servers.iter().next().unwrap();
|
|
assert_eq!(config["command"].as_str(), Some("npx"));
|
|
}
|
|
|
|
#[test]
|
|
fn command_without_args_is_accepted() {
|
|
// args should be optional for command-based MCP servers
|
|
let spec = json!({
|
|
"mcpServers": {
|
|
"s": { "command": "uvx" }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert!(servers.contains_key("s"));
|
|
assert_eq!(servers["s"]["command"].as_str(), Some("uvx"));
|
|
}
|
|
|
|
#[test]
|
|
fn single_server_shorthand_url_is_wrapped() {
|
|
let spec = json!({ "url": "https://example.com/mcp" }).to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
|
|
assert_eq!(servers.len(), 1);
|
|
let (_name, config) = servers.iter().next().unwrap();
|
|
assert_eq!(config["url"].as_str(), Some("https://example.com/mcp"));
|
|
}
|
|
|
|
#[test]
|
|
fn merge_multiple_specs_and_duplicate_name_errors() {
|
|
let s1 = json!({ "mcpServers": { "a": { "command": "npx", "args": [] } } }).to_string();
|
|
let s2 = json!({ "mcpServers": { "b": { "url": "https://example.com/mcp" } } }).to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(s1.clone()), MCPSpec::Json(s2)]);
|
|
assert!(servers.contains_key("a"));
|
|
assert!(servers.contains_key("b"));
|
|
|
|
let err =
|
|
build_mcp_servers_from_specs(&[MCPSpec::Json(s1.clone()), MCPSpec::Json(s1)]).unwrap_err();
|
|
|
|
assert!(err.to_string().contains("Duplicate MCP server name 'a'"));
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_escaped_strings_in_env_values() {
|
|
let spec = json!({
|
|
"mcpServers": {
|
|
"s": {
|
|
"command": "npx",
|
|
"args": [],
|
|
"env": {
|
|
"TOKEN": "a\"b\\c\n"
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let servers = build(vec![MCPSpec::Json(spec)]);
|
|
let token = servers["s"]["env"]["TOKEN"].as_str().unwrap();
|
|
|
|
// `serde_json` will decode escapes.
|
|
assert_eq!(token, "a\"b\\c\n");
|
|
}
|
|
|
|
#[test]
|
|
fn validation_rejects_invalid_entries() {
|
|
// Both command and url.
|
|
let spec = json!({
|
|
"mcpServers": {
|
|
"bad": { "command": "npx", "url": "https://example.com" }
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err
|
|
.to_string()
|
|
.contains("must have exactly one of: 'warp_id', 'command', or 'url'"));
|
|
|
|
// warp_id must be a UUID string.
|
|
let spec = json!({ "mcpServers": { "bad": { "warp_id": "not-a-uuid" } } }).to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("field 'warp_id' must be a UUID"));
|
|
|
|
// args must be array.
|
|
let spec = json!({ "mcpServers": { "bad": { "command": "npx", "args": "nope" } } }).to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("field 'args' must be an array"));
|
|
|
|
// args entries must be strings.
|
|
let spec = json!({ "mcpServers": { "bad": { "command": "npx", "args": [1] } } }).to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("args[0]"));
|
|
|
|
// env values must be strings.
|
|
let spec = json!({
|
|
"mcpServers": { "bad": { "command": "npx", "args": [], "env": { "X": 1 } } }
|
|
})
|
|
.to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("env.X"));
|
|
|
|
// headers values must be strings.
|
|
let spec = json!({
|
|
"mcpServers": { "bad": { "url": "https://example.com", "headers": { "X": 1 } } }
|
|
})
|
|
.to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("headers.X"));
|
|
|
|
// server config must be an object.
|
|
let spec = json!({ "mcpServers": { "bad": 1 } }).to_string();
|
|
let err = build_mcp_servers_from_specs(&[MCPSpec::Json(spec)]).unwrap_err();
|
|
assert!(err.to_string().contains("config must be a JSON object"));
|
|
}
|
|
|
|
#[test]
|
|
fn serializes_mcp_servers_as_object_not_string() {
|
|
use crate::ai::ambient_agents::AgentConfigSnapshot;
|
|
use crate::server::server_api::ai::SpawnAgentRequest;
|
|
|
|
let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
|
|
let mcp_servers = build_mcp_servers_from_specs(&[MCPSpec::Uuid(uuid)])
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
let request = SpawnAgentRequest {
|
|
prompt: "hello".to_string(),
|
|
config: Some(AgentConfigSnapshot {
|
|
mcp_servers: Some(mcp_servers),
|
|
..Default::default()
|
|
}),
|
|
title: None,
|
|
team: None,
|
|
skill: None,
|
|
attachments: vec![],
|
|
interactive: None,
|
|
parent_run_id: None,
|
|
runtime_skills: vec![],
|
|
referenced_attachments: vec![],
|
|
};
|
|
|
|
let value = serde_json::to_value(&request).unwrap();
|
|
|
|
let config = value.get("config").unwrap();
|
|
let mcp_servers = config.get("mcp_servers").unwrap();
|
|
|
|
assert!(mcp_servers.is_object());
|
|
assert!(mcp_servers.get("mcpServers").is_none());
|
|
|
|
let server = mcp_servers.get(uuid.to_string()).unwrap();
|
|
assert_eq!(
|
|
server.get("warp_id").unwrap(),
|
|
&Value::String(uuid.to_string())
|
|
);
|
|
}
|