7 Commits

28 changed files with 449 additions and 234 deletions
+3 -1
View File
@@ -243,7 +243,9 @@ instructions: |
When you write or modify files yourself (rather than delegating to coder): When you write or modify files yourself (rather than delegating to coder):
- **For writing files**, ALWAYS use `fs_write` (new file / full overwrite) or `fs_patch` (surgical edit). NEVER write files via `execute_command`. Do not use: - **For editing an existing file**, prefer `fs_patch`. It's a surgical edit that preserves unchanged content. Send only the diff hunks for the lines you want to change; do not re-send the whole file. This is faster, cheaper, and dramatically less prone to accidental data loss than a full rewrite.
- **For writing a NEW file or doing a COMPLETE rewrite**, use `fs_write`. Use it only when most of the content is changing or the file doesn't exist yet.
- **NEVER write files via `execute_command`.** Do not use:
- `cat > file`, `cat >> file`, `tee` - `cat > file`, `cat >> file`, `tee`
- `echo >`, `printf >` - `echo >`, `printf >`
- Heredocs (`<<EOF`, `<<-EOF`, `<<'EOF'`) - Heredocs (`<<EOF`, `<<-EOF`, `<<'EOF'`)
+3 -2
View File
@@ -32,7 +32,7 @@ def main():
agent_data = parse_raw_data(raw_data) agent_data = parse_raw_data(raw_data)
root_dir = "{config_dir}" root_dir = "{config_dir}"
setup_env(root_dir, agent_func) setup_env(root_dir, agent_func, raw_data)
agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py") agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py")
run(agent_tools_path, agent_func, agent_data) run(agent_tools_path, agent_func, agent_data)
@@ -65,13 +65,14 @@ def parse_argv():
return agent_func, agent_data return agent_func, agent_data
def setup_env(root_dir, agent_func): def setup_env(root_dir, agent_func, raw_data):
load_env(os.path.join(root_dir, ".env")) load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_ROOT_DIR"] = root_dir os.environ["LLM_ROOT_DIR"] = root_dir
os.environ["LLM_AGENT_NAME"] = "{agent_name}" os.environ["LLM_AGENT_NAME"] = "{agent_name}"
os.environ["LLM_AGENT_FUNC"] = agent_func os.environ["LLM_AGENT_FUNC"] = agent_func
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}") os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}")
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}") os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
def load_env(file_path): def load_env(file_path):
+1
View File
@@ -32,6 +32,7 @@ setup_env() {
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}" export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}"
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}" export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}" export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_AGENT_RAW_JSON="$agent_data"
} }
load_env() { load_env() {
+3 -2
View File
@@ -11,7 +11,7 @@ async function main(): Promise<void> {
const agentData = parseRawData(rawData); const agentData = parseRawData(rawData);
const configDir = "{config_dir}"; const configDir = "{config_dir}";
setupEnv(configDir, agentFunc); setupEnv(configDir, agentFunc, rawData);
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts"); const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
await run(agentToolsPath, agentFunc, agentData); await run(agentToolsPath, agentFunc, agentData);
@@ -48,13 +48,14 @@ function parseArgv(): { agentFunc: string; rawData: string } {
return { agentFunc, rawData: agentData }; return { agentFunc, rawData: agentData };
} }
function setupEnv(configDir: string, agentFunc: string): void { function setupEnv(configDir: string, agentFunc: string, rawData: string): void {
loadEnv(join(configDir, ".env")); loadEnv(join(configDir, ".env"));
process.env["LLM_ROOT_DIR"] = configDir; process.env["LLM_ROOT_DIR"] = configDir;
process.env["LLM_AGENT_NAME"] = "{agent_name}"; process.env["LLM_AGENT_NAME"] = "{agent_name}";
process.env["LLM_AGENT_FUNC"] = agentFunc; process.env["LLM_AGENT_FUNC"] = agentFunc;
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}"); process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}"); process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
process.env["LLM_AGENT_RAW_JSON"] = rawData;
} }
function loadEnv(filePath: string): void { function loadEnv(filePath: string): void {
+3 -2
View File
@@ -32,7 +32,7 @@ def main():
tool_data = parse_raw_data(raw_data) tool_data = parse_raw_data(raw_data)
root_dir = "{root_dir}" root_dir = "{root_dir}"
setup_env(root_dir) setup_env(root_dir, raw_data)
tool_path = "{tool_path}.py" tool_path = "{tool_path}.py"
run(tool_path, "run", tool_data) run(tool_path, "run", tool_data)
@@ -65,11 +65,12 @@ def parse_argv():
return tool_data return tool_data
def setup_env(root_dir): def setup_env(root_dir, raw_data):
load_env(os.path.join(root_dir, ".env")) load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_ROOT_DIR"] = root_dir os.environ["LLM_ROOT_DIR"] = root_dir
os.environ["LLM_TOOL_NAME"] = "{function_name}" os.environ["LLM_TOOL_NAME"] = "{function_name}"
os.environ["LLM_TOOL_CACHE_DIR"] = os.path.join(root_dir, "cache", "{function_name}") os.environ["LLM_TOOL_CACHE_DIR"] = os.path.join(root_dir, "cache", "{function_name}")
os.environ["LLM_TOOL_RAW_JSON"] = raw_data
def load_env(file_path): def load_env(file_path):
+1
View File
@@ -29,6 +29,7 @@ setup_env() {
export LLM_TOOL_NAME="{function_name}" export LLM_TOOL_NAME="{function_name}"
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}" export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}" export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_TOOL_RAW_JSON="$tool_data"
} }
load_env() { load_env() {
+3 -2
View File
@@ -11,7 +11,7 @@ async function main(): Promise<void> {
const toolData = parseRawData(rawData); const toolData = parseRawData(rawData);
const rootDir = "{root_dir}"; const rootDir = "{root_dir}";
setupEnv(rootDir); setupEnv(rootDir, rawData);
const toolPath = "{tool_path}.ts"; const toolPath = "{tool_path}.ts";
await run(toolPath, "run", toolData); await run(toolPath, "run", toolData);
@@ -45,11 +45,12 @@ function parseArgv(): string {
return toolData; return toolData;
} }
function setupEnv(rootDir: string): void { function setupEnv(rootDir: string, rawData: string): void {
loadEnv(join(rootDir, ".env")); loadEnv(join(rootDir, ".env"));
process.env["LLM_ROOT_DIR"] = rootDir; process.env["LLM_ROOT_DIR"] = rootDir;
process.env["LLM_TOOL_NAME"] = "{function_name}"; process.env["LLM_TOOL_NAME"] = "{function_name}";
process.env["LLM_TOOL_CACHE_DIR"] = join(rootDir, "cache", "{function_name}"); process.env["LLM_TOOL_CACHE_DIR"] = join(rootDir, "cache", "{function_name}");
process.env["LLM_TOOL_RAW_JSON"] = rawData;
} }
function loadEnv(filePath: string): void { function loadEnv(filePath: string): void {
@@ -10,6 +10,9 @@ set -e
source "$LLM_PROMPT_UTILS_FILE" source "$LLM_PROMPT_UTILS_FILE"
main() { main() {
# shellcheck disable=SC2154
argc_command="$(jq -r '.command' <<< "$LLM_TOOL_RAW_JSON")"
guard_operation guard_operation
local script local script
script="$(mktemp)" script="$(mktemp)"
@@ -14,6 +14,8 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154 # shellcheck disable=SC2154
main() { main() {
argc_code="$(jq -r '.code' <<< "$LLM_TOOL_RAW_JSON")"
if ! grep -qi '^select' <<<"$argc_code"; then if ! grep -qi '^select' <<<"$argc_code"; then
guard_operation "" guard_operation ""
fi fi
+7 -2
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
# @describe Apply a patch to a file at the specified path. # @describe Apply a unified-diff patch to a file at the specified path. Use this for editing an existing file. It's the
# This can be used to edit a file without having to rewrite the whole file. # PREFERRED way to modify a file. Prefer this over fs_write whenever the file already exists: it sends less data,
# preserves unchanged content automatically, and is less prone to accidental data loss from full rewrites.
# Use fs_write only when you are creating a new file or doing a complete rewrite where most of the content changes.
# @option --path! The path of the file to apply the patch to # @option --path! The path of the file to apply the patch to
# @option --contents! The patch to apply to the file # @option --contents! The patch to apply to the file
@@ -14,6 +16,9 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154 # shellcheck disable=SC2154
main() { main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
if [[ ! -f "$argc_path" ]]; then if [[ ! -f "$argc_path" ]]; then
error "Unable to find the specified file: $argc_path" error "Unable to find the specified file: $argc_path"
exit 1 exit 1
+6 -1
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
# @describe Write the full file contents to a file at the specified path. # @describe Write the FULL file contents to a file at the specified path. Use this for NEW files or COMPLETE rewrites
# only. For editing an existing file, prefer fs_patch. It's a surgical edit that preserves unchanged content, requires
# sending less data, and is less prone to accidental data loss.
# @option --path! The path of the file to write to # @option --path! The path of the file to write to
# @option --contents! The full contents to write to the file # @option --contents! The full contents to write to the file
@@ -13,6 +15,9 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154 # shellcheck disable=SC2154
main() { main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
if [[ -f "$argc_path" ]]; then if [[ -f "$argc_path" ]]; then
printf "%s" "$argc_contents" | git diff --no-index "$argc_path" - || true printf "%s" "$argc_contents" | git diff --no-index "$argc_path" - || true
guard_operation "Apply changes?" guard_operation "Apply changes?"
+4
View File
@@ -14,6 +14,10 @@ set -e
# shellcheck disable=SC2154 # shellcheck disable=SC2154
main() { main() {
argc_recipient="$(jq -r '.recipient' <<< "$LLM_TOOL_RAW_JSON")"
argc_subject="$(jq -r '.subject' <<< "$LLM_TOOL_RAW_JSON")"
argc_body="$(jq -r '.body' <<< "$LLM_TOOL_RAW_JSON")"
sender_name="${EMAIL_SENDER_NAME:-$(echo "$EMAIL_SMTP_USER" | awk -F'@' '{print $1}')}" sender_name="${EMAIL_SENDER_NAME:-$(echo "$EMAIL_SMTP_USER" | awk -F'@' '{print $1}')}"
printf "%s\n" "From: $sender_name <$EMAIL_SMTP_USER> printf "%s\n" "From: $sender_name <$EMAIL_SMTP_USER>
To: $argc_recipient To: $argc_recipient
+20 -3
View File
@@ -82,7 +82,14 @@ vault_password_file: null # Path to a file containing the password for th
function_calling_support: true # Enables or disables function calling (Globally). function_calling_support: true # Enables or disables function calling (Globally).
mapping_tools: # Alias for a tool or toolset mapping_tools: # Alias for a tool or toolset
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep' fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_coyote') enabled_tools: null # Which tools to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_tools:
# - fs
# - web_search_coyote
# Example (comma-separated form):
# enabled_tools: fs,web_search_coyote
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools') visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
# - demo_py.py # - demo_py.py
# - demo_sh.sh # - demo_sh.sh
@@ -118,7 +125,14 @@ visible_tools: # Which tools are visible to be compiled (and a
mcp_server_support: true # Enables or disables MCP servers (globally). mcp_server_support: true # Enables or disables MCP servers (globally).
mapping_mcp_servers: # Alias for an MCP server or set of servers mapping_mcp_servers: # Alias for an MCP server or set of servers
git: github,gitmcp git: github,gitmcp
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search') enabled_mcp_servers: null # Which MCP servers to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_mcp_servers:
# - github
# - slack
# Example (comma-separated form):
# enabled_mcp_servers: github,slack,ddg-search
# ---- Skills ---- # ---- Skills ----
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. # Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
@@ -131,10 +145,13 @@ visible_skills: # The universe of skills allowed to be enabled
- frontend-ui-ux - frontend-ui-ux
- git-master - git-master
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible. enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
# Example: only expose two skills in the bare REPL. # Accepts either a YAML list or a comma-separated string.
# Example (list form):
# enabled_skills: # enabled_skills:
# - git-master # - git-master
# - ai-slop-remover # - ai-slop-remover
# Example (comma-separated form):
# enabled_skills: git-master,ai-slop-remover
# ---- Auto-Continue (Todo System) ---- # ---- Auto-Continue (Todo System) ----
# The auto-continue system provides built-in task tracking for improved reliability. # The auto-continue system provides built-in task tracking for improved reliability.
+9 -4
View File
@@ -8,12 +8,17 @@ name: <role-name> # The name of the role
model: openai:gpt-4o # The model to use for this role model: openai:gpt-4o # The model to use for this role
temperature: 0.2 # The temperature to use for this role when querying the model temperature: 0.2 # The temperature to use for this role when querying the model
top_p: 0 # The top_p to use for this role when querying the model top_p: 0 # The top_p to use for this role when querying the model
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role enabled_tools: # Tools to enable for this role. Accepts a YAML list (preferred)
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role - fs_ls # or a comma-separated string (e.g. `enabled_tools: fs_ls,fs_cat`).
- fs_cat # Use `all` to enable every visible tool.
enabled_mcp_servers: # MCP servers to enable for this role. Accepts a YAML list (preferred)
- github # or a comma-separated string (e.g. `enabled_mcp_servers: github,gitmcp`).
- gitmcp # Use `all` to enable every configured MCP server.
skills_enabled: true # Master switch for skills in this role (default: inherit from global). skills_enabled: true # Master switch for skills in this role (default: inherit from global).
# Skills also require `function_calling_support: true` in the global config. # Skills also require `function_calling_support: true` in the global config.
enabled_skills: git-master,ai-slop-remover # Comma-separated list of skills available when this role is active. enabled_skills: # Skills available when this role is active. Accepts a YAML list (preferred)
# Must be a subset of global `visible_skills`. Omit to inherit the global default. - git-master # or a comma-separated string (e.g. `enabled_skills: git-master,ai-slop-remover`).
- ai-slop-remover # Must be a subset of global `visible_skills`. Omit to inherit the global default.
prompt: null # A custom prompt to use for this role that will immediately query prompt: null # A custom prompt to use for this role that will immediately query
# the model for output instead of using the instructions below # the model for output instead of using the instructions below
# Auto-Continue (Todo System) # Auto-Continue (Todo System)
+9 -11
View File
@@ -548,12 +548,12 @@ impl RoleLike for Agent {
self.config.top_p self.config.top_p
} }
fn enabled_tools(&self) -> Option<String> { fn enabled_tools(&self) -> Option<Vec<String>> {
None None
} }
fn enabled_mcp_servers(&self) -> Option<String> { fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
self.config.mcp_servers.clone().join(",").into() Some(self.config.mcp_servers.clone())
} }
fn set_model(&mut self, model: Model) { fn set_model(&mut self, model: Model) {
@@ -569,15 +569,14 @@ impl RoleLike for Agent {
self.config.top_p = value; self.config.top_p = value;
} }
fn set_enabled_tools(&mut self, value: Option<String>) { fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
match value { match value {
Some(tools) => { Some(tools) => {
let tools = tools self.config.global_tools = tools
.split(',') .into_iter()
.map(|v| v.trim().to_string()) .map(|v| v.trim().to_string())
.filter(|v| !v.is_empty()) .filter(|v| !v.is_empty())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.config.global_tools = tools;
} }
None => { None => {
self.config.global_tools.clear(); self.config.global_tools.clear();
@@ -585,15 +584,14 @@ impl RoleLike for Agent {
} }
} }
fn set_enabled_mcp_servers(&mut self, value: Option<String>) { fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
match value { match value {
Some(servers) => { Some(servers) => {
let servers = servers self.config.mcp_servers = servers
.split(',') .into_iter()
.map(|v| v.trim().to_string()) .map(|v| v.trim().to_string())
.filter(|v| !v.is_empty()) .filter(|v| !v.is_empty())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.config.mcp_servers = servers;
} }
None => { None => {
self.config.mcp_servers.clear(); self.config.mcp_servers.clear();
+11 -8
View File
@@ -34,16 +34,19 @@ pub struct AppConfig {
pub function_calling_support: bool, pub function_calling_support: bool,
pub mapping_tools: IndexMap<String, String>, pub mapping_tools: IndexMap<String, String>,
pub enabled_tools: Option<String>, #[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
pub enabled_tools: Option<Vec<String>>,
pub visible_tools: Option<Vec<String>>, pub visible_tools: Option<Vec<String>>,
pub skills_enabled: bool, pub skills_enabled: bool,
pub enabled_skills: Option<String>, #[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
pub enabled_skills: Option<Vec<String>>,
pub visible_skills: Option<Vec<String>>, pub visible_skills: Option<Vec<String>>,
pub mcp_server_support: bool, pub mcp_server_support: bool,
pub mapping_mcp_servers: IndexMap<String, String>, pub mapping_mcp_servers: IndexMap<String, String>,
pub enabled_mcp_servers: Option<String>, #[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
pub enabled_mcp_servers: Option<Vec<String>>,
pub auto_continue: bool, pub auto_continue: bool,
pub max_auto_continues: usize, pub max_auto_continues: usize,
@@ -392,7 +395,7 @@ impl AppConfig {
self.mapping_tools = v; self.mapping_tools = v;
} }
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) { if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) {
self.enabled_tools = v; self.enabled_tools = v.map(|raw| super::csv_to_vec(&raw));
} }
if let Some(Some(v)) = super::read_env_bool(&get_env_name("skills_enabled")) { if let Some(Some(v)) = super::read_env_bool(&get_env_name("skills_enabled")) {
@@ -400,7 +403,7 @@ impl AppConfig {
} }
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) { if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) {
self.enabled_skills = v; self.enabled_skills = v.map(|raw| super::csv_to_vec(&raw));
} }
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) { if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
@@ -412,7 +415,7 @@ impl AppConfig {
self.mapping_mcp_servers = v; self.mapping_mcp_servers = v;
} }
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) { if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) {
self.enabled_mcp_servers = v; self.enabled_mcp_servers = v.map(|raw| super::csv_to_vec(&raw));
} }
if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) { if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) {
@@ -514,12 +517,12 @@ impl AppConfig {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn set_enabled_tools_default(&mut self, value: Option<String>) { pub fn set_enabled_tools_default(&mut self, value: Option<Vec<String>>) {
self.enabled_tools = value; self.enabled_tools = value;
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<String>) { pub fn set_enabled_mcp_servers_default(&mut self, value: Option<Vec<String>>) {
self.enabled_mcp_servers = value; self.enabled_mcp_servers = value;
} }
+2 -2
View File
@@ -33,8 +33,8 @@ pub async fn macro_execute(
let mut app_config = (*ctx.app.config).clone(); let mut app_config = (*ctx.app.config).clone();
app_config.temperature = role.temperature(); app_config.temperature = role.temperature();
app_config.top_p = role.top_p(); app_config.top_p = role.top_p();
app_config.enabled_tools = role.enabled_tools().clone(); app_config.enabled_tools = role.enabled_tools();
app_config.enabled_mcp_servers = role.enabled_mcp_servers().clone(); app_config.enabled_mcp_servers = role.enabled_mcp_servers();
let mut app_state = (*ctx.app).clone(); let mut app_state = (*ctx.app).clone();
app_state.config = Arc::new(app_config); app_state.config = Arc::new(app_config);
+72 -3
View File
@@ -196,16 +196,19 @@ pub struct Config {
pub function_calling_support: bool, pub function_calling_support: bool,
pub mapping_tools: IndexMap<String, String>, pub mapping_tools: IndexMap<String, String>,
pub enabled_tools: Option<String>, #[serde(default, deserialize_with = "deserialize_csv_or_vec")]
pub enabled_tools: Option<Vec<String>>,
pub visible_tools: Option<Vec<String>>, pub visible_tools: Option<Vec<String>>,
pub skills_enabled: bool, pub skills_enabled: bool,
pub enabled_skills: Option<String>, #[serde(default, deserialize_with = "deserialize_csv_or_vec")]
pub enabled_skills: Option<Vec<String>>,
pub visible_skills: Option<Vec<String>>, pub visible_skills: Option<Vec<String>>,
pub mcp_server_support: bool, pub mcp_server_support: bool,
pub mapping_mcp_servers: IndexMap<String, String>, pub mapping_mcp_servers: IndexMap<String, String>,
pub enabled_mcp_servers: Option<String>, #[serde(default, deserialize_with = "deserialize_csv_or_vec")]
pub enabled_mcp_servers: Option<Vec<String>>,
pub auto_continue: bool, pub auto_continue: bool,
pub max_auto_continues: usize, pub max_auto_continues: usize,
@@ -783,6 +786,72 @@ where
Ok(value) Ok(value)
} }
pub(super) fn csv_to_vec(raw: &str) -> Vec<String> {
raw.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
}
pub(super) fn deserialize_csv_or_vec<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, SeqAccess, Visitor};
use std::fmt;
struct CsvOrVec;
impl<'de> Visitor<'de> for CsvOrVec {
type Value = Option<Vec<String>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a comma-separated string, a list of strings, or null")
}
fn visit_str<E: de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
Ok(Some(csv_to_vec(value)))
}
fn visit_string<E: de::Error>(self, value: String) -> std::result::Result<Self::Value, E> {
Ok(Some(csv_to_vec(&value)))
}
fn visit_none<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D2: serde::Deserializer<'de>>(
self,
deserializer: D2,
) -> std::result::Result<Self::Value, D2::Error> {
deserializer.deserialize_any(self)
}
fn visit_unit<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
Ok(None)
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<Self::Value, A::Error> {
let mut vec = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
let trimmed = item.trim().to_string();
if !trimmed.is_empty() {
vec.push(trimmed);
}
}
Ok(Some(vec))
}
}
deserializer.deserialize_option(CsvOrVec)
}
fn read_env_bool(key: &str) -> Option<Option<bool>> { fn read_env_bool(key: &str) -> Option<Option<bool>> {
let value = env::var(key).ok()?; let value = env::var(key).ok()?;
Some(parse_bool(&value)) Some(parse_bool(&value))
+73 -44
View File
@@ -700,7 +700,7 @@ impl RequestContext {
} }
} }
pub fn set_enabled_tools_on_role_like(&mut self, value: Option<String>) -> bool { pub fn set_enabled_tools_on_role_like(&mut self, value: Option<Vec<String>>) -> bool {
match self.role_like_mut() { match self.role_like_mut() {
Some(role_like) => { Some(role_like) => {
role_like.set_enabled_tools(value); role_like.set_enabled_tools(value);
@@ -710,7 +710,7 @@ impl RequestContext {
} }
} }
pub fn set_enabled_mcp_servers_on_role_like(&mut self, value: Option<String>) -> bool { pub fn set_enabled_mcp_servers_on_role_like(&mut self, value: Option<Vec<String>>) -> bool {
match self.role_like_mut() { match self.role_like_mut() {
Some(role_like) => { Some(role_like) => {
role_like.set_enabled_mcp_servers(value); role_like.set_enabled_mcp_servers(value);
@@ -854,11 +854,11 @@ impl RequestContext {
("top_p", super::format_option_value(&role.top_p())), ("top_p", super::format_option_value(&role.top_p())),
( (
"enabled_tools", "enabled_tools",
super::format_option_value(&role.enabled_tools()), super::format_option_value(&role.enabled_tools().map(|v| v.join(","))),
), ),
( (
"enabled_mcp_servers", "enabled_mcp_servers",
super::format_option_value(&role.enabled_mcp_servers()), super::format_option_value(&role.enabled_mcp_servers().map(|v| v.join(","))),
), ),
( (
"max_output_tokens", "max_output_tokens",
@@ -1148,10 +1148,10 @@ impl RequestContext {
} }
let mut tool_names: HashSet<String> = Default::default(); let mut tool_names: HashSet<String> = Default::default();
if enabled_tools == "all" { if enabled_tools.iter().any(|s| s.trim() == "all") {
tool_names.extend(declaration_names); tool_names.extend(declaration_names);
} else { } else {
for item in enabled_tools.split(',') { for item in enabled_tools.iter() {
let item = item.trim(); let item = item.trim();
if item.is_empty() { if item.is_empty() {
continue; continue;
@@ -1279,10 +1279,10 @@ impl RequestContext {
} }
let mut server_names: HashSet<String> = Default::default(); let mut server_names: HashSet<String> = Default::default();
if enabled_mcp_servers == "all" { if enabled_mcp_servers.iter().any(|s| s.trim() == "all") {
server_names.extend(mcp_declaration_names); server_names.extend(mcp_declaration_names);
} else { } else {
for item in enabled_mcp_servers.split(',') { for item in enabled_mcp_servers.iter() {
let item = item.trim(); let item = item.trim();
if item.is_empty() { if item.is_empty() {
continue; continue;
@@ -1714,14 +1714,29 @@ impl RequestContext {
} }
} }
"enabled_tools" => { "enabled_tools" => {
let value = super::parse_value(value)?; let raw: Option<String> = super::parse_value(value)?;
if !self.set_enabled_tools_on_role_like(value.clone()) { let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
self.update_app_config(|app| app.enabled_tools = value); if !self.set_enabled_tools_on_role_like(parsed.clone()) {
self.update_app_config(|app| app.enabled_tools = parsed.clone());
}
}
"enabled_skills" => {
let raw: Option<String> = super::parse_value(value)?;
let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
self.update_app_config(|app| app.enabled_skills = parsed.clone());
}
"skills_enabled" => {
let value: Option<bool> = super::parse_value(value)?;
if let Some(session) = self.session.as_mut() {
session.set_skills_enabled(value);
} else {
self.update_app_config(|app| app.skills_enabled = value.unwrap_or(true));
} }
} }
"enabled_mcp_servers" => { "enabled_mcp_servers" => {
let value: Option<String> = super::parse_value(value)?; let raw: Option<String> = super::parse_value(value)?;
if let Some(servers) = value.as_ref() { let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
if let Some(servers) = parsed.as_ref() {
let Some(mcp_config) = &self.app.mcp_config else { let Some(mcp_config) = &self.app.mcp_config else {
bail!( bail!(
"No MCP servers are configured. Please configure MCP servers first before setting 'enabled_mcp_servers'." "No MCP servers are configured. Please configure MCP servers first before setting 'enabled_mcp_servers'."
@@ -1733,7 +1748,7 @@ impl RequestContext {
); );
} }
if !servers.split(',').all(|s| { if !servers.iter().all(|s| {
let server = s.trim(); let server = s.trim();
server == "all" || mcp_config.mcp_servers.contains_key(server) server == "all" || mcp_config.mcp_servers.contains_key(server)
}) { }) {
@@ -1742,8 +1757,8 @@ impl RequestContext {
); );
} }
} }
if !self.set_enabled_mcp_servers_on_role_like(value.clone()) { if !self.set_enabled_mcp_servers_on_role_like(parsed.clone()) {
self.update_app_config(|app| app.enabled_mcp_servers = value.clone()); self.update_app_config(|app| app.enabled_mcp_servers = parsed.clone());
} }
if self.app.config.mcp_server_support { if self.app.config.mcp_server_support {
let app = Arc::clone(&self.app.config); let app = Arc::clone(&self.app.config);
@@ -1965,6 +1980,7 @@ impl RequestContext {
"dry_run", "dry_run",
"function_calling_support", "function_calling_support",
"mcp_server_support", "mcp_server_support",
"skills_enabled",
"stream", "stream",
"save", "save",
"highlight", "highlight",
@@ -2063,6 +2079,14 @@ impl RequestContext {
.collect() .collect()
} }
"mcp_server_support" => super::complete_bool(app.mcp_server_support), "mcp_server_support" => super::complete_bool(app.mcp_server_support),
"skills_enabled" => {
let current = if let Some(session) = &self.session {
session.skills_enabled()
} else {
Some(app.skills_enabled)
};
super::complete_option_bool(current)
}
"enabled_mcp_servers" => { "enabled_mcp_servers" => {
let mut prefix = String::new(); let mut prefix = String::new();
let mut ignores = HashSet::new(); let mut ignores = HashSet::new();
@@ -2141,7 +2165,7 @@ impl RequestContext {
async fn rebuild_tool_scope( async fn rebuild_tool_scope(
&mut self, &mut self,
app: &AppConfig, app: &AppConfig,
enabled_mcp_servers: Option<String>, enabled_mcp_servers: Option<Vec<String>>,
abort_signal: AbortSignal, abort_signal: AbortSignal,
) -> Result<()> { ) -> Result<()> {
let policy = SkillPolicy::effective( let policy = SkillPolicy::effective(
@@ -2153,21 +2177,23 @@ impl RequestContext {
let enabled_mcp_servers = if policy.skills_enabled && app.mcp_server_support { let enabled_mcp_servers = if policy.skills_enabled && app.mcp_server_support {
let skill_mcps = self.skill_registry.loaded_mcp_servers(); let skill_mcps = self.skill_registry.loaded_mcp_servers();
match (enabled_mcp_servers.as_deref(), skill_mcps.is_empty()) { let has_all = enabled_mcp_servers
(Some("all"), _) | (_, true) => enabled_mcp_servers, .as_ref()
(base, false) => { .map(|v| v.iter().any(|s| s.trim() == "all"))
let mut merged: BTreeSet<String> = skill_mcps; .unwrap_or(false);
if let Some(s) = base { if has_all || skill_mcps.is_empty() {
for token in s.split(',') { enabled_mcp_servers
let t = token.trim(); } else {
if !t.is_empty() { let mut merged: BTreeSet<String> = skill_mcps;
merged.insert(t.to_string()); if let Some(servers) = &enabled_mcp_servers {
} for token in servers {
let t = token.trim();
if !t.is_empty() {
merged.insert(t.to_string());
} }
} }
Some(merged.into_iter().collect::<Vec<_>>().join(","))
} }
Some(merged.into_iter().collect())
} }
} else { } else {
enabled_mcp_servers enabled_mcp_servers
@@ -2179,12 +2205,12 @@ impl RequestContext {
&& let Some(mcp_config) = &self.app.mcp_config && let Some(mcp_config) = &self.app.mcp_config
{ {
let server_ids: Vec<String> = match &enabled_mcp_servers { let server_ids: Vec<String> = match &enabled_mcp_servers {
Some(servers) if servers == "all" => { Some(servers) if servers.iter().any(|s| s.trim() == "all") => {
mcp_config.mcp_servers.keys().cloned().collect() mcp_config.mcp_servers.keys().cloned().collect()
} }
Some(servers) => { Some(servers) => {
let mut ids = Vec::new(); let mut ids = Vec::new();
for item in servers.split(',').map(|s| s.trim()) { for item in servers.iter().map(|s| s.trim()) {
if mcp_config.mcp_servers.contains_key(item) { if mcp_config.mcp_servers.contains_key(item) {
ids.push(item.to_string()); ids.push(item.to_string());
} else if let Some(mapped) = app.mapping_mcp_servers.get(item) { } else if let Some(mapped) = app.mapping_mcp_servers.get(item) {
@@ -2263,7 +2289,7 @@ impl RequestContext {
if names.is_empty() { if names.is_empty() {
None None
} else { } else {
Some(names.join(",")) Some(names.to_vec())
} }
} else if let Some(role) = &self.role { } else if let Some(role) = &self.role {
role.enabled_mcp_servers() role.enabled_mcp_servers()
@@ -2423,7 +2449,7 @@ impl RequestContext {
} }
let mcp_servers = if app.mcp_server_support { let mcp_servers = if app.mcp_server_support {
(!agent.mcp_server_names().is_empty()).then(|| agent.mcp_server_names().join(",")) (!agent.mcp_server_names().is_empty()).then(|| agent.mcp_server_names().to_vec())
} else { } else {
if !agent.mcp_server_names().is_empty() { if !agent.mcp_server_names().is_empty() {
bail!( bail!(
@@ -2599,7 +2625,7 @@ impl RequestContext {
let skill = Skill::load(name)?; let skill = Skill::load(name)?;
let needs_mcps = skill let needs_mcps = skill
.enabled_mcp_servers() .enabled_mcp_servers()
.map(|s| !s.trim().is_empty()) .map(|v| !v.is_empty())
.unwrap_or(false); .unwrap_or(false);
if needs_mcps && !self.app.config.mcp_server_support { if needs_mcps && !self.app.config.mcp_server_support {
@@ -2706,13 +2732,13 @@ impl RequestContext {
&self, &self,
app: &AppConfig, app: &AppConfig,
start_mcp_servers: bool, start_mcp_servers: bool,
) -> Option<String> { ) -> Option<Vec<String>> {
if !start_mcp_servers || !app.mcp_server_support { if !start_mcp_servers || !app.mcp_server_support {
return None; return None;
} }
if let Some(agent) = self.agent.as_ref() { if let Some(agent) = self.agent.as_ref() {
return (!agent.mcp_server_names().is_empty()) return (!agent.mcp_server_names().is_empty())
.then(|| agent.mcp_server_names().join(",")); .then(|| agent.mcp_server_names().to_vec());
} }
if let Some(session) = self.session.as_ref() { if let Some(session) = self.session.as_ref() {
return session.enabled_mcp_servers(); return session.enabled_mcp_servers();
@@ -3205,7 +3231,7 @@ mod tests {
let app = ctx.app.config.clone(); let app = ctx.app.config.clone();
let abort = utils::create_abort_signal(); let abort = utils::create_abort_signal();
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap(); run_async(ctx.rebuild_tool_scope(&app, Some(vec!["all".to_string()]), abort)).unwrap();
assert!(ctx.tool_scope.mcp_runtime.is_empty()); assert!(ctx.tool_scope.mcp_runtime.is_empty());
} }
@@ -3233,7 +3259,7 @@ mod tests {
let app = ctx.app.config.clone(); let app = ctx.app.config.clone();
let abort = utils::create_abort_signal(); let abort = utils::create_abort_signal();
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap(); run_async(ctx.rebuild_tool_scope(&app, Some(vec!["all".to_string()]), abort)).unwrap();
assert!(ctx.tool_scope.mcp_runtime.is_empty()); assert!(ctx.tool_scope.mcp_runtime.is_empty());
} }
@@ -3341,7 +3367,7 @@ mod tests {
}; };
let ctx = RequestContext::new(app_state, WorkingMode::Cmd); let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("all".to_string())); role.set_enabled_tools(Some(vec!["all".to_string()]));
assert!(ctx.select_functions(&role).is_none()); assert!(ctx.select_functions(&role).is_none());
} }
@@ -3352,7 +3378,7 @@ mod tests {
ctx.tool_scope.functions.append_user_interaction_functions(); ctx.tool_scope.functions.append_user_interaction_functions();
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("all".to_string())); role.set_enabled_tools(Some(vec!["all".to_string()]));
let fns = ctx.select_functions(&role).unwrap(); let fns = ctx.select_functions(&role).unwrap();
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect(); let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3366,7 +3392,10 @@ mod tests {
ctx.tool_scope.functions.append_todo_functions(); ctx.tool_scope.functions.append_todo_functions();
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("todo__init, todo__add".to_string())); role.set_enabled_tools(Some(vec![
"todo__init".to_string(),
"todo__add".to_string(),
]));
let fns = ctx.select_functions(&role).unwrap(); let fns = ctx.select_functions(&role).unwrap();
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect(); let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3395,7 +3424,7 @@ mod tests {
}; };
let ctx = RequestContext::new(app_state, WorkingMode::Cmd); let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("all".to_string())); role.set_enabled_mcp_servers(Some(vec!["all".to_string()]));
let result = ctx.select_enabled_mcp_servers(&role); let result = ctx.select_enabled_mcp_servers(&role);
assert!(result.is_empty()); assert!(result.is_empty());
} }
@@ -3408,7 +3437,7 @@ mod tests {
.append_mcp_meta_functions(vec!["github".into(), "slack".into()]); .append_mcp_meta_functions(vec!["github".into(), "slack".into()]);
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("all".to_string())); role.set_enabled_mcp_servers(Some(vec!["all".to_string()]));
let fns = ctx.select_enabled_mcp_servers(&role); let fns = ctx.select_enabled_mcp_servers(&role);
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect(); let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3425,7 +3454,7 @@ mod tests {
.append_mcp_meta_functions(vec!["github".into(), "slack".into()]); .append_mcp_meta_functions(vec!["github".into(), "slack".into()]);
let mut role = Role::new("r", "p"); let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("github".to_string())); role.set_enabled_mcp_servers(Some(vec!["github".to_string()]));
let fns = ctx.select_enabled_mcp_servers(&role); let fns = ctx.select_enabled_mcp_servers(&role);
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect(); let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
+71 -27
View File
@@ -28,13 +28,13 @@ pub trait RoleLike {
fn model(&self) -> &Model; fn model(&self) -> &Model;
fn temperature(&self) -> Option<f64>; fn temperature(&self) -> Option<f64>;
fn top_p(&self) -> Option<f64>; fn top_p(&self) -> Option<f64>;
fn enabled_tools(&self) -> Option<String>; fn enabled_tools(&self) -> Option<Vec<String>>;
fn enabled_mcp_servers(&self) -> Option<String>; fn enabled_mcp_servers(&self) -> Option<Vec<String>>;
fn set_model(&mut self, model: Model); fn set_model(&mut self, model: Model);
fn set_temperature(&mut self, value: Option<f64>); fn set_temperature(&mut self, value: Option<f64>);
fn set_top_p(&mut self, value: Option<f64>); fn set_top_p(&mut self, value: Option<f64>);
fn set_enabled_tools(&mut self, value: Option<String>); fn set_enabled_tools(&mut self, value: Option<Vec<String>>);
fn set_enabled_mcp_servers(&mut self, value: Option<String>); fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>);
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -51,14 +51,26 @@ pub struct Role {
temperature: Option<f64>, temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>, top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
enabled_tools: Option<String>, default,
#[serde(skip_serializing_if = "Option::is_none")] skip_serializing_if = "Option::is_none",
enabled_mcp_servers: Option<String>, deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_tools: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
skills_enabled: Option<bool>, skills_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
enabled_skills: Option<String>, default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_skills: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
auto_continue: Option<bool>, auto_continue: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -98,12 +110,12 @@ impl Role {
"model" => role.model_id = value.as_str().map(|v| v.to_string()), "model" => role.model_id = value.as_str().map(|v| v.to_string()),
"temperature" => role.temperature = value.as_f64(), "temperature" => role.temperature = value.as_f64(),
"top_p" => role.top_p = value.as_f64(), "top_p" => role.top_p = value.as_f64(),
"enabled_tools" => role.enabled_tools = value.as_str().map(|v| v.to_string()), "enabled_tools" => role.enabled_tools = parse_string_or_array(value),
"enabled_mcp_servers" => { "enabled_mcp_servers" => {
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string()) role.enabled_mcp_servers = parse_string_or_array(value)
} }
"skills_enabled" => role.skills_enabled = value.as_bool(), "skills_enabled" => role.skills_enabled = value.as_bool(),
"enabled_skills" => role.enabled_skills = value.as_str().map(|v| v.to_string()), "enabled_skills" => role.enabled_skills = parse_string_or_array(value),
"auto_continue" => role.auto_continue = value.as_bool(), "auto_continue" => role.auto_continue = value.as_bool(),
"max_auto_continues" => { "max_auto_continues" => {
role.max_auto_continues = value.as_u64().map(|v| v as usize) role.max_auto_continues = value.as_u64().map(|v| v as usize)
@@ -147,17 +159,21 @@ impl Role {
if let Some(top_p) = self.top_p() { if let Some(top_p) = self.top_p() {
metadata.push(format!("top_p: {top_p}")); metadata.push(format!("top_p: {top_p}"));
} }
if let Some(enabled_tools) = self.enabled_tools() { if let Some(enabled_tools) = &self.enabled_tools {
metadata.push(format!("enabled_tools: {enabled_tools}")); let inline = serde_json::to_string(enabled_tools).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_tools: {inline}"));
} }
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() { if let Some(enabled_mcp_servers) = &self.enabled_mcp_servers {
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}")); let inline =
serde_json::to_string(enabled_mcp_servers).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_mcp_servers: {inline}"));
} }
if let Some(skills_enabled) = self.skills_enabled { if let Some(skills_enabled) = self.skills_enabled {
metadata.push(format!("skills_enabled: {skills_enabled}")); metadata.push(format!("skills_enabled: {skills_enabled}"));
} }
if let Some(enabled_skills) = &self.enabled_skills { if let Some(enabled_skills) = &self.enabled_skills {
metadata.push(format!("enabled_skills: {enabled_skills}")); let inline = serde_json::to_string(enabled_skills).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_skills: {inline}"));
} }
if let Some(auto_continue) = self.auto_continue { if let Some(auto_continue) = self.auto_continue {
metadata.push(format!("auto_continue: {auto_continue}")); metadata.push(format!("auto_continue: {auto_continue}"));
@@ -225,8 +241,8 @@ impl Role {
model: &Model, model: &Model,
temperature: Option<f64>, temperature: Option<f64>,
top_p: Option<f64>, top_p: Option<f64>,
enabled_tools: Option<String>, enabled_tools: Option<Vec<String>>,
enabled_mcp_servers: Option<String>, enabled_mcp_servers: Option<Vec<String>>,
) { ) {
self.set_model(model.clone()); self.set_model(model.clone());
if temperature.is_some() { if temperature.is_some() {
@@ -287,7 +303,7 @@ impl Role {
self.skills_enabled self.skills_enabled
} }
pub fn enabled_skills(&self) -> Option<&str> { pub fn enabled_skills(&self) -> Option<&[String]> {
self.enabled_skills.as_deref() self.enabled_skills.as_deref()
} }
@@ -360,11 +376,11 @@ impl RoleLike for Role {
self.top_p self.top_p
} }
fn enabled_tools(&self) -> Option<String> { fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone() self.enabled_tools.clone()
} }
fn enabled_mcp_servers(&self) -> Option<String> { fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
self.enabled_mcp_servers.clone() self.enabled_mcp_servers.clone()
} }
@@ -383,15 +399,37 @@ impl RoleLike for Role {
self.top_p = value; self.top_p = value;
} }
fn set_enabled_tools(&mut self, value: Option<String>) { fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
self.enabled_tools = value; self.enabled_tools = value;
} }
fn set_enabled_mcp_servers(&mut self, value: Option<String>) { fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
self.enabled_mcp_servers = value; self.enabled_mcp_servers = value;
} }
} }
fn parse_string_or_array(value: &Value) -> Option<Vec<String>> {
if value.is_null() {
return None;
}
if let Some(s) = value.as_str() {
return Some(csv_to_vec(s));
}
if let Some(arr) = value.as_array() {
let items: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.trim().to_string()))
.filter(|s| !s.is_empty())
.collect();
return Some(items);
}
None
}
fn parse_structure_prompt(prompt: &str) -> (&str, Vec<(&str, &str)>) { fn parse_structure_prompt(prompt: &str) -> (&str, Vec<(&str, &str)>) {
let mut text = prompt; let mut text = prompt;
let mut search_input = true; let mut search_input = true;
@@ -466,14 +504,20 @@ mod tests {
fn role_new_parses_enabled_tools() { fn role_new_parses_enabled_tools() {
let content = "---\nenabled_tools: tool1,tool2\n---\nPrompt"; let content = "---\nenabled_tools: tool1,tool2\n---\nPrompt";
let role = Role::new("test", content); let role = Role::new("test", content);
assert_eq!(role.enabled_tools(), Some("tool1,tool2".to_string())); assert_eq!(
role.enabled_tools(),
Some(vec!["tool1".to_string(), "tool2".to_string()])
);
} }
#[test] #[test]
fn role_new_parses_enabled_mcp_servers() { fn role_new_parses_enabled_mcp_servers() {
let content = "---\nenabled_mcp_servers: github,jira\n---\nPrompt"; let content = "---\nenabled_mcp_servers: github,jira\n---\nPrompt";
let role = Role::new("test", content); let role = Role::new("test", content);
assert_eq!(role.enabled_mcp_servers(), Some("github,jira".to_string())); assert_eq!(
role.enabled_mcp_servers(),
Some(vec!["github".to_string(), "jira".to_string()])
);
} }
#[test] #[test]
+48 -15
View File
@@ -24,14 +24,26 @@ pub struct Session {
temperature: Option<f64>, temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>, top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
enabled_tools: Option<String>, default,
#[serde(skip_serializing_if = "Option::is_none")] skip_serializing_if = "Option::is_none",
enabled_mcp_servers: Option<String>, deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_tools: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
skills_enabled: Option<bool>, skills_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
enabled_skills: Option<String>, default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_skills: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
save_session: Option<bool>, save_session: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -83,10 +95,17 @@ impl Session {
self.skills_enabled self.skills_enabled
} }
pub fn enabled_skills(&self) -> Option<&str> { pub fn enabled_skills(&self) -> Option<&[String]> {
self.enabled_skills.as_deref() self.enabled_skills.as_deref()
} }
pub fn set_skills_enabled(&mut self, value: Option<bool>) {
if self.skills_enabled != value {
self.skills_enabled = value;
self.dirty = true;
}
}
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self { pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
let role = ctx.extract_role(app); let role = ctx.extract_role(app);
let mut session = Self { let mut session = Self {
@@ -182,10 +201,16 @@ impl Session {
data["top_p"] = top_p.into(); data["top_p"] = top_p.into();
} }
if let Some(enabled_tools) = self.enabled_tools() { if let Some(enabled_tools) = self.enabled_tools() {
data["enabled_tools"] = enabled_tools.into(); data["enabled_tools"] = json!(enabled_tools);
} }
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() { if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
data["enabled_mcp_servers"] = enabled_mcp_servers.into(); data["enabled_mcp_servers"] = json!(enabled_mcp_servers);
}
if let Some(skills_enabled) = self.skills_enabled() {
data["skills_enabled"] = skills_enabled.into();
}
if let Some(enabled_skills) = self.enabled_skills() {
data["enabled_skills"] = json!(enabled_skills);
} }
if let Some(save_session) = self.save_session() { if let Some(save_session) = self.save_session() {
data["save_session"] = save_session.into(); data["save_session"] = save_session.into();
@@ -242,11 +267,19 @@ impl Session {
} }
if let Some(enabled_tools) = self.enabled_tools() { if let Some(enabled_tools) = self.enabled_tools() {
items.push(("enabled_tools", enabled_tools)); items.push(("enabled_tools", enabled_tools.join(",")));
} }
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() { if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
items.push(("enabled_mcp_servers", enabled_mcp_servers)); items.push(("enabled_mcp_servers", enabled_mcp_servers.join(",")));
}
if let Some(skills_enabled) = self.skills_enabled() {
items.push(("skills_enabled", skills_enabled.to_string()));
}
if let Some(enabled_skills) = self.enabled_skills() {
items.push(("enabled_skills", enabled_skills.join(",")));
} }
if let Some(save_session) = self.save_session() { if let Some(save_session) = self.save_session() {
@@ -682,11 +715,11 @@ impl RoleLike for Session {
self.top_p self.top_p
} }
fn enabled_tools(&self) -> Option<String> { fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone() self.enabled_tools.clone()
} }
fn enabled_mcp_servers(&self) -> Option<String> { fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
self.enabled_mcp_servers.clone() self.enabled_mcp_servers.clone()
} }
@@ -713,14 +746,14 @@ impl RoleLike for Session {
} }
} }
fn set_enabled_tools(&mut self, value: Option<String>) { fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
if self.enabled_tools != value { if self.enabled_tools != value {
self.enabled_tools = value; self.enabled_tools = value;
self.dirty = true; self.dirty = true;
} }
} }
fn set_enabled_mcp_servers(&mut self, value: Option<String>) { fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
if self.enabled_mcp_servers != value { if self.enabled_mcp_servers != value {
self.enabled_mcp_servers = value; self.enabled_mcp_servers = value;
self.dirty = true; self.dirty = true;
+33 -9
View File
@@ -33,9 +33,9 @@ pub struct Skill {
#[serde(default)] #[serde(default)]
body: String, body: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<String>, enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
enabled_mcp_servers: Option<String>, enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
auto_unload: Option<bool>, auto_unload: Option<bool>,
} }
@@ -69,10 +69,10 @@ impl Skill {
} }
} }
"enabled_tools" => { "enabled_tools" => {
skill.enabled_tools = value.as_str().map(|v| v.to_string()); skill.enabled_tools = parse_skill_string_or_array(value);
} }
"enabled_mcp_servers" => { "enabled_mcp_servers" => {
skill.enabled_mcp_servers = value.as_str().map(|v| v.to_string()); skill.enabled_mcp_servers = parse_skill_string_or_array(value);
} }
"auto_unload" => { "auto_unload" => {
skill.auto_unload = value.as_bool(); skill.auto_unload = value.as_bool();
@@ -134,11 +134,11 @@ impl Skill {
&self.body &self.body
} }
pub fn enabled_tools(&self) -> Option<&str> { pub fn enabled_tools(&self) -> Option<&[String]> {
self.enabled_tools.as_deref() self.enabled_tools.as_deref()
} }
pub fn enabled_mcp_servers(&self) -> Option<&str> { pub fn enabled_mcp_servers(&self) -> Option<&[String]> {
self.enabled_mcp_servers.as_deref() self.enabled_mcp_servers.as_deref()
} }
@@ -157,11 +157,29 @@ impl Skill {
fn declares_mcp_servers(&self) -> bool { fn declares_mcp_servers(&self) -> bool {
self.enabled_mcp_servers self.enabled_mcp_servers
.as_deref() .as_deref()
.map(|s| !s.trim().is_empty()) .map(|servers| !servers.is_empty())
.unwrap_or(false) .unwrap_or(false)
} }
} }
fn parse_skill_string_or_array(value: &Value) -> Option<Vec<String>> {
if value.is_null() {
return None;
}
if let Some(s) = value.as_str() {
return Some(csv_to_vec(s));
}
if let Some(arr) = value.as_array() {
let items: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.trim().to_string()))
.filter(|s| !s.is_empty())
.collect();
return Some(items);
}
None
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -189,8 +207,14 @@ mod tests {
assert_eq!(skill.name(), "git-master"); assert_eq!(skill.name(), "git-master");
assert_eq!(skill.description(), "Atomic commits, rebase surgery"); assert_eq!(skill.description(), "Atomic commits, rebase surgery");
assert_eq!(skill.enabled_tools(), Some("shell,fs")); assert_eq!(
assert_eq!(skill.enabled_mcp_servers(), Some("github")); skill.enabled_tools(),
Some(["shell".to_string(), "fs".to_string()].as_slice())
);
assert_eq!(
skill.enabled_mcp_servers(),
Some(["github".to_string()].as_slice())
);
assert!(skill.auto_unload()); assert!(skill.auto_unload());
assert_eq!(skill.body(), "You are a git expert"); assert_eq!(skill.body(), "You are a git expert");
} }
+5 -13
View File
@@ -67,10 +67,10 @@ impl SkillPolicy {
.map(|v| v.iter().cloned().collect()); .map(|v| v.iter().cloned().collect());
let enabled_raw: Option<Vec<String>> = session let enabled_raw: Option<Vec<String>> = session
.and_then(|s| parse_csv_opt(s.enabled_skills())) .and_then(|s| s.enabled_skills().map(|v| v.to_vec()))
.or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec()))) .or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec())))
.or_else(|| role.and_then(|r| parse_csv_opt(r.enabled_skills()))) .or_else(|| role.and_then(|r| r.enabled_skills().map(|v| v.to_vec())))
.or_else(|| parse_csv_opt(global.enabled_skills.as_deref())); .or_else(|| global.enabled_skills.clone());
let enabled: HashSet<String> = match enabled_raw { let enabled: HashSet<String> = match enabled_raw {
Some(explicit) => { Some(explicit) => {
@@ -107,17 +107,9 @@ impl SkillPolicy {
} }
} }
fn parse_csv_opt(s: Option<&str>) -> Option<Vec<String>> {
s.map(|raw| {
raw.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::csv_to_vec;
use super::*; use super::*;
fn always_true(_: &str) -> bool { fn always_true(_: &str) -> bool {
@@ -135,7 +127,7 @@ mod tests {
) -> AppConfig { ) -> AppConfig {
AppConfig { AppConfig {
skills_enabled, skills_enabled,
enabled_skills: enabled.map(|s| s.to_string()), enabled_skills: enabled.map(csv_to_vec),
visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()), visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()),
..AppConfig::default() ..AppConfig::default()
} }
+26 -33
View File
@@ -38,8 +38,8 @@ impl SkillRegistry {
pub fn loaded_mcp_servers(&self) -> BTreeSet<String> { pub fn loaded_mcp_servers(&self) -> BTreeSet<String> {
let mut out = BTreeSet::new(); let mut out = BTreeSet::new();
for skill in self.loaded.values() { for skill in self.loaded.values() {
if let Some(csv) = skill.enabled_mcp_servers() { if let Some(servers) = skill.enabled_mcp_servers() {
for token in csv.split(',') { for token in servers {
let t = token.trim(); let t = token.trim();
if !t.is_empty() { if !t.is_empty() {
out.insert(t.to_string()); out.insert(t.to_string());
@@ -69,12 +69,22 @@ impl SkillRegistry {
let base_tools_set = effective.enabled_tools().is_some(); let base_tools_set = effective.enabled_tools().is_some();
let base_mcps_set = effective.enabled_mcp_servers().is_some(); let base_mcps_set = effective.enabled_mcp_servers().is_some();
let mut tools = parse_csv(effective.enabled_tools().as_deref()); let mut tools: BTreeSet<String> = effective
let mut mcps = parse_csv(effective.enabled_mcp_servers().as_deref()); .enabled_tools()
.map(|v| v.into_iter().collect())
.unwrap_or_default();
let mut mcps: BTreeSet<String> = effective
.enabled_mcp_servers()
.map(|v| v.into_iter().collect())
.unwrap_or_default();
for (_, skill) in &self.loaded { for (_, skill) in &self.loaded {
tools.extend(parse_csv(skill.enabled_tools())); if let Some(skill_tools) = skill.enabled_tools() {
mcps.extend(parse_csv(skill.enabled_mcp_servers())); tools.extend(skill_tools.iter().cloned());
}
if let Some(servers) = skill.enabled_mcp_servers() {
mcps.extend(servers.iter().cloned());
}
if !skip_body && !skill.body().is_empty() { if !skip_body && !skill.body().is_empty() {
let separator = if effective.is_empty_prompt() { let separator = if effective.is_empty_prompt() {
"" ""
@@ -87,34 +97,17 @@ impl SkillRegistry {
} }
if base_tools_set || !tools.is_empty() { if base_tools_set || !tools.is_empty() {
effective.set_enabled_tools(Some(join_csv(&tools))); effective.set_enabled_tools(Some(tools.into_iter().collect()));
} }
if base_mcps_set || !mcps.is_empty() { if base_mcps_set || !mcps.is_empty() {
effective.set_enabled_mcp_servers(Some(join_csv(&mcps))); effective.set_enabled_mcp_servers(Some(mcps.into_iter().collect()));
} }
effective effective
} }
} }
fn parse_csv(s: Option<&str>) -> BTreeSet<String> {
let mut set = BTreeSet::new();
if let Some(raw) = s {
for token in raw.split(',') {
let trimmed = token.trim();
if !trimmed.is_empty() {
set.insert(trimmed.to_string());
}
}
}
set
}
fn join_csv(set: &BTreeSet<String>) -> String {
set.iter().cloned().collect::<Vec<_>>().join(",")
}
#[cfg(test)] #[cfg(test)]
impl SkillRegistry { impl SkillRegistry {
fn insert_for_test(&mut self, skill: Skill) { fn insert_for_test(&mut self, skill: Skill) {
@@ -194,7 +187,7 @@ mod tests {
assert_eq!(effective.prompt(), "Process: __INPUT__"); assert_eq!(effective.prompt(), "Process: __INPUT__");
let tools = effective.enabled_tools().expect("tools set by skill"); let tools = effective.enabled_tools().expect("tools set by skill");
assert!(tools.contains("shell")); assert!(tools.iter().any(|s| s == "shell"));
} }
#[test] #[test]
@@ -223,16 +216,16 @@ mod tests {
)); ));
let mut base = Role::new("test", "body"); let mut base = Role::new("test", "body");
base.set_enabled_tools(Some("web_search".to_string())); base.set_enabled_tools(Some(vec!["web_search".to_string()]));
let effective = registry.effective_role(&base); let effective = registry.effective_role(&base);
let tools_str = effective.enabled_tools().unwrap(); let tools_vec = effective.enabled_tools().unwrap();
let tools: BTreeSet<&str> = tools_str.split(',').collect(); let tools: BTreeSet<&str> = tools_vec.iter().map(|s| s.as_str()).collect();
assert_eq!(tools, BTreeSet::from(["fs", "git", "shell", "web_search"])); assert_eq!(tools, BTreeSet::from(["fs", "git", "shell", "web_search"]));
let mcps_str = effective.enabled_mcp_servers().unwrap(); let mcps_vec = effective.enabled_mcp_servers().unwrap();
let mcps: BTreeSet<&str> = mcps_str.split(',').collect(); let mcps: BTreeSet<&str> = mcps_vec.iter().map(|s| s.as_str()).collect();
assert_eq!(mcps, BTreeSet::from(["github", "jira"])); assert_eq!(mcps, BTreeSet::from(["github", "jira"]));
} }
@@ -254,10 +247,10 @@ mod tests {
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge")); registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
let mut base = Role::new("test", "Base"); let mut base = Role::new("test", "Base");
base.set_enabled_tools(Some(String::new())); base.set_enabled_tools(Some(Vec::new()));
let effective = registry.effective_role(&base); let effective = registry.effective_role(&base);
assert_eq!(effective.enabled_tools().as_deref(), Some("")); assert_eq!(effective.enabled_tools().as_deref(), Some([].as_slice()));
} }
#[test] #[test]
+4 -28
View File
@@ -127,8 +127,8 @@ fn handle_list(ctx: &RequestContext, policy: &SkillPolicy) -> Result<Value> {
entries.push(json!({ entries.push(json!({
"name": skill.name(), "name": skill.name(),
"description": skill.description(), "description": skill.description(),
"grants_tools": csv_to_vec(skill.enabled_tools()), "grants_tools": skill.enabled_tools().unwrap_or_default(),
"grants_mcp_servers": csv_to_vec(skill.enabled_mcp_servers()), "grants_mcp_servers": skill.enabled_mcp_servers().unwrap_or_default(),
"loaded": ctx.skill_registry.is_loaded(skill.name()), "loaded": ctx.skill_registry.is_loaded(skill.name()),
})); }));
} }
@@ -166,11 +166,11 @@ async fn handle_load(
let tools_declared = skill let tools_declared = skill
.enabled_tools() .enabled_tools()
.map(|s| !s.trim().is_empty()) .map(|v| !v.is_empty())
.unwrap_or(false); .unwrap_or(false);
let mcps_declared = skill let mcps_declared = skill
.enabled_mcp_servers() .enabled_mcp_servers()
.map(|s| !s.trim().is_empty()) .map(|v| !v.is_empty())
.unwrap_or(false); .unwrap_or(false);
if tools_declared && !function_calling_on { if tools_declared && !function_calling_on {
@@ -226,16 +226,6 @@ async fn handle_unload(ctx: &mut RequestContext, args: &Value) -> Result<Value>
})) }))
} }
fn csv_to_vec(csv: Option<&str>) -> Vec<String> {
csv.map(|raw| {
raw.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
})
.unwrap_or_default()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -293,18 +283,4 @@ mod tests {
assert!(required, "skill__list should have no required parameters"); assert!(required, "skill__list should have no required parameters");
} }
#[test]
fn csv_to_vec_empty_input() {
assert!(csv_to_vec(None).is_empty());
assert!(csv_to_vec(Some("")).is_empty());
assert!(csv_to_vec(Some(" ")).is_empty());
}
#[test]
fn csv_to_vec_parses_and_trims() {
let v = csv_to_vec(Some("a, b ,c,, d"));
assert_eq!(v, vec!["a", "b", "c", "d"]);
}
} }
+6 -6
View File
@@ -256,18 +256,18 @@ fn build_inline_role(
} }
if node.tools.as_deref().unwrap_or_default().is_empty() { if node.tools.as_deref().unwrap_or_default().is_empty() {
role.set_enabled_tools(Some(String::new())); role.set_enabled_tools(Some(Vec::new()));
role.set_enabled_mcp_servers(Some(String::new())); role.set_enabled_mcp_servers(Some(Vec::new()));
} else { } else {
if !regular_tools.is_empty() { if !regular_tools.is_empty() {
role.set_enabled_tools(Some(regular_tools.join(","))); role.set_enabled_tools(Some(regular_tools.to_vec()));
} else { } else {
role.set_enabled_tools(Some(String::new())); role.set_enabled_tools(Some(Vec::new()));
} }
if !mcp_servers.is_empty() { if !mcp_servers.is_empty() {
role.set_enabled_mcp_servers(Some(mcp_servers.join(","))); role.set_enabled_mcp_servers(Some(mcp_servers.to_vec()));
} else { } else {
role.set_enabled_mcp_servers(Some(String::new())); role.set_enabled_mcp_servers(Some(Vec::new()));
} }
} }
+4 -4
View File
@@ -55,8 +55,8 @@ async fn extract_via_extractor(
fn build_extractor_role() -> Result<Role> { fn build_extractor_role() -> Result<Role> {
let mut role = Role::new(EXTRACTOR_ROLE_NAME, EXTRACTOR_ROLE_PROMPT); let mut role = Role::new(EXTRACTOR_ROLE_NAME, EXTRACTOR_ROLE_PROMPT);
role.set_enabled_tools(Some(String::new())); role.set_enabled_tools(Some(Vec::new()));
role.set_enabled_mcp_servers(Some(String::new())); role.set_enabled_mcp_servers(Some(Vec::new()));
Ok(role) Ok(role)
} }
@@ -183,7 +183,7 @@ mod tests {
fn build_extractor_role_disables_tools_and_mcp() { fn build_extractor_role_disables_tools_and_mcp() {
let role = build_extractor_role().expect("builtin role must exist"); let role = build_extractor_role().expect("builtin role must exist");
assert_eq!(role.enabled_tools().as_deref(), Some("")); assert_eq!(role.enabled_tools().as_deref(), Some([].as_slice()));
assert_eq!(role.enabled_mcp_servers().as_deref(), Some("")); assert_eq!(role.enabled_mcp_servers().as_deref(), Some([].as_slice()));
} }
} }
+17 -12
View File
@@ -146,7 +146,7 @@ impl McpRegistry {
pub async fn init( pub async fn init(
log_path: Option<PathBuf>, log_path: Option<PathBuf>,
start_mcp_servers: bool, start_mcp_servers: bool,
enabled_mcp_servers: Option<String>, enabled_mcp_servers: Option<Vec<String>>,
abort_signal: AbortSignal, abort_signal: AbortSignal,
app_config: &AppConfig, app_config: &AppConfig,
vault: &Vault, vault: &Vault,
@@ -216,7 +216,7 @@ impl McpRegistry {
async fn start_select_mcp_servers( async fn start_select_mcp_servers(
&mut self, &mut self,
enabled_mcp_servers: Option<String>, enabled_mcp_servers: Option<Vec<String>>,
) -> Result<()> { ) -> Result<()> {
if self.config.is_none() { if self.config.is_none() {
debug!( debug!(
@@ -292,15 +292,15 @@ impl McpRegistry {
Ok((id.to_string(), service, catalog)) Ok((id.to_string(), service, catalog))
} }
fn resolve_server_ids(&self, enabled_mcp_servers: Option<String>) -> Vec<String> { fn resolve_server_ids(&self, enabled_mcp_servers: Option<Vec<String>>) -> Vec<String> {
if let Some(config) = &self.config if let Some(config) = &self.config
&& let Some(servers) = enabled_mcp_servers && let Some(servers) = enabled_mcp_servers
{ {
if servers == "all" { if servers.iter().any(|s| s.trim() == "all") {
config.mcp_servers.keys().cloned().collect() config.mcp_servers.keys().cloned().collect()
} else { } else {
let enabled_servers: HashSet<String> = let enabled_servers: HashSet<String> =
servers.split(',').map(|s| s.trim().to_string()).collect(); servers.into_iter().map(|s| s.trim().to_string()).collect();
config config
.mcp_servers .mcp_servers
.keys() .keys()
@@ -754,7 +754,7 @@ mod tests {
#[test] #[test]
fn resolve_all_returns_all_configured_servers() { fn resolve_all_returns_all_configured_servers() {
let registry = make_registry_with_config(&["github", "slack", "jira"]); let registry = make_registry_with_config(&["github", "slack", "jira"]);
let mut ids = registry.resolve_server_ids(Some("all".to_string())); let mut ids = registry.resolve_server_ids(Some(vec!["all".to_string()]));
ids.sort(); ids.sort();
assert_eq!(ids, vec!["github", "jira", "slack"]); assert_eq!(ids, vec!["github", "jira", "slack"]);
} }
@@ -762,7 +762,8 @@ mod tests {
#[test] #[test]
fn resolve_comma_separated_returns_matching_servers() { fn resolve_comma_separated_returns_matching_servers() {
let registry = make_registry_with_config(&["github", "slack", "jira"]); let registry = make_registry_with_config(&["github", "slack", "jira"]);
let mut ids = registry.resolve_server_ids(Some("github, jira".to_string())); let mut ids =
registry.resolve_server_ids(Some(vec!["github".to_string(), "jira".to_string()]));
ids.sort(); ids.sort();
assert_eq!(ids, vec!["github", "jira"]); assert_eq!(ids, vec!["github", "jira"]);
} }
@@ -770,7 +771,7 @@ mod tests {
#[test] #[test]
fn resolve_single_server_name() { fn resolve_single_server_name() {
let registry = make_registry_with_config(&["github", "slack"]); let registry = make_registry_with_config(&["github", "slack"]);
let ids = registry.resolve_server_ids(Some("slack".to_string())); let ids = registry.resolve_server_ids(Some(vec!["slack".to_string()]));
assert_eq!(ids, vec!["slack"]); assert_eq!(ids, vec!["slack"]);
} }
@@ -784,28 +785,32 @@ mod tests {
#[test] #[test]
fn resolve_no_config_returns_empty() { fn resolve_no_config_returns_empty() {
let registry = McpRegistry::default(); let registry = McpRegistry::default();
let ids = registry.resolve_server_ids(Some("all".to_string())); let ids = registry.resolve_server_ids(Some(vec!["all".to_string()]));
assert!(ids.is_empty()); assert!(ids.is_empty());
} }
#[test] #[test]
fn resolve_nonexistent_server_filtered_out() { fn resolve_nonexistent_server_filtered_out() {
let registry = make_registry_with_config(&["github"]); let registry = make_registry_with_config(&["github"]);
let ids = registry.resolve_server_ids(Some("github, nonexistent".to_string())); let ids = registry
.resolve_server_ids(Some(vec!["github".to_string(), "nonexistent".to_string()]));
assert_eq!(ids, vec!["github"]); assert_eq!(ids, vec!["github"]);
} }
#[test] #[test]
fn resolve_all_nonexistent_returns_empty() { fn resolve_all_nonexistent_returns_empty() {
let registry = make_registry_with_config(&["github"]); let registry = make_registry_with_config(&["github"]);
let ids = registry.resolve_server_ids(Some("foo, bar".to_string())); let ids = registry.resolve_server_ids(Some(vec!["foo".to_string(), "bar".to_string()]));
assert!(ids.is_empty()); assert!(ids.is_empty());
} }
#[test] #[test]
fn resolve_trims_whitespace() { fn resolve_trims_whitespace() {
let registry = make_registry_with_config(&["github", "slack"]); let registry = make_registry_with_config(&["github", "slack"]);
let mut ids = registry.resolve_server_ids(Some(" github , slack ".to_string())); let mut ids = registry.resolve_server_ids(Some(vec![
" github ".to_string(),
" slack ".to_string(),
]));
ids.sort(); ids.sort();
assert_eq!(ids, vec!["github", "slack"]); assert_eq!(ids, vec!["github", "slack"]);
} }