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