fix(tui): count workspace MCP servers in status surfaces
fix(tui): count workspace MCP servers in status surfaces
This commit is contained in:
@@ -264,6 +264,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- The TUI footer, `/status`, `/mcp` manager, and command-palette MCP entries
|
||||
now count trusted workspace-local `.codewhale/mcp.json` servers together with
|
||||
the global MCP config, matching `codewhale mcp list` for merged global +
|
||||
project setups (#2787). Thanks @yekern for the detailed reproduction.
|
||||
- Sub-agent shell completions now refresh the workspace branch/status chip
|
||||
immediately, and `/subagents` plus the Agents sidebar show each sub-agent's
|
||||
current workspace branch when it is running in a child worktree.
|
||||
|
||||
@@ -264,6 +264,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- The TUI footer, `/status`, `/mcp` manager, and command-palette MCP entries
|
||||
now count trusted workspace-local `.codewhale/mcp.json` servers together with
|
||||
the global MCP config, matching `codewhale mcp list` for merged global +
|
||||
project setups (#2787). Thanks @yekern for the detailed reproduction.
|
||||
- Sub-agent shell completions now refresh the workspace branch/status chip
|
||||
immediately, and `/subagents` plus the Agents sidebar show each sub-agent's
|
||||
current workspace branch when it is running in a child worktree.
|
||||
|
||||
@@ -2859,6 +2859,7 @@ pub fn set_server_enabled(path: &Path, name: &str, enabled: bool) -> Result<()>
|
||||
save_config(path, &cfg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn manager_snapshot_from_config(
|
||||
path: &Path,
|
||||
restart_required: bool,
|
||||
@@ -2873,6 +2874,22 @@ pub fn manager_snapshot_from_config(
|
||||
))
|
||||
}
|
||||
|
||||
pub fn manager_snapshot_from_config_with_workspace(
|
||||
path: &Path,
|
||||
workspace: &Path,
|
||||
restart_required: bool,
|
||||
) -> Result<McpManagerSnapshot> {
|
||||
let cfg = load_config_with_workspace(path, workspace)?;
|
||||
Ok(snapshot_from_config(
|
||||
path,
|
||||
path.exists(),
|
||||
restart_required,
|
||||
&cfg,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn discover_manager_snapshot(
|
||||
path: &Path,
|
||||
network_policy: Option<NetworkPolicyDecider>,
|
||||
@@ -2898,6 +2915,32 @@ pub async fn discover_manager_snapshot(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn discover_manager_snapshot_with_workspace(
|
||||
path: &Path,
|
||||
workspace: &Path,
|
||||
network_policy: Option<NetworkPolicyDecider>,
|
||||
restart_required: bool,
|
||||
) -> Result<McpManagerSnapshot> {
|
||||
let cfg = load_config_with_workspace(path, workspace)?;
|
||||
let mut pool = McpPool::new(cfg.clone());
|
||||
if let Some(policy) = network_policy {
|
||||
pool = pool.with_network_policy(policy);
|
||||
}
|
||||
let errors = pool
|
||||
.connect_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(name, err)| (name, format!("{err:#}")))
|
||||
.collect::<HashMap<_, _>>();
|
||||
Ok(snapshot_from_config(
|
||||
path,
|
||||
path.exists(),
|
||||
restart_required,
|
||||
&cfg,
|
||||
Some((&pool, &errors)),
|
||||
))
|
||||
}
|
||||
|
||||
fn snapshot_from_config(
|
||||
path: &Path,
|
||||
config_exists: bool,
|
||||
@@ -3385,6 +3428,49 @@ mod tests {
|
||||
assert_eq!(shared.cwd.as_deref(), Some(workspace.as_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_manager_snapshot_counts_global_and_project_servers() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let global_path = dir.path().join("global-mcp.json");
|
||||
let workspace = dir.path().join("workspace");
|
||||
let project_dir = workspace.join(".codewhale");
|
||||
fs::create_dir_all(&project_dir).unwrap();
|
||||
let _trust = mark_workspace_trusted(&workspace);
|
||||
fs::write(
|
||||
&global_path,
|
||||
r#"{
|
||||
"servers": {
|
||||
"chrome-devtools": {"command": "npx", "args": ["-y", "chrome-devtools-mcp@latest"]},
|
||||
"context7": {"command": "npx", "args": ["-y", "@upstash/context7-mcp@latest"]}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
project_dir.join("mcp.json"),
|
||||
r#"{
|
||||
"servers": {
|
||||
"laravel-boost": {"command": "php", "args": ["artisan", "boost:mcp"]}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plain = manager_snapshot_from_config(&global_path, false).unwrap();
|
||||
let merged =
|
||||
manager_snapshot_from_config_with_workspace(&global_path, &workspace, false).unwrap();
|
||||
|
||||
assert_eq!(plain.servers.len(), 2);
|
||||
assert_eq!(merged.servers.len(), 3);
|
||||
assert!(
|
||||
merged
|
||||
.servers
|
||||
.iter()
|
||||
.any(|server| server.name == "laravel-boost"),
|
||||
"workspace-aware snapshots must include trusted project MCP servers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_mcp_config_ignores_project_file_until_workspace_trusted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -2043,6 +2043,10 @@ impl App {
|
||||
}
|
||||
_ => (String::new(), 0, false),
|
||||
};
|
||||
let mcp_configured_count =
|
||||
crate::mcp::load_config_with_workspace(&mcp_config_path, &workspace)
|
||||
.map(|cfg| cfg.servers.len())
|
||||
.unwrap_or(0);
|
||||
Self {
|
||||
mode: initial_mode,
|
||||
composer: ComposerState {
|
||||
@@ -2201,11 +2205,9 @@ impl App {
|
||||
// Read the MCP config once at boot to know how many servers
|
||||
// the user has declared. The footer chip uses this even when
|
||||
// no live snapshot is available (#502). Cheap (just reads
|
||||
// the JSON file); errors fall through to zero so a missing
|
||||
// the JSON files); errors fall through to zero so a missing
|
||||
// or malformed config simply hides the chip.
|
||||
mcp_configured_count: crate::mcp::load_config(&mcp_config_path)
|
||||
.map(|cfg| cfg.servers.len())
|
||||
.unwrap_or(0),
|
||||
mcp_configured_count,
|
||||
mcp_restart_required: false,
|
||||
tool_log: Vec::new(),
|
||||
active_skill: None,
|
||||
|
||||
@@ -162,7 +162,7 @@ pub fn build_entries(
|
||||
tool_entries.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
entries.extend(tool_entries);
|
||||
|
||||
entries.extend(build_mcp_entries(mcp_config_path, mcp_snapshot));
|
||||
entries.extend(build_mcp_entries(workspace, mcp_config_path, mcp_snapshot));
|
||||
|
||||
entries.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
entries.sort_by_key(|entry| entry.section);
|
||||
@@ -170,11 +170,13 @@ pub fn build_entries(
|
||||
}
|
||||
|
||||
fn build_mcp_entries(
|
||||
workspace: &Path,
|
||||
mcp_config_path: &Path,
|
||||
mcp_snapshot: Option<&crate::mcp::McpManagerSnapshot>,
|
||||
) -> Vec<CommandPaletteEntry> {
|
||||
let owned_snapshot = if mcp_snapshot.is_none() {
|
||||
crate::mcp::manager_snapshot_from_config(mcp_config_path, false).ok()
|
||||
crate::mcp::manager_snapshot_from_config_with_workspace(mcp_config_path, workspace, false)
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -6529,9 +6529,19 @@ async fn handle_mcp_ui_action(
|
||||
let network_policy = config.network.clone().map(|toml_cfg| {
|
||||
crate::network_policy::NetworkPolicyDecider::with_default_audit(toml_cfg.into_runtime())
|
||||
});
|
||||
mcp::discover_manager_snapshot(&path, network_policy, app.mcp_restart_required).await
|
||||
mcp::discover_manager_snapshot_with_workspace(
|
||||
&path,
|
||||
&app.workspace,
|
||||
network_policy,
|
||||
app.mcp_restart_required,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
mcp::manager_snapshot_from_config(&path, app.mcp_restart_required)
|
||||
mcp::manager_snapshot_from_config_with_workspace(
|
||||
&path,
|
||||
&app.workspace,
|
||||
app.mcp_restart_required,
|
||||
)
|
||||
};
|
||||
|
||||
match snapshot_result {
|
||||
|
||||
Reference in New Issue
Block a user