lean_ctx_server/tools/registered/
ctx_proof.rs1use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_bool, get_str, get_usize};
7use lean_ctx::tool_defs::tool_def;
8
9pub struct CtxProofTool;
10
11impl McpTool for CtxProofTool {
12 fn name(&self) -> &'static str {
13 "ctx_proof"
14 }
15
16 fn tool_def(&self) -> Tool {
17 tool_def(
18 "ctx_proof",
19 "Export machine-readable ContextProofV1 (Verifier, Pipeline, Provenance).\n\
20 WORKFLOW: call after completing a task to generate audit trail.\n\
21 ANTIPATTERN: not for budget analysis — use ctx_radar/ctx_metrics instead.\n\
22 action=export (only valid); format=json|summary|both; write=true|false;\n\
23 max_evidence=max tool receipts (default 50). Writes to .lean-ctx/proofs/.",
24 json!({
25 "type": "object",
26 "properties": {
27 "action": { "type": "string", "description": "export" },
28 "project_root": { "type": "string", "description": "Project root" },
29 "format": { "type": "string", "description": "json|summary|both" },
30 "write": { "type": "boolean", "description": "Write to .lean-ctx/proofs/" },
31 "filename": { "type": "string", "description": "Optional output filename" },
32 "max_evidence": { "type": "integer", "description": "Max tool receipts" },
33 "max_ledger_files": { "type": "integer", "description": "Max ledger files" }
34 },
35 "required": ["action"]
36 }),
37 )
38 }
39
40 fn handle(
41 &self,
42 args: &Map<String, Value>,
43 ctx: &ToolContext,
44 ) -> Result<ToolOutput, ErrorData> {
45 let action = get_str(args, "action")
46 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
47 if action != "export" {
48 return Err(ErrorData::invalid_params(
49 "unsupported action (expected: export)",
50 None,
51 ));
52 }
53
54 let root = if let Some(p) = ctx.resolved_path("project_root") {
55 p.to_string()
56 } else if let Some(err) = ctx.path_error("project_root") {
57 return Err(ErrorData::invalid_params(
58 format!("project_root: {err}"),
59 None,
60 ));
61 } else {
62 ctx.project_root.clone()
63 };
64 let format = get_str(args, "format");
65 let write = get_bool(args, "write").unwrap_or(true);
66 let filename = get_str(args, "filename");
67 let max_evidence = get_usize(args, "max_evidence").map(|v| v.min(100_000));
68 let max_ledger_files = get_usize(args, "max_ledger_files").map(|v| v.min(100_000));
69
70 let session_data = ctx
71 .session
72 .as_ref()
73 .map(|s| tokio::task::block_in_place(|| s.blocking_read()).clone());
74 let pipeline_data = ctx
75 .pipeline_stats
76 .as_ref()
77 .map(|p| tokio::task::block_in_place(|| p.blocking_read()).clone());
78 let ledger_data = ctx
79 .ledger
80 .as_ref()
81 .map(|l| tokio::task::block_in_place(|| l.blocking_read()).clone());
82
83 let sources = lean_ctx::core::context_proof::ProofSources {
84 project_root: Some(root.clone()),
85 session: session_data,
86 pipeline: pipeline_data,
87 ledger: ledger_data,
88 };
89
90 let out = crate::tools::ctx_proof::handle_export(
91 &root,
92 format.as_deref(),
93 write,
94 filename.as_deref(),
95 max_evidence,
96 max_ledger_files,
97 sources,
98 )
99 .map_err(|e| ErrorData::invalid_params(e, None))?;
100
101 Ok(ToolOutput {
102 text: out,
103 original_tokens: 0,
104 saved_tokens: 0,
105 mode: Some(action),
106 path: Some(root),
107 changed: false,
108 shell_outcome: None,
109 content_blocks: None,
110 })
111 }
112}