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