use std::collections::BTreeMap;
use serde_json::json;
use shuvarie_config::PermissionsConfig;
use shuvarie_llm::{ToolResult, ToolSet};
use shuvarie_mcp::testkit;
use super::all_tools;
use crate::Skills;
use crate::permissions::{AskScope, PermissionAnswer};
use crate::question::QuestionGate;
use crate::scenes::ToolScene;
use crate::test_util::{access_with_answering_gate, new_ctx};
use crate::tools::{FileLocks, ReadCache, ShellOutputTx, todos};
fn config_from_kdl(text: &str) -> shuvarie_config::Config {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("shuvarie.kdl");
std::fs::write(&path, text).expect("write config");
shuvarie_config::Config::load_from(&path).expect("parse config")
}
fn roster(
config: &shuvarie_config::Config,
access: crate::permissions::Access,
mcp: Option<&shuvarie_mcp::SharedMcpManager>,
mcp_roster: &BTreeMap<String, shuvarie_mcp::McpToolMap>,
) -> ToolSet {
let (question_tx, _question_rx) = tokio::sync::mpsc::channel(1);
let (shell_tx, _shell_rx) = tokio::sync::mpsc::channel(1);
let lsp = std::sync::Arc::new(tokio::sync::Mutex::new(shuvarie_lsp::LspManager::new(
std::path::PathBuf::from("."),
false,
Default::default(),
)));
let tools = all_tools(
lsp,
FileLocks::new(),
ReadCache::new(),
100,
100,
QuestionGate::new(question_tx),
access,
ShellOutputTx::new(shell_tx),
crate::shell::resolve(None).shell,
todos::TodoState::from_records(&[]),
&ToolScene::default(),
config.tools.web_search.as_ref(),
&Skills::default(),
&config.tools.tools,
mcp,
mcp_roster,
);
ToolSet::from_dynamic_tools(tools)
}
fn definition(set: &ToolSet, name: &str) -> shuvarie_llm::ToolDefinition {
set.get_tool_definitions()
.into_iter()
.find(|d| d.name == name)
.unwrap_or_else(|| panic!("no definition for {name}"))
}
fn text(result: &ToolResult) -> &str {
result
.output()
.as_text()
.expect("result carries model-visible text")
}
const MIXED_KDL: &str = r#"
tools {
tool name="greet" {
description "Greet someone by name"
cmd "printf" "%s\n" "hello {{msg}}"
timeout 30
params {
param "msg" type="string" required=#true description="Who to greet"
}
}
mcp {
stdio name="tester" {
command "shuvarie-e2e-never-spawned"
}
}
}
"#;
#[tokio::test]
async fn kdl_config_reaches_a_live_mcp_tool_through_the_canonical_dispatch() {
let config = config_from_kdl(MIXED_KDL);
assert!(config.tools.tools.contains_key("greet"));
assert!(config.tools.mcp.stdio.contains_key("tester"));
let mcp = crate::mcp_manager::mcp_manager(&config.tools.mcp);
let (roster_map, _server) = {
let mut manager = mcp.lock().await;
assert!(
manager.specs().contains_key("tester"),
"specs came from the config"
);
let (conn, server) = testkit::duplex_connection("tester").await;
manager.inject_connection("tester", conn);
manager.ensure_connected("tester").await.expect("connected");
(manager.roster(), server)
};
let (access, mut requests) = access_with_answering_gate(&PermissionsConfig::builtin());
let set = roster(&config, access, Some(&mcp), &roster_map);
assert!(set.contains("greet"));
assert!(set.contains("mcp__tester__echo"));
let greet = definition(&set, "greet");
assert_eq!(greet.description, "Greet someone by name");
assert!(
greet.parameters["properties"]["msg"].is_object(),
"params: {}",
greet.parameters
);
let echo = definition(&set, "mcp__tester__echo");
assert_eq!(echo.description, "Echo a message");
assert!(
echo.parameters["properties"]["message"].is_object(),
"params: {}",
echo.parameters
);
let mut ctx = new_ctx();
let (result, _) = tokio::join!(
set.execute(
"mcp__tester__echo",
json!({"message": "asked"}).to_string(),
&mut ctx
),
async {
let request = tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::task::yield_now().await;
requests.recv().await
})
.await
.expect("no permission request within 5s")
.expect("permission request");
assert!(
request
.description
.contains("Allow tool `mcp__tester__echo`?"),
"got: {}",
request.description
);
assert_eq!(
request.scope,
Some(AskScope::Tool("mcp__tester__echo".to_string()))
);
request
.respond
.send(PermissionAnswer::AllowSession)
.expect("answer sent");
}
);
assert!(result.error().is_none(), "result: {result:?}");
assert_eq!(text(&result), "echo: asked");
assert!(requests.try_recv().is_err());
let mut ctx = new_ctx();
let result = set
.execute(
"mcp__tester__echo",
json!({"message": "granted"}).to_string(),
&mut ctx,
)
.await;
assert!(requests.try_recv().is_err(), "grant covers later calls");
assert_eq!(text(&result), "echo: granted");
let mut ctx = new_ctx();
let (result, _) = tokio::join!(set.execute("mcp__tester__fail", "{}", &mut ctx), async {
let request = tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::task::yield_now().await;
requests.recv().await
})
.await
.expect("no permission request within 5s")
.expect("permission request");
assert_eq!(
request.scope,
Some(AskScope::Tool("mcp__tester__fail".to_string()))
);
request
.respond
.send(PermissionAnswer::AllowSession)
.expect("answer sent");
});
assert!(result.error().is_some(), "result: {result:?}");
assert!(text(&result).contains("boom"), "text: {}", text(&result));
}
#[cfg(unix)]
#[tokio::test]
async fn kdl_config_spawns_a_real_subprocess_through_the_canonical_dispatch() {
let config = config_from_kdl(MIXED_KDL);
let (access, mut requests) = access_with_answering_gate(&PermissionsConfig::builtin());
let set = roster(&config, access, None, &Default::default());
let mut ctx = new_ctx();
let (result, _) = tokio::join!(
set.execute("greet", json!({"msg": "e2e"}).to_string(), &mut ctx),
async {
let request = tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::task::yield_now().await;
requests.recv().await
})
.await
.expect("no permission request within 5s")
.expect("permission request");
assert_eq!(request.scope, Some(AskScope::Tool("greet".to_string())));
request
.respond
.send(PermissionAnswer::AllowSession)
.expect("answer sent");
}
);
assert!(
result.error().is_none(),
"result: {result:?} output: {:?}",
result.output().as_text()
);
assert_eq!(text(&result), "hello e2e\n");
let mut ctx = new_ctx();
let result = set
.execute("greet", json!({"msg": "again"}).to_string(), &mut ctx)
.await;
assert!(requests.try_recv().is_err(), "grant covers later calls");
assert_eq!(text(&result), "hello again\n");
let mut ctx = new_ctx();
let result = set
.execute(
"greet",
json!({"msg": "x", "extra": 1}).to_string(),
&mut ctx,
)
.await;
assert!(result.error().is_some(), "result: {result:?}");
}
fn example_binary() -> Option<String> {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../target/debug/examples/mcp-test-server"
);
std::path::Path::new(path)
.exists()
.then(|| path.to_string())
}
#[cfg(unix)]
#[tokio::test]
async fn kdl_config_spawns_a_real_mcp_child_process_end_to_end() {
let Some(bin) = example_binary() else {
eprintln!("skipping: mcp-test-server example binary not found");
return;
};
let kdl = format!(
r#"
tools {{
mcp {{
stdio name="tester" {{
command "{bin}"
}}
}}
}}
"#
);
let config = config_from_kdl(&kdl);
let mcp = crate::mcp_manager::mcp_manager(&config.tools.mcp);
let (roster_map, status) = {
let mut manager = mcp.lock().await;
manager
.ensure_connected("tester")
.await
.expect("spawn server");
let snapshot = manager.status_snapshot();
(manager.roster(), snapshot)
};
assert!(mcp.lock().await.connected("tester"));
assert_eq!(
status.first().map(|s| (s.name.as_str(), s.state)),
Some(("tester", shuvarie_mcp::McpStatusState::Connected))
);
let (access, mut requests) = access_with_answering_gate(&PermissionsConfig::builtin());
let set = roster(&config, access, Some(&mcp), &roster_map);
assert!(set.contains("mcp__tester__echo"));
let mut ctx = new_ctx();
let (result, _) = tokio::join!(
set.execute(
"mcp__tester__echo",
json!({"message": "hello child process"}).to_string(),
&mut ctx,
),
async {
let request = tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::task::yield_now().await;
requests.recv().await
})
.await
.expect("no permission request within 5s")
.expect("permission request");
assert_eq!(
request.scope,
Some(AskScope::Tool("mcp__tester__echo".to_string()))
);
request
.respond
.send(PermissionAnswer::AllowSession)
.expect("answer sent");
}
);
assert!(result.error().is_none(), "result: {result:?}");
assert_eq!(text(&result), "echo: hello child process");
}