feat: prefer ~/.config/coyote/mcp.json for the user-scope MCP config
The user-scope MCP config historically lived at <config-dir>/functions/mcp.json, a leftover from when MCP support was part of the llm-functions tooling. It now resolves through a single choke point with these semantics: - Preferred location: <config-dir>/mcp.json (created there on first run) - Historical <config-dir>/functions/mcp.json still honored when the preferred file does not exist, so existing installs are unchanged - If both exist, the preferred location wins --info/.info now reports the resolved location as mcp_config_file, and the --scope help text plus config.agent.example.yaml reference the new default. This also removes the asymmetry with the workspace scope, which already used .coyote/mcp.json directly.
This commit is contained in:
@@ -48,7 +48,7 @@ summarization_model: null # Model to use for summarizing sub-agent output
|
|||||||
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
||||||
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
||||||
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
||||||
- github # Corresponds to the name of an MCP server in the `<coyote-config-dir>/functions/mcp.json` file
|
- github # Corresponds to the name of an MCP server in the `<coyote-config-dir>/mcp.json` file
|
||||||
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
||||||
- web_search
|
- web_search
|
||||||
- fs
|
- fs
|
||||||
|
|||||||
+1
-1
@@ -315,7 +315,7 @@ pub struct Cli {
|
|||||||
/// URL for http/sse MCP server (used with --mcp-add)
|
/// URL for http/sse MCP server (used with --mcp-add)
|
||||||
#[arg(long, value_name = "URL", help_heading = "MCP Servers")]
|
#[arg(long, value_name = "URL", help_heading = "MCP Servers")]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
/// Scope for MCP config: user (~/.config/coyote/functions/mcp.json) or workspace (./.coyote/mcp.json). Default: user
|
/// Scope for MCP config: user (~/.config/coyote/mcp.json) or workspace (./.coyote/mcp.json). Default: user
|
||||||
#[arg(long, value_enum, value_name = "SCOPE", help_heading = "MCP Servers")]
|
#[arg(long, value_enum, value_name = "SCOPE", help_heading = "MCP Servers")]
|
||||||
pub scope: Option<McpScopeArg>,
|
pub scope: Option<McpScopeArg>,
|
||||||
/// Environment variable for stdio MCP server (repeatable): --env KEY=VALUE
|
/// Environment variable for stdio MCP server (repeatable): --env KEY=VALUE
|
||||||
|
|||||||
@@ -287,6 +287,18 @@ pub fn functions_bin_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mcp_config_file() -> PathBuf {
|
pub fn mcp_config_file() -> PathBuf {
|
||||||
|
let preferred = local_dir(MCP_FILE_NAME);
|
||||||
|
if preferred.exists() {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
let legacy = legacy_mcp_config_file();
|
||||||
|
if legacy.exists() {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
preferred
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legacy_mcp_config_file() -> PathBuf {
|
||||||
functions_dir().join(MCP_FILE_NAME)
|
functions_dir().join(MCP_FILE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,6 +849,80 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod user_mcp_resolution {
|
||||||
|
use super::*;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
fn with_config_dir<F: FnOnce(&Path)>(f: F) {
|
||||||
|
let unique = time::SystemTime::now()
|
||||||
|
.duration_since(time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let root = env::temp_dir().join(format!("coyote-user-mcp-test-{unique}"));
|
||||||
|
fs::create_dir_all(root.join(FUNCTIONS_DIR_NAME)).unwrap();
|
||||||
|
let config_env = get_env_name("config_dir");
|
||||||
|
let functions_env = get_env_name("functions_dir");
|
||||||
|
let prev_config = env::var_os(&config_env);
|
||||||
|
let prev_functions = env::var_os(&functions_env);
|
||||||
|
unsafe {
|
||||||
|
env::set_var(&config_env, &root);
|
||||||
|
env::set_var(&functions_env, root.join(FUNCTIONS_DIR_NAME));
|
||||||
|
}
|
||||||
|
f(&root);
|
||||||
|
unsafe {
|
||||||
|
match prev_config {
|
||||||
|
Some(v) => env::set_var(&config_env, v),
|
||||||
|
None => env::remove_var(&config_env),
|
||||||
|
}
|
||||||
|
match prev_functions {
|
||||||
|
Some(v) => env::set_var(&functions_env, v),
|
||||||
|
None => env::remove_var(&functions_env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn defaults_to_preferred_location_when_neither_exists() {
|
||||||
|
with_config_dir(|root| {
|
||||||
|
assert_eq!(mcp_config_file(), root.join(MCP_FILE_NAME));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn falls_back_to_legacy_location_when_only_it_exists() {
|
||||||
|
with_config_dir(|root| {
|
||||||
|
let legacy = root.join(FUNCTIONS_DIR_NAME).join(MCP_FILE_NAME);
|
||||||
|
fs::write(&legacy, "{}").unwrap();
|
||||||
|
assert_eq!(mcp_config_file(), legacy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn prefers_new_location_when_both_exist() {
|
||||||
|
with_config_dir(|root| {
|
||||||
|
let preferred = root.join(MCP_FILE_NAME);
|
||||||
|
let legacy = root.join(FUNCTIONS_DIR_NAME).join(MCP_FILE_NAME);
|
||||||
|
fs::write(&preferred, "{}").unwrap();
|
||||||
|
fs::write(&legacy, "{}").unwrap();
|
||||||
|
assert_eq!(mcp_config_file(), preferred);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn uses_preferred_location_when_only_it_exists() {
|
||||||
|
with_config_dir(|root| {
|
||||||
|
let preferred = root.join(MCP_FILE_NAME);
|
||||||
|
fs::write(&preferred, "{}").unwrap();
|
||||||
|
assert_eq!(mcp_config_file(), preferred);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sandbox_kit_override_reflects_env_var_state() {
|
fn sandbox_kit_override_reflects_env_var_state() {
|
||||||
let env_name = get_env_name("sandbox_kit");
|
let env_name = get_env_name("sandbox_kit");
|
||||||
|
|||||||
@@ -1865,6 +1865,7 @@ impl RequestContext {
|
|||||||
("rags_dir", display_path(&paths::rags_dir())),
|
("rags_dir", display_path(&paths::rags_dir())),
|
||||||
("macros_dir", display_path(&paths::macros_dir())),
|
("macros_dir", display_path(&paths::macros_dir())),
|
||||||
("functions_dir", display_path(&paths::functions_dir())),
|
("functions_dir", display_path(&paths::functions_dir())),
|
||||||
|
("mcp_config_file", display_path(&paths::mcp_config_file())),
|
||||||
("sbx_kit_dir", display_path(&paths::sbx_kit_dir())),
|
("sbx_kit_dir", display_path(&paths::sbx_kit_dir())),
|
||||||
("messages_file", display_path(&self.messages_file())),
|
("messages_file", display_path(&self.messages_file())),
|
||||||
];
|
];
|
||||||
|
|||||||
+5
-1
@@ -517,7 +517,11 @@ impl Functions {
|
|||||||
anyhow!("Failed to load embedded function file: {}", file.as_ref())
|
anyhow!("Failed to load embedded function file: {}", file.as_ref())
|
||||||
})?;
|
})?;
|
||||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||||
let file_path = paths::functions_dir().join(file.as_ref());
|
let file_path = if file.as_ref() == "mcp.json" {
|
||||||
|
paths::mcp_config_file()
|
||||||
|
} else {
|
||||||
|
paths::functions_dir().join(file.as_ref())
|
||||||
|
};
|
||||||
let is_script = file_path
|
let is_script = file_path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(OsStr::to_str)
|
.and_then(OsStr::to_str)
|
||||||
|
|||||||
Reference in New Issue
Block a user