feat(mcp): add mcp_tools allowlist config surfaces across roles, sessions, agents, graphs, and skills

This commit is contained in:
Dark-Alex-17 committed 2026-08-27 14:13:35 -06:00
1 parent 5711432ac4
commit a92ebb5b94
9 files changed
+480

No files matched your search

+142
View File
@@ -214,6 +214,19 @@ impl GraphValidator {
return;
};
let expand_alias = |name: &str| {
ctx.app_config
.mapping_mcp_servers
.get(name)
.into_iter()
.flat_map(|mapped| mapped.split(',').map(|s| s.trim().to_string()))
};
let mut enabled_servers: HashSet<String> = ctx.mcp_servers.clone();
for server in &ctx.mcp_servers {
enabled_servers.extend(expand_alias(server));
}
let all_servers_enabled = ctx.mcp_servers.iter().any(|s| s.trim() == "all");
for (node_id, node) in &graph.nodes {
let NodeType::Llm(llm) = &node.node_type else {
continue;
@@ -237,6 +250,23 @@ impl GraphValidator {
}
}
if let Some(mcp_tools) = &llm.mcp_tools
&& !all_servers_enabled
{
for key in mcp_tools.keys() {
let enabled = enabled_servers.contains(key)
|| expand_alias(key).any(|id| enabled_servers.contains(&id));
if !enabled {
result.error(ValidationError::with_node(
node_id,
format!(
"llm node 'mcp_tools' references MCP server '{key}' not enabled by this graph"
),
));
}
}
}
if let Some(model_id) = &llm.model
&& Model::retrieve_model(ctx.app_config.as_ref(), model_id, ModelType::Chat)
.is_err()
@@ -1001,6 +1031,7 @@ mod tests {
max_concurrent_jobs: None,
global_tools: Vec::new(),
mcp_servers: Vec::new(),
mcp_tools: None,
skills_enabled: None,
enabled_skills: None,
inject_skill_instructions: None,
@@ -1099,6 +1130,7 @@ mod tests {
instructions: None,
prompt: "p".into(),
tools: None,
mcp_tools: None,
model: None,
temperature: None,
top_p: None,
@@ -1257,6 +1289,19 @@ mod tests {
node
}
fn llm_node_with_mcp_tools(id: &str, servers: &[&str]) -> Node {
let mut node = llm_node(id, None, Some("end"));
if let NodeType::Llm(ref mut n) = node.node_type {
let mut mcp_tools = IndexMap::new();
for server in servers {
mcp_tools.insert(server.to_string(), vec!["get_*".to_string()]);
}
n.mcp_tools = Some(mcp_tools);
}
node
}
#[test]
fn llm_node_unknown_tool_is_an_error() {
let graph = graph_with(
@@ -1340,6 +1385,103 @@ mod tests {
assert!(result.is_valid());
}
#[test]
fn llm_node_mcp_tools_enabled_server_passes() {
let graph = graph_with(
vec![
("l", llm_node_with_mcp_tools("l", &["github"])),
("end", end_node("end")),
],
"l",
);
let result = validator()
.with_agent_context(agent_ctx(&[], &["github"]))
.validate(&graph);
assert!(result.is_valid());
}
#[test]
fn llm_node_mcp_tools_unknown_server_is_an_error() {
let graph = graph_with(
vec![
("l", llm_node_with_mcp_tools("l", &["slack"])),
("end", end_node("end")),
],
"l",
);
let result = validator()
.with_agent_context(agent_ctx(&[], &["github"]))
.validate(&graph);
assert!(!result.is_valid());
assert!(
result
.errors
.iter()
.any(|e| e.message.contains("'slack' not enabled"))
);
}
#[test]
fn llm_node_mcp_tools_alias_key_passes() {
let graph = graph_with(
vec![
("l", llm_node_with_mcp_tools("l", &["gh"])),
("end", end_node("end")),
],
"l",
);
let mut ctx = agent_ctx(&[], &["github-mcp"]);
let mut app = AppConfig::default();
app.mapping_mcp_servers
.insert("gh".to_string(), "github-mcp".to_string());
ctx.app_config = Arc::new(app);
let result = validator().with_agent_context(ctx).validate(&graph);
assert!(result.is_valid(), "errors: {:?}", result.errors);
}
#[test]
fn llm_node_mcp_tools_key_matching_alias_expansion_passes() {
let graph = graph_with(
vec![
("l", llm_node_with_mcp_tools("l", &["github-mcp"])),
("end", end_node("end")),
],
"l",
);
let mut ctx = agent_ctx(&[], &["gh"]);
let mut app = AppConfig::default();
app.mapping_mcp_servers
.insert("gh".to_string(), "github-mcp".to_string());
ctx.app_config = Arc::new(app);
let result = validator().with_agent_context(ctx).validate(&graph);
assert!(result.is_valid(), "errors: {:?}", result.errors);
}
#[test]
fn llm_node_mcp_tools_with_all_sentinel_passes() {
let graph = graph_with(
vec![
("l", llm_node_with_mcp_tools("l", &["github"])),
("end", end_node("end")),
],
"l",
);
let result = validator()
.with_agent_context(agent_ctx(&[], &["all"]))
.validate(&graph);
assert!(result.is_valid(), "errors: {:?}", result.errors);
}
#[test]
fn llm_node_unknown_model_is_an_error() {
let graph = graph_with(