Skip to main content

lean_ctx_server/tools/
server.rs

1// SPDX-License-Identifier: Apache-2.0
2use std::sync::Arc;
3use std::sync::atomic::AtomicUsize;
4use std::time::Instant;
5use tokio::sync::RwLock;
6
7use lean_ctx::core::cache::SessionCache;
8use rmcp::service::{Peer, RoleServer};
9
10pub(super) struct CepComputedStats {
11    pub(super) cep_score: u32,
12    pub(super) cache_util: u32,
13    pub(super) mode_diversity: u32,
14    pub(super) compression_rate: u32,
15    pub(super) total_original: u64,
16    pub(super) total_compressed: u64,
17    pub(super) total_saved: u64,
18    pub(super) mode_counts: std::collections::HashMap<String, u64>,
19    pub(super) complexity: String,
20    pub(super) cache_hits: u64,
21    pub(super) total_reads: u64,
22    pub(super) tool_call_count: u64,
23}
24
25pub use lean_ctx::core::protocol::CrpMode;
26// CrpMode is now defined in core::protocol to avoid reverse-dependency.
27// Re-exported here for backward compatibility. Its inherent methods live in
28// core::protocol as well, so the type stays complete below the tools layer.
29
30/// Thread-safe handle to the shared file content cache.
31pub type SharedCache = Arc<RwLock<SessionCache>>;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SessionMode {
35    /// Traditional single-client session persistence under `~/.lean-ctx/sessions/`.
36    Personal,
37    /// Context OS mode: shared sessions + event bus for multi-client HTTP/team-server.
38    Shared,
39}
40
41/// IDE interaction mode communicated via `_meta.interactionMode` in MCP requests.
42/// Controls which tools are advertised and callable — Plan mode restricts to
43/// read-only tools (see `plan_mode_tools()`), Agent mode exposes the full set.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45#[repr(u8)]
46pub enum InteractionMode {
47    Agent = 0,
48    Plan = 1,
49}
50
51impl InteractionMode {
52    pub fn from_u8(v: u8) -> Self {
53        match v {
54            1 => Self::Plan,
55            _ => Self::Agent,
56        }
57    }
58
59    /// Parse the string value from `_meta.interactionMode`.
60    /// Accepts multiple conventions across IDEs (OpenCode, Cursor, Claude Code).
61    pub fn from_meta_str(s: &str) -> Option<Self> {
62        match s.to_ascii_lowercase().as_str() {
63            "plan" | "readonly" | "read-only" => Some(Self::Plan),
64            "agent" | "normal" | "edit" | "act" => Some(Self::Agent),
65            _ => None,
66        }
67    }
68}
69
70/// Central MCP server state: cache, session, metrics, and autonomy runtime.
71#[derive(Clone)]
72pub struct LeanCtxServer {
73    pub cache: SharedCache,
74    pub session: lean_ctx::core::session::SessionSlot,
75    pub tool_calls: Arc<RwLock<Vec<ToolCallRecord>>>,
76    pub call_count: Arc<AtomicUsize>,
77    /// Every tool call, counted unconditionally at the top of
78    /// `call_tool_guarded`.
79    ///
80    /// `call_count` only advances inside `record_checkpoint`, which returns
81    /// early whenever checkpointing is skipped — and `minimal_overhead`
82    /// (default `true`) skips it for every tool. Anything scheduled off
83    /// `call_count` therefore never runs in a default install. The daily
84    /// telemetry flush was scheduled that way and consequently never sent.
85    pub background_tick: Arc<AtomicUsize>,
86    pub cache_ttl_secs: u64,
87    pub last_call: Arc<RwLock<Instant>>,
88    pub agent_id: Arc<RwLock<Option<String>>>,
89    pub task_envelope: Arc<RwLock<Option<lean_ctx_protocol::TaskEnvelopeV1>>>,
90    /// Installed by the trusted host before serving; never sourced from tool arguments.
91    pub native_receipt_authority:
92        Option<Arc<lean_ctx::core::execution_ledger::host::HostReceiptAuthority>>,
93    pub presence_agent_id: Arc<RwLock<Option<String>>>,
94    /// Single-flight for the fail-closed presence retry: concurrent first
95    /// calls register once instead of racing N writers on the registry lock.
96    pub(crate) presence_init: Arc<tokio::sync::Mutex<()>>,
97    /// The role this session resolved at `initialize` (#1766). The fail-closed
98    /// presence retry re-registers with it instead of the construction-time
99    /// `context-engine` placeholder, which silently turned a `reviewer` (or a
100    /// `coder`) into a context engine.
101    pub(crate) presence_role: Arc<RwLock<Option<String>>>,
102    pub client_name: Arc<RwLock<String>>,
103    pub autonomy: Arc<lean_ctx::core::autonomy::AutonomyState>,
104    pub loop_detector: Arc<RwLock<lean_ctx::core::loop_detection::LoopDetector>>,
105    pub workflow: Arc<RwLock<Option<lean_ctx::core::workflow::WorkflowRun>>>,
106    pub ledger: Arc<RwLock<lean_ctx::core::context_ledger::ContextLedger>>,
107    pub pipeline_stats: Arc<RwLock<lean_ctx::core::pipeline::PipelineStats>>,
108    pub session_mode: SessionMode,
109    pub workspace_id: String,
110    pub channel_id: String,
111    pub context_os: Option<Arc<lean_ctx::core::context_os::ContextOsRuntime>>,
112    pub context_ir: Option<Arc<RwLock<lean_ctx::core::context_ir::ContextIrV1>>>,
113    pub registry: Option<Arc<crate::server::registry::ToolRegistry>>,
114    pub(crate) rules_stale_checked: Arc<std::sync::atomic::AtomicBool>,
115    pub(crate) rules_tip_shown: Arc<std::sync::atomic::AtomicBool>,
116    pub(crate) last_seen_event_id: Arc<std::sync::atomic::AtomicI64>,
117    pub(crate) startup_project_root: Option<String>,
118    pub(crate) startup_shell_cwd: Option<String>,
119    pub(crate) peer: Arc<RwLock<Option<Peer<RoleServer>>>>,
120    /// The user's answers to shell approval prompts in this session.
121    pub(crate) shell_approvals: Arc<crate::server::shell_approval::ShellApprovalState>,
122    pub(crate) has_client_roots: Arc<std::sync::atomic::AtomicBool>,
123    pub(crate) roots_resolved: Arc<std::sync::atomic::AtomicBool>,
124    /// Failed `roots/list` attempts (GH #694): transient failures re-arm
125    /// `roots_resolved` until a small budget is exhausted.
126    pub(crate) roots_list_attempts: Arc<std::sync::atomic::AtomicU32>,
127    pub(crate) bm25_cache:
128        Arc<std::sync::Mutex<Option<lean_ctx::core::bm25_cache::Bm25CacheEntry>>>,
129    pub(crate) _eviction_target: Arc<lean_ctx::core::eviction_orchestrator::EvictionOrchestrator>,
130    pub(crate) progress_sender: crate::server::progress::SharedProgressSender,
131    pub(crate) last_tools_config_hash: Arc<std::sync::atomic::AtomicU64>,
132    /// Current IDE interaction mode (Agent/Plan). Updated from `_meta.interactionMode`
133    /// on each tool call; controls tool visibility and destructive-tool guards.
134    pub(crate) interaction_mode: Arc<std::sync::atomic::AtomicU8>,
135}
136
137pub use lean_ctx::core::protocol::ToolCallRecord;
138
139#[cfg(test)]
140mod tests {
141    use super::InteractionMode;
142
143    #[test]
144    fn from_u8_round_trips() {
145        assert_eq!(InteractionMode::from_u8(0), InteractionMode::Agent);
146        assert_eq!(InteractionMode::from_u8(1), InteractionMode::Plan);
147        assert_eq!(InteractionMode::from_u8(42), InteractionMode::Agent);
148    }
149
150    #[test]
151    fn from_meta_str_plan_variants() {
152        for s in ["plan", "Plan", "PLAN", "readonly", "read-only"] {
153            assert_eq!(
154                InteractionMode::from_meta_str(s),
155                Some(InteractionMode::Plan),
156                "failed for: {s}"
157            );
158        }
159    }
160
161    #[test]
162    fn from_meta_str_agent_variants() {
163        for s in ["agent", "Agent", "normal", "edit", "act"] {
164            assert_eq!(
165                InteractionMode::from_meta_str(s),
166                Some(InteractionMode::Agent),
167                "failed for: {s}"
168            );
169        }
170    }
171
172    #[test]
173    fn from_meta_str_unknown_returns_none() {
174        assert_eq!(InteractionMode::from_meta_str("debug"), None);
175        assert_eq!(InteractionMode::from_meta_str(""), None);
176    }
177
178    #[test]
179    fn default_mode_is_agent() {
180        let mode = std::sync::atomic::AtomicU8::new(InteractionMode::Agent as u8);
181        assert_eq!(
182            InteractionMode::from_u8(mode.load(std::sync::atomic::Ordering::Relaxed)),
183            InteractionMode::Agent,
184        );
185    }
186}