pub(crate) mod activity;
pub mod background_shell;
pub use lean_ctx::core::bounded_lock;
pub use lean_ctx::core::bypass_hint;
mod call_identity;
#[cfg(test)]
mod catalog_tests;
pub mod compaction_sync;
pub mod context_gate;
#[cfg(test)]
mod decision_loop_integration_tests;
mod dispatch;
pub use dispatch::canonicalize_arg_aliases;
pub mod dynamic_tools;
pub mod elicitation;
pub(crate) mod execute;
#[cfg(unix)]
pub use execute::register_protected_shell_runner;
mod file_resource;
pub mod helpers;
#[cfg(any(test, feature = "test-support"))]
pub mod isolation_fixture;
pub mod mcp_idle;
pub mod multi_path;
pub mod native_receipts;
pub mod notifications;
pub mod permission_inheritance;
pub mod policy_guard;
pub mod predictive_preload;
pub mod progress;
pub mod prompts;
pub use lean_ctx::core::reference_store;
pub mod registry;
pub mod resources;
pub mod role_guard;
pub use lean_ctx::core::mcp_roots as roots;
pub mod schema_hook;
pub(crate) mod shell_approval;
pub use lean_ctx::core::mcp_roots::{derive_project_root_from, derive_project_root_from_cwd};
use lean_ctx::core::pathutil::has_project_marker;
pub mod tool_trait;
pub mod tool_visibility;
pub mod tools_config_watch;
pub(crate) mod walk_hint;
#[cfg(all(test, unix))]
mod workspace_roots_tests;
use futures::FutureExt;
use rmcp::ErrorData;
use rmcp::handler::server::ServerHandler;
use rmcp::model::{
CallToolRequestParams, CallToolResult, ContentBlock, Implementation, InitializeRequestParams,
InitializeResult, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerConfig,
};
use rmcp::service::{RequestContext, RoleServer};
use crate::tools::{CrpMode, LeanCtxServer};
mod call_tool;
pub(crate) use call_tool::SessionSwitch;
mod post_dispatch;
mod post_process;
mod server_handler;
pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
lean_ctx::instructions::build_instructions_for_test(crp_mode)
}
pub fn build_claude_code_instructions_for_test() -> String {
lean_ctx::instructions::claude_code_instructions()
}
pub fn build_claude_code_static_instructions_for_test() -> String {
lean_ctx::instructions::claude_code_static_instructions_for_test()
}
#[cfg(test)]
use lean_ctx::core::pathutil::is_broad_or_unsafe_root;
pub fn tool_descriptions_for_test() -> Vec<(String, String)> {
crate::server::registry::build_registry()
.tool_defs()
.into_iter()
.map(|t| {
(
t.name.to_string(),
t.description.as_deref().unwrap_or("").to_string(),
)
})
.collect()
}
pub fn tool_schemas_json_for_test() -> String {
crate::server::registry::build_registry()
.tool_defs()
.iter()
.map(|t| {
format!(
"{}: {}",
t.name,
serde_json::to_string(&t.input_schema).unwrap_or_default()
)
})
.collect::<Vec<_>>()
.join("\n")
}
pub use lean_ctx::core::workflow::WORKFLOW_PASSTHROUGH_TOOLS;
pub fn is_workflow_stale(run: &lean_ctx::core::workflow::types::WorkflowRun) -> bool {
lean_ctx::core::workflow::is_run_stale(run)
}
fn is_client_hook_covered(client_name: &str) -> bool {
lean_ctx::core::home::resolve_home_dir()
.is_some_and(|home| lean_ctx::core::rules_channel::client_hook_covered(client_name, &home))
}
fn is_shell_tool_name(name: &str) -> bool {
matches!(name, "ctx_shell" | "ctx_execute")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn home_dir_detected_as_broad_root() {
let _env = lean_ctx::core::data_dir::test_env_lock();
if let Some(home) = dirs::home_dir() {
assert!(is_broad_or_unsafe_root(&home));
}
}
#[test]
fn agent_dirs_detected() {
let claude = std::path::PathBuf::from("/home/user/.claude");
assert!(is_broad_or_unsafe_root(&claude));
let codex = std::path::PathBuf::from("/home/user/.codex");
assert!(is_broad_or_unsafe_root(&codex));
let project = std::path::PathBuf::from("/home/user/projects/myapp");
assert!(!is_broad_or_unsafe_root(&project));
}
#[test]
fn test_unified_tool_count() {
let tools = lean_ctx::tool_defs::unified_tool_defs();
assert_eq!(tools.len(), 5, "Expected 5 unified tools");
}
#[test]
fn test_granular_tool_count() {
registry::register_tool_catalog();
let tools = lean_ctx::tool_defs::granular_tool_defs();
assert!(tools.len() >= 25, "Expected at least 25 granular tools");
}
#[test]
fn production_server_always_has_registry() {
let server = crate::tools::create_server();
assert!(
server.registry.is_some(),
"production server must carry a tool registry"
);
}
#[test]
fn disabled_tools_filters_list() {
registry::register_tool_catalog();
let all = lean_ctx::tool_defs::granular_tool_defs();
let total = all.len();
let disabled = ["ctx_graph".to_string()];
let filtered: Vec<_> = all
.into_iter()
.filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
.collect();
assert_eq!(filtered.len(), total - 1);
assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_graph"));
assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_agent"));
}
#[test]
fn empty_disabled_tools_returns_all() {
let all = lean_ctx::tool_defs::granular_tool_defs();
let total = all.len();
let disabled: Vec<String> = vec![];
let filtered: Vec<_> = all
.into_iter()
.filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
.collect();
assert_eq!(filtered.len(), total);
}
#[test]
fn misspelled_disabled_tool_is_silently_ignored() {
let all = lean_ctx::tool_defs::granular_tool_defs();
let total = all.len();
let disabled = ["ctx_nonexistent_tool".to_string()];
let filtered: Vec<_> = all
.into_iter()
.filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
.collect();
assert_eq!(filtered.len(), total);
}
#[test]
fn is_broad_or_unsafe_root_rejects_home() {
let _env = lean_ctx::core::data_dir::test_env_lock();
if let Some(home) = dirs::home_dir() {
assert!(is_broad_or_unsafe_root(&home));
}
}
#[test]
fn is_broad_or_unsafe_root_rejects_filesystem_root() {
assert!(is_broad_or_unsafe_root(std::path::Path::new("/")));
}
#[test]
fn is_broad_or_unsafe_root_rejects_agent_dirs() {
assert!(is_broad_or_unsafe_root(std::path::Path::new(
"/home/user/.claude"
)));
assert!(is_broad_or_unsafe_root(std::path::Path::new(
"/home/user/.codex"
)));
}
#[test]
fn is_broad_or_unsafe_root_allows_project_subdir() {
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("my-project");
std::fs::create_dir_all(&subdir).unwrap();
assert!(!is_broad_or_unsafe_root(&subdir));
}
#[test]
fn is_broad_or_unsafe_root_allows_tmp_subdirs() {
assert!(!is_broad_or_unsafe_root(std::path::Path::new(
"/tmp/leanctx-test"
)));
assert!(!is_broad_or_unsafe_root(std::path::Path::new(
"/tmp/my-project"
)));
}
#[test]
fn is_broad_or_unsafe_root_allows_home_subdirs() {
let _env = lean_ctx::core::data_dir::test_env_lock();
if let Some(home) = dirs::home_dir() {
let subdir = home.join("projects").join("my-app");
assert!(!is_broad_or_unsafe_root(&subdir));
}
}
}