Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6958e9cba8
|
||
|
|
825f9f6bf5
|
+4
-2
@@ -138,8 +138,10 @@ enabled_mcp_servers: null # Which MCP servers to enable by default.
|
|||||||
# enabled_mcp_servers: github,slack,ddg-search
|
# enabled_mcp_servers: github,slack,ddg-search
|
||||||
no_workspace_mcp: false # Disable loading workspace-local MCP servers from .coyote/mcp.json (default: false).
|
no_workspace_mcp: false # Disable loading workspace-local MCP servers from .coyote/mcp.json (default: false).
|
||||||
# When false (the default), Coyote merges .coyote/mcp.json from the current directory
|
# When false (the default), Coyote merges .coyote/mcp.json from the current directory
|
||||||
# into the global MCP registry at startup. Workspace entries shadow global ones on
|
# into the global MCP registry at startup. If mcp.json is absent, Coyote falls back
|
||||||
# name collision. Set to true (or pass --no-workspace-mcp) to skip this entirely.
|
# to .coyote/.mcp.json (leading dot) for compatibility with Claude-style
|
||||||
|
# configurations. Workspace entries shadow global ones on name collision.
|
||||||
|
# Set to true (or pass --no-workspace-mcp) to skip this entirely.
|
||||||
|
|
||||||
# ---- 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.
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ pub struct Cli {
|
|||||||
/// Display the message without sending it
|
/// Display the message without sending it
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
/// Disable loading workspace MCP servers from .coyote/mcp.json
|
/// Disable loading workspace MCP servers from .coyote/mcp.json (or .coyote/.mcp.json)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_workspace_mcp: bool,
|
pub no_workspace_mcp: bool,
|
||||||
/// Disable memory for this invocation
|
/// Disable memory for this invocation
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ const GLOBAL_TOOLS_DIR_NAME: &str = "tools";
|
|||||||
const GLOBAL_TOOLS_UTILS_DIR_NAME: &str = "utils";
|
const GLOBAL_TOOLS_UTILS_DIR_NAME: &str = "utils";
|
||||||
const BASH_PROMPT_UTILS_FILE_NAME: &str = "prompt-utils.sh";
|
const BASH_PROMPT_UTILS_FILE_NAME: &str = "prompt-utils.sh";
|
||||||
const MCP_FILE_NAME: &str = "mcp.json";
|
const MCP_FILE_NAME: &str = "mcp.json";
|
||||||
|
const HIDDEN_MCP_FILE_NAME: &str = ".mcp.json";
|
||||||
const MEMORY_DIR_NAME: &str = "memory";
|
const MEMORY_DIR_NAME: &str = "memory";
|
||||||
const MEMORY_INDEX_FILE_NAME: &str = "MEMORY.md";
|
const MEMORY_INDEX_FILE_NAME: &str = "MEMORY.md";
|
||||||
const WORKSPACE_MEMORY_FILE_NAME: &str = "COYOTE.md";
|
const WORKSPACE_MEMORY_FILE_NAME: &str = "COYOTE.md";
|
||||||
|
|||||||
+84
-12
@@ -2,10 +2,10 @@ use super::role::Role;
|
|||||||
use super::{
|
use super::{
|
||||||
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
||||||
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
||||||
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
|
GLOBAL_TOOLS_UTILS_DIR_NAME, HIDDEN_MCP_FILE_NAME, MACROS_DIR_NAME, MCP_FILE_NAME,
|
||||||
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
|
MEMORY_DIR_NAME, MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME,
|
||||||
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_MIXIN_KITS_DIR_NAME, SBX_VAULT_MIXINS_DIR_NAME,
|
SBX_KIT_DIR_NAME, SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_MIXIN_KITS_DIR_NAME,
|
||||||
SKILLS_DIR_NAME, WORKSPACE_COYOTE_DIR_NAME,
|
SBX_VAULT_MIXINS_DIR_NAME, SKILLS_DIR_NAME, WORKSPACE_COYOTE_DIR_NAME,
|
||||||
};
|
};
|
||||||
use crate::client::ProviderModels;
|
use crate::client::ProviderModels;
|
||||||
use crate::config::REPL_HISTORY_DIR_NAME;
|
use crate::config::REPL_HISTORY_DIR_NAME;
|
||||||
@@ -193,22 +193,31 @@ pub fn skill_file(name: &str) -> PathBuf {
|
|||||||
skill_dir(name).join("SKILL.md")
|
skill_dir(name).join("SKILL.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_skills_dir() -> PathBuf {
|
pub fn workspace_config_dir() -> PathBuf {
|
||||||
|
let workspace_dir_name = match env::var(get_env_name("workspace_config_dir")) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => WORKSPACE_COYOTE_DIR_NAME.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
env::current_dir()
|
env::current_dir()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.join(WORKSPACE_COYOTE_DIR_NAME)
|
.join(workspace_dir_name)
|
||||||
.join(SKILLS_DIR_NAME)
|
}
|
||||||
|
|
||||||
|
pub fn workspace_skills_dir() -> PathBuf {
|
||||||
|
workspace_config_dir().join(SKILLS_DIR_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_skill_file(name: &str) -> PathBuf {
|
pub fn workspace_skill_file(name: &str) -> PathBuf {
|
||||||
workspace_skills_dir().join(name).join("SKILL.md")
|
workspace_skills_dir().join(name).join("SKILL.md")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_mcp_config_file() -> PathBuf {
|
pub fn workspace_mcp_config_file() -> Option<PathBuf> {
|
||||||
env::current_dir()
|
let dir = workspace_config_dir();
|
||||||
.unwrap_or_default()
|
[MCP_FILE_NAME, HIDDEN_MCP_FILE_NAME]
|
||||||
.join(WORKSPACE_COYOTE_DIR_NAME)
|
.into_iter()
|
||||||
.join(MCP_FILE_NAME)
|
.map(|name| dir.join(name))
|
||||||
|
.find(|candidate| candidate.is_file())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_skill_name(name: &str) -> Result<()> {
|
pub fn validate_skill_name(name: &str) -> Result<()> {
|
||||||
@@ -682,6 +691,69 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod workspace_mcp_resolution {
|
||||||
|
use super::*;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
fn with_workspace_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-workspace-mcp-test-{unique}"));
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
let env_name = get_env_name("workspace_config_dir");
|
||||||
|
let prev = env::var_os(&env_name);
|
||||||
|
unsafe {
|
||||||
|
env::set_var(&env_name, &root);
|
||||||
|
}
|
||||||
|
f(&root);
|
||||||
|
unsafe {
|
||||||
|
match prev {
|
||||||
|
Some(v) => env::set_var(&env_name, v),
|
||||||
|
None => env::remove_var(&env_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn returns_none_when_no_config_exists() {
|
||||||
|
with_workspace_dir(|_| {
|
||||||
|
assert_eq!(workspace_mcp_config_file(), None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn finds_mcp_json() {
|
||||||
|
with_workspace_dir(|root| {
|
||||||
|
fs::write(root.join("mcp.json"), "{}").unwrap();
|
||||||
|
assert_eq!(workspace_mcp_config_file(), Some(root.join("mcp.json")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn falls_back_to_claude_style_hidden_mcp_json() {
|
||||||
|
with_workspace_dir(|root| {
|
||||||
|
fs::write(root.join(".mcp.json"), "{}").unwrap();
|
||||||
|
assert_eq!(workspace_mcp_config_file(), Some(root.join(".mcp.json")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn prefers_mcp_json_when_both_exist() {
|
||||||
|
with_workspace_dir(|root| {
|
||||||
|
fs::write(root.join("mcp.json"), "{}").unwrap();
|
||||||
|
fs::write(root.join(".mcp.json"), "{}").unwrap();
|
||||||
|
assert_eq!(workspace_mcp_config_file(), Some(root.join("mcp.json")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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");
|
||||||
|
|||||||
+6
-7
@@ -215,9 +215,9 @@ impl McpRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut merged = mcp_servers_config;
|
let mut merged = mcp_servers_config;
|
||||||
if !app_config.no_workspace_mcp {
|
if !app_config.no_workspace_mcp
|
||||||
let ws_path = paths::workspace_mcp_config_file();
|
&& let Some(ws_path) = paths::workspace_mcp_config_file()
|
||||||
if ws_path.try_exists().unwrap_or(false) {
|
{
|
||||||
match tokio::fs::read_to_string(&ws_path).await {
|
match tokio::fs::read_to_string(&ws_path).await {
|
||||||
Ok(ws_content) if !ws_content.trim().is_empty() => {
|
Ok(ws_content) if !ws_content.trim().is_empty() => {
|
||||||
match interpolate_secrets(&ws_content, vault) {
|
match interpolate_secrets(&ws_content, vault) {
|
||||||
@@ -243,9 +243,9 @@ impl McpRegistry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => warn!(
|
Err(e) => {
|
||||||
"Failed to parse workspace MCP config: {e}. Skipping."
|
warn!("Failed to parse workspace MCP config: {e}. Skipping.")
|
||||||
),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((_, missing)) => warn!(
|
Ok((_, missing)) => warn!(
|
||||||
@@ -259,7 +259,6 @@ impl McpRegistry {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
registry.config = Some(merged);
|
registry.config = Some(merged);
|
||||||
|
|
||||||
if start_mcp_servers && app_config.mcp_server_support {
|
if start_mcp_servers && app_config.mcp_server_support {
|
||||||
|
|||||||
Reference in New Issue
Block a user