lean_ctx_server/tools/registered/
ctx_control.rs1use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
7use lean_ctx::tool_defs::tool_def;
8
9pub struct CtxControlTool;
10
11impl McpTool for CtxControlTool {
12 fn name(&self) -> &'static str {
13 "ctx_control"
14 }
15
16 fn tool_def(&self) -> Tool {
17 tool_def(
18 "ctx_control",
19 "Fine-tune context — exclude, include, pin, unpin, evict, set_view, set_priority, mark_outdated, reset, list, history.\n\
20 Overlay-based, reversible, scoped to call/session/project.\n\
21 WORKFLOW: after ctx_compose, exclude low-relevance files.\n\
22 ANTIPATTERN: not for initial context building — use ctx_compose/ctx_read first.",
23 json!({
24 "type": "object",
25 "properties": {
26 "action": {
27 "type": "string",
28 "description": "exclude|include|pin|unpin|evict|set_view|set_priority|mark_outdated|reset|list|history"
29 },
30 "target": { "type": "string", "description": "@F1 handle, file path or item ID" },
31 "targets": { "type": "string", "description": "evict: comma-separated file paths" },
32 "value": { "type": "string", "description": "New content, view name, or priority" },
33 "scope": { "type": "string", "description": "call (this turn only), session (rest of this session), project (persists)" },
34 "reason": { "type": "string", "description": "Reason for the action" }
35 },
36 "required": ["action"]
37 }),
38 )
39 }
40
41 fn handle(
42 &self,
43 args: &Map<String, Value>,
44 ctx: &ToolContext,
45 ) -> Result<ToolOutput, ErrorData> {
46 let root = if let Some(ref session_lock) = ctx.session {
47 crate::server::bounded_lock::read(session_lock, "ctx_control:session")
48 .as_ref()
49 .and_then(|s| s.project_root.clone())
50 .unwrap_or_else(|| ctx.project_root.clone())
51 } else {
52 ctx.project_root.clone()
53 };
54
55 let mut overlays = lean_ctx::core::context_overlay::OverlayStore::load_project(
56 &std::path::PathBuf::from(&root),
57 );
58
59 let result = if let Some(ref ledger_lock) = ctx.ledger {
60 let Some(mut ledger) =
61 crate::server::bounded_lock::write(ledger_lock, "ctx_control:ledger")
62 else {
63 return Ok(ToolOutput::simple(
64 "[control unavailable — ledger busy, retry]".to_string(),
65 ));
66 };
67 let r = control(args, &mut ledger, &mut overlays, &root);
68 ledger.save();
69 r
70 } else {
71 let mut ledger = lean_ctx::core::context_ledger::ContextLedger::load();
72 let r = control(args, &mut ledger, &mut overlays, &root);
73 ledger.save();
74 r
75 };
76 let _ = overlays.save_project(&std::path::PathBuf::from(&root));
77
78 Ok(ToolOutput::simple(result))
79 }
80}
81
82fn control(
85 args: &Map<String, Value>,
86 ledger: &mut lean_ctx::core::context_ledger::ContextLedger,
87 overlays: &mut lean_ctx::core::context_overlay::OverlayStore,
88 root: &str,
89) -> String {
90 if args.get("action").and_then(Value::as_str) == Some("evict") {
91 let targets = args
92 .get("targets")
93 .or_else(|| args.get("target"))
94 .and_then(Value::as_str)
95 .unwrap_or_default();
96 return crate::tools::ctx_control::evict(targets, ledger, overlays, root);
97 }
98 crate::tools::ctx_control::handle(Some(args), ledger, overlays)
99}