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

This commit is contained in:
2026-08-27 14:13:35 -06:00
parent 5711432ac4
commit a92ebb5b94
9 changed files with 480 additions and 0 deletions
+62
View File
@@ -40,6 +40,8 @@ pub struct Session {
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
mcp_tools: Option<IndexMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
skills_enabled: Option<bool>,
#[serde(
@@ -249,6 +251,9 @@ impl Session {
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
data["enabled_mcp_servers"] = json!(enabled_mcp_servers);
}
if let Some(mcp_tools) = self.mcp_tools() {
data["mcp_tools"] = json!(mcp_tools);
}
if let Some(skills_enabled) = self.skills_enabled() {
data["skills_enabled"] = skills_enabled.into();
}
@@ -329,6 +334,13 @@ impl Session {
items.push(("enabled_mcp_servers", enabled_mcp_servers.join(",")));
}
if let Some(mcp_tools) = self.mcp_tools() {
items.push((
"mcp_tools",
serde_json::to_string(&mcp_tools).unwrap_or_default(),
));
}
if let Some(skills_enabled) = self.skills_enabled() {
items.push(("skills_enabled", skills_enabled.to_string()));
}
@@ -870,6 +882,10 @@ impl RoleLike for Session {
self.enabled_mcp_servers.clone()
}
fn mcp_tools(&self) -> Option<IndexMap<String, Vec<String>>> {
self.mcp_tools.clone()
}
fn set_model(&mut self, model: Model) {
if self.model().id() != model.id() {
self.model_id = model.id();
@@ -913,6 +929,13 @@ impl RoleLike for Session {
self.dirty = true;
}
}
fn set_mcp_tools(&mut self, value: Option<IndexMap<String, Vec<String>>>) {
if self.mcp_tools != value {
self.mcp_tools = value;
self.dirty = true;
}
}
}
#[derive(Debug, Clone, Default)]
@@ -1044,6 +1067,45 @@ mod tests {
assert_eq!(session.enabled_macros, None);
}
#[test]
fn session_mcp_tools_survives_yaml_round_trip() {
let mut session = Session::default();
let mut mcp_tools = IndexMap::new();
mcp_tools.insert("github".to_string(), vec!["get_*".to_string()]);
mcp_tools.insert("slack".to_string(), vec![]);
session.set_mcp_tools(Some(mcp_tools.clone()));
let yaml = serde_yaml::to_string(&session).unwrap();
let reloaded: Session = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(reloaded.mcp_tools(), Some(mcp_tools));
}
#[test]
fn session_set_role_does_not_copy_mcp_tools() {
let role = Role::new(
"test",
"---\nmcp_tools:\n github: [get_issue]\n---\nPrompt",
);
assert!(role.mcp_tools().is_some());
let mut session = Session::default();
session.set_role(role);
assert_eq!(session.mcp_tools(), None);
}
#[test]
fn session_set_mcp_tools_marks_dirty() {
let mut session = Session::default();
assert!(!session.dirty());
session.set_mcp_tools(Some(IndexMap::new()));
assert!(session.dirty());
assert_eq!(session.mcp_tools(), Some(IndexMap::new()));
}
#[test]
fn session_enabled_macros_empty_list_is_some_empty() {
let session: Session =