lean_ctx_server/server/
mod.rs1pub(crate) mod activity;
3pub mod background_shell;
4pub use lean_ctx::core::bounded_lock;
5pub use lean_ctx::core::bypass_hint;
6mod call_identity;
7#[cfg(test)]
8mod catalog_tests;
9pub mod compaction_sync;
10pub mod context_gate;
11#[cfg(test)]
12mod decision_loop_integration_tests;
13mod dispatch;
14pub use dispatch::canonicalize_arg_aliases;
15pub mod dynamic_tools;
16pub mod elicitation;
17pub(crate) mod execute;
18#[cfg(unix)]
19pub use execute::register_protected_shell_runner;
20mod file_resource;
21pub mod helpers;
22#[cfg(any(test, feature = "test-support"))]
23pub mod isolation_fixture;
24pub mod mcp_idle;
25pub mod multi_path;
26pub mod native_receipts;
27pub mod notifications;
28pub mod permission_inheritance;
29pub mod policy_guard;
30pub mod predictive_preload;
31pub mod progress;
32pub mod prompts;
33pub use lean_ctx::core::reference_store;
34pub mod registry;
35pub mod resources;
36pub mod role_guard;
37pub use lean_ctx::core::mcp_roots as roots;
38pub mod schema_hook;
39pub(crate) mod shell_approval;
40pub use lean_ctx::core::mcp_roots::{derive_project_root_from, derive_project_root_from_cwd};
41use lean_ctx::core::pathutil::has_project_marker;
42pub mod tool_trait;
43pub mod tool_visibility;
44pub mod tools_config_watch;
45pub(crate) mod walk_hint;
46#[cfg(all(test, unix))]
48mod workspace_roots_tests;
49
50use futures::FutureExt;
51use rmcp::ErrorData;
52use rmcp::handler::server::ServerHandler;
53use rmcp::model::{
54 CallToolRequestParams, CallToolResult, ContentBlock, Implementation, InitializeRequestParams,
55 InitializeResult, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerConfig,
56};
57use rmcp::service::{RequestContext, RoleServer};
58
59use crate::tools::{CrpMode, LeanCtxServer};
60mod call_tool;
61pub(crate) use call_tool::SessionSwitch;
62mod post_dispatch;
63mod post_process;
64mod server_handler;
65
66pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
67 lean_ctx::instructions::build_instructions_for_test(crp_mode)
68}
69
70pub fn build_claude_code_instructions_for_test() -> String {
71 lean_ctx::instructions::claude_code_instructions()
72}
73
74pub fn build_claude_code_static_instructions_for_test() -> String {
77 lean_ctx::instructions::claude_code_static_instructions_for_test()
78}
79
80#[cfg(test)]
82use lean_ctx::core::pathutil::is_broad_or_unsafe_root;
83
84pub fn tool_descriptions_for_test() -> Vec<(String, String)> {
85 crate::server::registry::build_registry()
86 .tool_defs()
87 .into_iter()
88 .map(|t| {
89 (
90 t.name.to_string(),
91 t.description.as_deref().unwrap_or("").to_string(),
92 )
93 })
94 .collect()
95}
96
97pub fn tool_schemas_json_for_test() -> String {
98 crate::server::registry::build_registry()
99 .tool_defs()
100 .iter()
101 .map(|t| {
102 format!(
103 "{}: {}",
104 t.name,
105 serde_json::to_string(&t.input_schema).unwrap_or_default()
106 )
107 })
108 .collect::<Vec<_>>()
109 .join("\n")
110}
111
112pub use lean_ctx::core::workflow::WORKFLOW_PASSTHROUGH_TOOLS;
113
114pub fn is_workflow_stale(run: &lean_ctx::core::workflow::types::WorkflowRun) -> bool {
120 lean_ctx::core::workflow::is_run_stale(run)
121}
122
123fn is_client_hook_covered(client_name: &str) -> bool {
127 lean_ctx::core::home::resolve_home_dir()
128 .is_some_and(|home| lean_ctx::core::rules_channel::client_hook_covered(client_name, &home))
129}
130
131fn is_shell_tool_name(name: &str) -> bool {
132 matches!(name, "ctx_shell" | "ctx_execute")
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn home_dir_detected_as_broad_root() {
141 let _env = lean_ctx::core::data_dir::test_env_lock();
144 if let Some(home) = dirs::home_dir() {
145 assert!(is_broad_or_unsafe_root(&home));
146 }
147 }
148
149 #[test]
150 fn agent_dirs_detected() {
151 let claude = std::path::PathBuf::from("/home/user/.claude");
152 assert!(is_broad_or_unsafe_root(&claude));
153 let codex = std::path::PathBuf::from("/home/user/.codex");
154 assert!(is_broad_or_unsafe_root(&codex));
155 let project = std::path::PathBuf::from("/home/user/projects/myapp");
156 assert!(!is_broad_or_unsafe_root(&project));
157 }
158
159 #[test]
160 fn test_unified_tool_count() {
161 let tools = lean_ctx::tool_defs::unified_tool_defs();
162 assert_eq!(tools.len(), 5, "Expected 5 unified tools");
163 }
164
165 #[test]
166 fn test_granular_tool_count() {
167 registry::register_tool_catalog();
168 let tools = lean_ctx::tool_defs::granular_tool_defs();
169 assert!(tools.len() >= 25, "Expected at least 25 granular tools");
170 }
171
172 #[test]
173 fn production_server_always_has_registry() {
174 let server = crate::tools::create_server();
180 assert!(
181 server.registry.is_some(),
182 "production server must carry a tool registry"
183 );
184 }
185
186 #[test]
187 fn disabled_tools_filters_list() {
188 registry::register_tool_catalog();
189 let all = lean_ctx::tool_defs::granular_tool_defs();
190 let total = all.len();
191 let disabled = ["ctx_graph".to_string()];
196 let filtered: Vec<_> = all
197 .into_iter()
198 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
199 .collect();
200 assert_eq!(filtered.len(), total - 1);
201 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_graph"));
202 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_agent"));
203 }
204
205 #[test]
206 fn empty_disabled_tools_returns_all() {
207 let all = lean_ctx::tool_defs::granular_tool_defs();
208 let total = all.len();
209 let disabled: Vec<String> = vec![];
210 let filtered: Vec<_> = all
211 .into_iter()
212 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
213 .collect();
214 assert_eq!(filtered.len(), total);
215 }
216
217 #[test]
218 fn misspelled_disabled_tool_is_silently_ignored() {
219 let all = lean_ctx::tool_defs::granular_tool_defs();
220 let total = all.len();
221 let disabled = ["ctx_nonexistent_tool".to_string()];
222 let filtered: Vec<_> = all
223 .into_iter()
224 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
225 .collect();
226 assert_eq!(filtered.len(), total);
227 }
228
229 #[test]
230 fn is_broad_or_unsafe_root_rejects_home() {
231 let _env = lean_ctx::core::data_dir::test_env_lock();
234 if let Some(home) = dirs::home_dir() {
235 assert!(is_broad_or_unsafe_root(&home));
236 }
237 }
238
239 #[test]
240 fn is_broad_or_unsafe_root_rejects_filesystem_root() {
241 assert!(is_broad_or_unsafe_root(std::path::Path::new("/")));
242 }
243
244 #[test]
245 fn is_broad_or_unsafe_root_rejects_agent_dirs() {
246 assert!(is_broad_or_unsafe_root(std::path::Path::new(
247 "/home/user/.claude"
248 )));
249 assert!(is_broad_or_unsafe_root(std::path::Path::new(
250 "/home/user/.codex"
251 )));
252 }
253
254 #[test]
255 fn is_broad_or_unsafe_root_allows_project_subdir() {
256 let tmp = tempfile::tempdir().unwrap();
257 let subdir = tmp.path().join("my-project");
258 std::fs::create_dir_all(&subdir).unwrap();
259 assert!(!is_broad_or_unsafe_root(&subdir));
260 }
261
262 #[test]
263 fn is_broad_or_unsafe_root_allows_tmp_subdirs() {
264 assert!(!is_broad_or_unsafe_root(std::path::Path::new(
265 "/tmp/leanctx-test"
266 )));
267 assert!(!is_broad_or_unsafe_root(std::path::Path::new(
268 "/tmp/my-project"
269 )));
270 }
271
272 #[test]
273 fn is_broad_or_unsafe_root_allows_home_subdirs() {
274 let _env = lean_ctx::core::data_dir::test_env_lock();
277 if let Some(home) = dirs::home_dir() {
278 let subdir = home.join("projects").join("my-app");
279 assert!(!is_broad_or_unsafe_root(&subdir));
280 }
281 }
282}