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

+61
View File
@@ -37,6 +37,8 @@ pub struct Skill {
#[serde(skip_serializing_if = "Option::is_none")]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
mcp_tools: Option<IndexMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_unload: Option<bool>,
}
@@ -74,6 +76,9 @@ impl Skill {
"enabled_mcp_servers" => {
skill.enabled_mcp_servers = parse_skill_string_or_array(value);
}
"mcp_tools" => {
skill.mcp_tools = parse_skill_mcp_tools_map(value);
}
"auto_unload" => {
skill.auto_unload = value.as_bool();
}
@@ -147,6 +152,11 @@ impl Skill {
self.enabled_mcp_servers.as_deref()
}
#[allow(dead_code)]
pub fn mcp_tools(&self) -> Option<&IndexMap<String, Vec<String>>> {
self.mcp_tools.as_ref()
}
pub fn auto_unload(&self) -> bool {
self.auto_unload.unwrap_or(false)
}
@@ -185,6 +195,19 @@ fn parse_skill_string_or_array(value: &Value) -> Option<Vec<String>> {
None
}
fn parse_skill_mcp_tools_map(value: &Value) -> Option<IndexMap<String, Vec<String>>> {
let map = value.as_object()?;
let mut mcp_tools = IndexMap::new();
for (server, tools) in map {
if tools.is_null() {
mcp_tools.insert(server.clone(), Vec::new());
} else if let Some(tools) = parse_skill_string_or_array(tools) {
mcp_tools.insert(server.clone(), tools);
}
}
Some(mcp_tools)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -198,6 +221,44 @@ mod tests {
assert_eq!(skill.description(), "");
}
#[test]
fn skill_new_parses_mcp_tools_list_and_csv_values() {
let content = "---\nmcp_tools:\n github: [get_*, list_*]\n slack: a,b\n---\nBody";
let skill = Skill::new("test", content);
let mcp_tools = skill.mcp_tools().unwrap();
assert_eq!(
mcp_tools.get("github"),
Some(&vec!["get_*".to_string(), "list_*".to_string()])
);
assert_eq!(
mcp_tools.get("slack"),
Some(&vec!["a".to_string(), "b".to_string()])
);
}
#[test]
fn skill_new_mcp_tools_absent_is_none() {
let skill = Skill::new("test", "---\ndescription: d\n---\nBody");
assert_eq!(skill.mcp_tools(), None);
}
#[test]
fn skill_new_mcp_tools_empty_list_server_is_some_empty() {
let skill = Skill::new("test", "---\nmcp_tools:\n github: []\n---\nBody");
assert_eq!(skill.mcp_tools().unwrap().get("github"), Some(&vec![]));
}
#[test]
fn skill_new_mcp_tools_per_server_null_is_some_empty() {
let skill = Skill::new("test", "---\nmcp_tools:\n github:\n---\nBody");
assert_eq!(skill.mcp_tools().unwrap().get("github"), Some(&vec![]));
}
#[test]
fn skill_new_parses_full_metadata() {
let content = "---\n\