lean_ctx/core/context_kernel/
mcp_bridge.rs1use std::sync::{Mutex, MutexGuard, OnceLock};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use super::client_wiring::OptimizationLevel;
7use super::coverage_class::{self, CoverageClass};
8use super::etpao_live::{EtpaoLive, OutcomeMetrics, RequestMetrics};
9use super::identity::{CallerIdentity, CallerRole, IdentityLedger};
10use super::outcome_signal::{self, OutcomeSignal};
11use super::types::ReceiptOutcome;
12
13static MCP_ETPAO: OnceLock<Mutex<EtpaoLive>> = OnceLock::new();
14static MCP_IDENTITY: OnceLock<Mutex<IdentityLedger>> = OnceLock::new();
15
16const MCP_SESSION_ID: &str = "mcp-session";
17
18#[derive(Debug, Clone, Default)]
20pub struct McpClientInfo {
21 pub client_name: String,
23 pub supports_roots: bool,
25 pub supports_sampling: bool,
27 pub tool_count: usize,
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct McpCallData {
34 pub tool_name: String,
36 pub input_tokens: usize,
38 pub output_tokens: usize,
40 pub is_retry: bool,
42 pub call_number: usize,
44}
45
46#[derive(Debug, Clone)]
48pub struct McpKernelResult {
49 pub coverage: CoverageClass,
51 pub coverage_label: &'static str,
53 pub is_addressable: bool,
55 pub optimization_level: OptimizationLevel,
57 pub schema_budget: usize,
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct McpSummary {
64 pub total_calls: usize,
66 pub total_input_tokens: usize,
68 pub total_output_tokens: usize,
70 pub accepted_calls: usize,
72 pub etpao: f64,
74 pub coverage_label: String,
76}
77
78#[must_use]
80pub fn process_mcp_context(info: &McpClientInfo) -> McpKernelResult {
81 let coverage = super::mcp_coverage::detect_mcp_coverage(
82 &info.client_name,
83 info.supports_roots,
84 info.supports_sampling,
85 );
86 let profile = super::mcp_coverage::mcp_client_profile(&info.client_name);
87
88 McpKernelResult {
89 coverage,
90 coverage_label: coverage_class::coverage_label(coverage),
91 is_addressable: coverage_class::is_addressable(coverage),
92 optimization_level: optimization_level(coverage),
93 schema_budget: profile.tool_budget.max_schema_tokens,
94 }
95}
96
97pub fn record_mcp_call(data: &McpCallData) {
99 let call_number = if data.is_retry {
100 data.call_number.max(2)
101 } else {
102 data.call_number
103 };
104 let inferred = outcome_signal::infer_outcome(call_number, data.is_retry, data.output_tokens);
105 let accepted = inferred.outcome == ReceiptOutcome::Accepted;
106
107 lock_etpao().record_request(RequestMetrics {
108 input_tokens: data.input_tokens,
109 output_tokens: data.output_tokens,
110 reasoning_tokens: 0,
111 schema_tokens: 0,
112 cache_write_tokens: 0,
113 retry_count: usize::from(data.is_retry),
114 client_id: MCP_SESSION_ID.to_owned(),
115 coverage_class: CoverageClass::ContextControlled,
116 });
117 lock_etpao().record_outcome(OutcomeMetrics {
118 accepted,
119 quality_score: inferred.confidence,
120 first_pass: inferred.signal == OutcomeSignal::FirstPass,
121 client_id: MCP_SESSION_ID.to_owned(),
122 });
123
124 lock_identity().record(
125 &mcp_identity(),
126 data.input_tokens,
127 data.output_tokens,
128 accepted,
129 );
130}
131
132#[must_use]
134pub fn generate_mcp_receipt(
135 plan_id: &str,
136 tool_name: &str,
137 _input_tokens: usize,
138 output_tokens: usize,
139 cache_hit: bool,
140) -> super::types::ContextReceiptV1 {
141 use std::collections::HashMap;
142
143 use super::types::{ContextReceiptV1, ReceiptOutcome};
144
145 ContextReceiptV1 {
146 receipt_id: format!("mcp-{tool_name}-{}", uuid_v4_short()),
147 plan_id: plan_id.to_owned(),
148 delivered_tokens: output_tokens,
149 cache_hits: usize::from(cache_hit),
150 cache_misses: usize::from(!cache_hit),
151 outcome: ReceiptOutcome::Accepted,
152 quality_signals: vec![],
153 feedback_attribution: HashMap::new(),
154 }
155}
156
157#[must_use]
159pub fn mcp_etpao() -> f64 {
160 lock_etpao().current_etpao()
161}
162
163#[must_use]
165pub fn mcp_summary() -> McpSummary {
166 let etpao = lock_etpao();
167 let etpao_summary = etpao.summary();
168 let total_calls = etpao.request_count();
169 drop(etpao);
170
171 let identity_summary = lock_identity().summary();
172 McpSummary {
173 total_calls,
174 total_input_tokens: identity_summary.total_tokens,
175 total_output_tokens: identity_summary.total_savings,
176 accepted_calls: etpao_summary.accepted_outcomes,
177 etpao: etpao_summary.etpao,
178 coverage_label: coverage_class::coverage_label(CoverageClass::ContextControlled).to_owned(),
179 }
180}
181
182pub fn reset_mcp_state() {
184 *lock_etpao() = EtpaoLive::new();
185 *lock_identity() = IdentityLedger::new();
186}
187
188fn optimization_level(coverage: CoverageClass) -> OptimizationLevel {
189 match coverage {
190 CoverageClass::FullInline => OptimizationLevel::Full,
191 CoverageClass::ContextControlled => OptimizationLevel::Partial,
192 CoverageClass::ObserveOnly => OptimizationLevel::ObserveOnly,
193 CoverageClass::Unmanaged => OptimizationLevel::None,
194 }
195}
196
197fn mcp_identity() -> CallerIdentity {
198 CallerIdentity {
199 user_id: Some(MCP_SESSION_ID.to_owned()),
200 role: CallerRole::Agent,
201 session_id: Some(MCP_SESSION_ID.to_owned()),
202 ..CallerIdentity::default()
203 }
204}
205
206fn uuid_v4_short() -> String {
207 SystemTime::now()
208 .duration_since(UNIX_EPOCH)
209 .unwrap_or_default()
210 .as_nanos()
211 .to_string()
212}
213
214fn lock_etpao() -> MutexGuard<'static, EtpaoLive> {
215 MCP_ETPAO
216 .get_or_init(|| Mutex::new(EtpaoLive::new()))
217 .lock()
218 .unwrap_or_else(std::sync::PoisonError::into_inner)
219}
220
221fn lock_identity() -> MutexGuard<'static, IdentityLedger> {
222 MCP_IDENTITY
223 .get_or_init(|| Mutex::new(IdentityLedger::new()))
224 .lock()
225 .unwrap_or_else(std::sync::PoisonError::into_inner)
226}
227
228#[cfg(test)]
229mod tests {
230 use std::sync::{Mutex, MutexGuard};
231
232 use super::{
233 McpCallData, McpClientInfo, generate_mcp_receipt, mcp_etpao, mcp_summary,
234 process_mcp_context, record_mcp_call, reset_mcp_state,
235 };
236 use crate::core::context_kernel::coverage_class::CoverageClass;
237 use crate::core::context_kernel::types::ReceiptOutcome;
238
239 static TEST_LOCK: Mutex<()> = Mutex::new(());
240
241 fn test_guard() -> MutexGuard<'static, ()> {
242 TEST_LOCK
243 .lock()
244 .unwrap_or_else(std::sync::PoisonError::into_inner)
245 }
246
247 fn call(number: usize, retry: bool) -> McpCallData {
248 McpCallData {
249 tool_name: "ctx_read".to_owned(),
250 input_tokens: 100,
251 output_tokens: 20,
252 is_retry: retry,
253 call_number: number,
254 }
255 }
256
257 #[test]
258 fn process_cursor_context() {
259 let _guard = test_guard();
260 let result = process_mcp_context(&McpClientInfo {
261 client_name: "cursor".to_owned(),
262 ..McpClientInfo::default()
263 });
264 assert_eq!(result.coverage, CoverageClass::FullInline);
265 }
266
267 #[test]
268 fn process_vscode_context() {
269 let _guard = test_guard();
270 let result = process_mcp_context(&McpClientInfo {
271 client_name: "vscode".to_owned(),
272 ..McpClientInfo::default()
273 });
274 assert_eq!(result.coverage, CoverageClass::ContextControlled);
275 }
276
277 #[test]
278 fn process_unknown_context() {
279 let _guard = test_guard();
280 let result = process_mcp_context(&McpClientInfo {
281 client_name: "unknown".to_owned(),
282 ..McpClientInfo::default()
283 });
284 assert_eq!(result.coverage, CoverageClass::ObserveOnly);
285 }
286
287 #[test]
288 fn record_call_updates_etpao() {
289 let _guard = test_guard();
290 reset_mcp_state();
291 record_mcp_call(&call(1, false));
292 assert!(mcp_etpao() > 0.0);
293 }
294
295 #[test]
296 fn record_retry_is_rejected() {
297 let _guard = test_guard();
298 reset_mcp_state();
299 record_mcp_call(&call(2, true));
300 assert_eq!(mcp_summary().accepted_calls, 0);
301 }
302
303 #[test]
304 fn summary_aggregates() {
305 let _guard = test_guard();
306 reset_mcp_state();
307 for number in 1..=5 {
308 record_mcp_call(&call(number, false));
309 }
310 let summary = mcp_summary();
311 assert_eq!(summary.total_calls, 5);
312 assert_eq!(summary.total_input_tokens, 500);
313 assert_eq!(summary.total_output_tokens, 100);
314 }
315
316 #[test]
317 fn reset_clears() {
318 let _guard = test_guard();
319 reset_mcp_state();
320 record_mcp_call(&call(1, false));
321 reset_mcp_state();
322 assert_eq!(mcp_etpao(), 0.0);
323 assert_eq!(mcp_summary().total_calls, 0);
324 }
325
326 #[test]
327 fn test_generate_mcp_receipt_fields() {
328 let receipt = generate_mcp_receipt("plan-42", "ctx_read", 100, 20, false);
329
330 assert_eq!(receipt.plan_id, "plan-42");
331 assert!(receipt.receipt_id.starts_with("mcp-ctx_read-"));
332 assert_eq!(receipt.delivered_tokens, 20);
333 assert_eq!(receipt.outcome, ReceiptOutcome::Accepted);
334 }
335
336 #[test]
337 fn test_receipt_cache_hit_counting() {
338 let receipt = generate_mcp_receipt("plan-42", "ctx_read", 100, 20, true);
339
340 assert_eq!(receipt.cache_hits, 1);
341 assert_eq!(receipt.cache_misses, 0);
342 }
343
344 #[test]
345 fn test_receipt_cache_miss_counting() {
346 let receipt = generate_mcp_receipt("plan-42", "ctx_read", 100, 20, false);
347
348 assert_eq!(receipt.cache_hits, 0);
349 assert_eq!(receipt.cache_misses, 1);
350 }
351}