use super::dynamic_tools::{ToolCategory, categorize_tool};
use crate::core::tool_profiles::ToolProfile;
pub const INVOKER: &str = "ctx_call";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateSet {
Full,
Unified,
ProfileAuthoritative,
LazyCore,
}
#[must_use]
pub fn candidate_set(full_mode: bool, unified_env: bool, explicit_profile: bool) -> CandidateSet {
if full_mode {
CandidateSet::Full
} else if unified_env {
CandidateSet::Unified
} else if explicit_profile {
CandidateSet::ProfileAuthoritative
} else {
CandidateSet::LazyCore
}
}
#[must_use]
pub fn explicit_profile(cfg: &crate::core::config::Config) -> bool {
cfg.tool_profile.is_some()
|| !cfg.tools_enabled.is_empty()
|| std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok()
}
#[must_use]
pub fn is_tool_visible(
name: &str,
profile: &ToolProfile,
disabled: &[String],
is_zed: bool,
role_allows: bool,
) -> bool {
if categorize_tool(name) == ToolCategory::Internal {
return false;
}
if super::dynamic_tools::is_deprecated_alias(name) {
return false;
}
if !profile.is_tool_enabled(name) {
return false;
}
if disabled.iter().any(|d| d == name) {
return false;
}
if is_zed && name == "ctx_edit" {
return false;
}
role_allows
}
#[must_use]
pub fn advertised_tool_defs_default() -> Vec<rmcp::model::Tool> {
let cfg = crate::core::config::Config::load();
let disabled = cfg.disabled_tools_effective();
let profile = cfg.tool_profile_effective();
let full_mode = crate::tool_defs::is_full_mode();
let registry = crate::server::registry::build_registry();
let candidate = candidate_set(
full_mode,
std::env::var("LEAN_CTX_UNIFIED").is_ok(),
explicit_profile(&cfg),
);
let pool: Vec<rmcp::model::Tool> = match candidate {
CandidateSet::Full | CandidateSet::ProfileAuthoritative => registry.tool_defs(),
CandidateSet::Unified => crate::tool_defs::unified_tool_defs(),
CandidateSet::LazyCore => {
let core = crate::tool_defs::core_tool_names();
registry
.tool_defs()
.into_iter()
.filter(|t| core.contains(&t.name.as_ref()))
.collect()
}
};
let mut tools: Vec<_> = pool
.into_iter()
.filter(|t| is_tool_visible(t.name.as_ref(), &profile, &disabled, false, true))
.collect();
let already = tools.iter().any(|t| t.name.as_ref() == INVOKER);
if needs_invoker(full_mode, already, true, &disabled)
&& let Some(def) = registry
.tool_defs()
.into_iter()
.find(|t| t.name.as_ref() == INVOKER)
{
tools.push(def);
}
let level = crate::core::config::CompressionLevel::effective(&cfg);
let mode = crate::core::terse::mcp_compress::DescriptionMode::from_compression_level(&level);
if mode == crate::core::terse::mcp_compress::DescriptionMode::Full {
return tools;
}
tools
.into_iter()
.map(|mut t| {
let compressed = crate::core::terse::mcp_compress::compress_description(
t.name.as_ref(),
t.description.as_deref().unwrap_or(""),
mode,
);
t.description = Some(compressed.into());
t
})
.collect()
}
#[must_use]
pub fn category_gate_applies(supports_list_changed: bool, explicit_profile: bool) -> bool {
supports_list_changed && !explicit_profile
}
#[must_use]
pub fn needs_invoker(
full_mode: bool,
already_present: bool,
invoker_role_allowed: bool,
disabled: &[String],
) -> bool {
!full_mode && !already_present && invoker_role_allowed && !disabled.iter().any(|d| d == INVOKER)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal_tools_never_visible_even_in_power() {
let p = ToolProfile::Power;
assert!(!is_tool_visible("ctx_metrics", &p, &[], false, true));
assert!(!is_tool_visible("ctx_cost", &p, &[], false, true));
assert!(!is_tool_visible("ctx_discover_tools", &p, &[], false, true));
}
#[test]
fn deprecated_aliases_never_visible_even_in_power() {
let p = ToolProfile::Power;
assert!(!is_tool_visible("ctx_smart_read", &p, &[], false, true));
assert!(!is_tool_visible("ctx_multi_read", &p, &[], false, true));
}
#[test]
fn deprecated_aliases_stay_registered_and_callable() {
let _guard = crate::core::data_dir::isolated_data_dir();
let defs = crate::server::registry::build_registry().tool_defs();
for name in [
"ctx_smart_read",
"ctx_multi_read",
"ctx_semantic_search",
"ctx_symbol",
] {
assert!(
defs.iter().any(|t| t.name.as_ref() == name),
"{name} must stay registered (callable) even though hidden"
);
assert!(
!is_tool_visible(name, &ToolProfile::Power, &[], false, true),
"{name} must be hidden from tools/list"
);
}
}
#[test]
fn core_tool_visible_under_power() {
assert!(is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&[],
false,
true
));
}
#[test]
fn standard_exposes_its_advertised_tools() {
let p = ToolProfile::Standard;
assert!(is_tool_visible("ctx_execute", &p, &[], false, true));
assert!(is_tool_visible("ctx_explore", &p, &[], false, true));
assert!(is_tool_visible("ctx_callgraph", &p, &[], false, true));
assert!(is_tool_visible("ctx_graph", &p, &[], false, true));
}
#[test]
fn folded_search_aliases_never_visible() {
let p = ToolProfile::Power;
assert!(!is_tool_visible(
"ctx_semantic_search",
&p,
&[],
false,
true
));
assert!(!is_tool_visible("ctx_symbol", &p, &[], false, true));
assert!(is_tool_visible("ctx_search", &p, &[], false, true));
}
#[test]
fn minimal_hides_non_minimal_tools() {
let p = ToolProfile::Minimal;
assert!(is_tool_visible("ctx_read", &p, &[], false, true));
assert!(!is_tool_visible("ctx_architecture", &p, &[], false, true));
}
#[test]
fn disabled_list_filters() {
let disabled = vec!["ctx_read".to_string()];
assert!(!is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&disabled,
false,
true
));
}
#[test]
fn zed_hides_ctx_edit_only() {
let p = ToolProfile::Power;
assert!(!is_tool_visible("ctx_edit", &p, &[], true, true));
assert!(is_tool_visible("ctx_read", &p, &[], true, true));
}
#[test]
fn role_block_hides_tool() {
assert!(!is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&[],
false,
false
));
}
#[test]
fn category_gate_only_in_default_lean_mode() {
assert!(category_gate_applies(true, false));
assert!(!category_gate_applies(true, true));
assert!(!category_gate_applies(false, false));
assert!(!category_gate_applies(false, true));
}
#[test]
fn invoker_added_when_missing_in_lazy_mode() {
assert!(needs_invoker(false, false, true, &[]));
}
#[test]
fn invoker_not_added_in_full_mode() {
assert!(!needs_invoker(true, false, true, &[]));
}
#[test]
fn invoker_not_duplicated_when_present() {
assert!(!needs_invoker(false, true, true, &[]));
}
#[test]
fn invoker_respects_role_and_disabled() {
assert!(!needs_invoker(false, false, false, &[]));
assert!(!needs_invoker(
false,
false,
true,
&["ctx_call".to_string()]
));
}
#[test]
fn core_tool_surface_stays_within_budget() {
const PER_TOOL_BUDGET: usize = 360;
const TOTAL_BUDGET: usize = 2340;
let _guard = crate::core::data_dir::isolated_data_dir();
let core = crate::tool_defs::core_tool_names();
let defs: Vec<_> = crate::server::registry::build_registry()
.tool_defs()
.into_iter()
.filter(|t| core.contains(&t.name.as_ref()))
.collect();
assert_eq!(defs.len(), core.len(), "every core tool must be registered");
let mut total = 0usize;
for t in &defs {
let desc = t.description.as_deref().unwrap_or("");
let schema = serde_json::to_string(&t.input_schema).unwrap_or_default();
let cost = crate::core::tokens::count_tokens(desc)
+ crate::core::tokens::count_tokens(&schema);
eprintln!("{:24} {cost:4} tok", t.name.as_ref());
assert!(
cost <= PER_TOOL_BUDGET,
"{} costs {cost} tok (budget {PER_TOOL_BUDGET}) — trim its description/schema",
t.name
);
total += cost;
}
eprintln!("CORE TOTAL: {total} tok / {} tools", defs.len());
assert!(
total <= TOTAL_BUDGET,
"core surface costs {total} tok (budget {TOTAL_BUDGET})"
);
}
}