1use axum::{
19 extract::State,
20 http::StatusCode,
21 response::{
22 sse::{Event, KeepAlive, Sse},
23 IntoResponse,
24 },
25 Json,
26};
27use futures::stream::{self, Stream};
28use serde::{Deserialize, Serialize};
29use serde_json::{json, Value};
30use std::convert::Infallible;
31use std::sync::Arc;
32use tokio::sync::RwLock;
33
34use crate::mcp::{McpConfig, McpContext};
35use crate::mcp::consciousness::ConsciousnessManager;
36
37pub type SharedMcpContext = Arc<RwLock<Option<Arc<McpContext>>>>;
39
40pub fn create_mcp_context() -> SharedMcpContext {
42 Arc::new(RwLock::new(None))
43}
44
45async fn ensure_mcp_context(state: &SharedMcpContext) -> Arc<McpContext> {
47 let read_guard = state.read().await;
48 if let Some(ctx) = read_guard.as_ref() {
49 return ctx.clone();
50 }
51 drop(read_guard);
52
53 let config = McpConfig::default();
55 let consciousness = Arc::new(tokio::sync::Mutex::new(ConsciousnessManager::new_silent()));
56
57 let ctx = Arc::new(McpContext {
58 cache: Arc::new(crate::mcp::cache::AnalysisCache::new(config.cache_ttl)),
59 config: Arc::new(config),
60 permissions: Arc::new(tokio::sync::Mutex::new(crate::mcp::permissions::PermissionCache::new())),
61 sessions: Arc::new(crate::mcp::session::SessionManager::new()),
62 assistant: Arc::new(crate::mcp::assistant::McpAssistant::new()),
63 consciousness,
64 dashboard_bridge: None,
65 });
66
67 let mut write_guard = state.write().await;
68 *write_guard = Some(ctx.clone());
69 ctx
70}
71
72#[derive(Debug, Deserialize)]
77pub struct McpInitializeRequest {
78 #[serde(default)]
79 pub client_info: Option<ClientInfo>,
80}
81
82#[derive(Debug, Deserialize)]
83pub struct ClientInfo {
84 pub name: Option<String>,
85 pub version: Option<String>,
86}
87
88#[derive(Debug, Serialize)]
89pub struct McpInitializeResponse {
90 pub protocol_version: String,
91 pub server_info: ServerInfo,
92 pub capabilities: Capabilities,
93}
94
95#[derive(Debug, Serialize)]
96pub struct ServerInfo {
97 pub name: String,
98 pub version: String,
99 pub description: String,
100}
101
102#[derive(Debug, Serialize)]
103pub struct Capabilities {
104 pub tools: ToolCapabilities,
105 pub resources: ResourceCapabilities,
106 pub prompts: PromptCapabilities,
107}
108
109#[derive(Debug, Serialize)]
110pub struct ToolCapabilities {
111 pub list_changed: bool,
112}
113
114#[derive(Debug, Serialize)]
115pub struct ResourceCapabilities {
116 pub subscribe: bool,
117 pub list_changed: bool,
118}
119
120#[derive(Debug, Serialize)]
121pub struct PromptCapabilities {
122 pub list_changed: bool,
123}
124
125#[derive(Debug, Deserialize)]
126pub struct ToolCallRequest {
127 pub name: String,
128 #[serde(default)]
129 pub arguments: Option<Value>,
130}
131
132pub async fn mcp_initialize(
138 State(state): State<SharedMcpContext>,
139 Json(req): Json<McpInitializeRequest>,
140) -> impl IntoResponse {
141 let _ctx = ensure_mcp_context(&state).await;
142
143 if let Some(client) = &req.client_info {
145 tracing::info!(
146 "MCP client connected: {} v{}",
147 client.name.as_deref().unwrap_or("unknown"),
148 client.version.as_deref().unwrap_or("?")
149 );
150 }
151
152 Json(McpInitializeResponse {
153 protocol_version: "2025-06-18".to_string(),
154 server_info: ServerInfo {
155 name: "smart-tree".to_string(),
156 version: env!("CARGO_PKG_VERSION").to_string(),
157 description: "Smart Tree Daemon - HTTP MCP with The Custodian watching".to_string(),
158 },
159 capabilities: Capabilities {
160 tools: ToolCapabilities { list_changed: false },
161 resources: ResourceCapabilities { subscribe: false, list_changed: false },
162 prompts: PromptCapabilities { list_changed: false },
163 },
164 })
165}
166
167pub async fn mcp_tools_list(
169 State(state): State<SharedMcpContext>,
170) -> impl IntoResponse {
171 let _ctx = ensure_mcp_context(&state).await;
172
173 let tools = crate::mcp::tools_consolidated_enhanced::get_enhanced_consolidated_tools();
175 let welcome = crate::mcp::tools_consolidated_enhanced::get_welcome_message();
176
177 Json(json!({
178 "tools": tools,
179 "_welcome": welcome,
180 "_custodian": "🧹 The Custodian is watching. All operations are monitored for your protection."
181 }))
182}
183
184pub async fn mcp_tools_call(
186 State(state): State<SharedMcpContext>,
187 Json(req): Json<ToolCallRequest>,
188) -> impl IntoResponse {
189 let ctx = ensure_mcp_context(&state).await;
190
191 let custodian_alert = evaluate_operation(&req.name, &req.arguments);
194 if let Some(alert) = &custodian_alert {
195 tracing::warn!("🧹 Custodian Alert: {}", alert);
196 }
197
198 let result = crate::mcp::tools_consolidated_enhanced::dispatch_consolidated_tool(
200 &req.name,
201 req.arguments,
202 ctx,
203 ).await;
204
205 match result {
206 Ok(mut value) => {
207 if let Some(alert) = custodian_alert {
209 if let Some(obj) = value.as_object_mut() {
210 obj.insert("_custodian_alert".to_string(), json!(alert));
211 }
212 }
213 (StatusCode::OK, Json(value))
214 }
215 Err(e) => (
216 StatusCode::INTERNAL_SERVER_ERROR,
217 Json(json!({
218 "error": {
219 "code": -32603,
220 "message": e.to_string()
221 }
222 }))
223 )
224 }
225}
226
227pub async fn mcp_resources_list(
229 State(_state): State<SharedMcpContext>,
230) -> impl IntoResponse {
231 Json(json!({
233 "resources": []
234 }))
235}
236
237pub async fn mcp_prompts_list(
239 State(_state): State<SharedMcpContext>,
240) -> impl IntoResponse {
241 Json(json!({
242 "prompts": [
243 {
244 "name": "project-overview",
245 "description": "Get a comprehensive overview of a project",
246 "arguments": [
247 {
248 "name": "path",
249 "description": "Path to the project",
250 "required": false
251 }
252 ]
253 },
254 {
255 "name": "code-review",
256 "description": "Review code changes in a file or directory",
257 "arguments": [
258 {
259 "name": "path",
260 "description": "Path to review",
261 "required": true
262 }
263 ]
264 }
265 ]
266 }))
267}
268
269fn evaluate_operation(tool_name: &str, args: &Option<Value>) -> Option<String> {
276 if let Some(args) = args {
280 let args_str = args.to_string().to_lowercase();
281
282 if args_str.contains("ipfs") || args_str.contains("ipns")
284 || args_str.contains("dweb.link") || args_str.contains("w3s.link") {
285 return Some(format!(
286 "🧹 Custodian Notice: Operation '{}' references IPFS/IPNS. \
287 Data may be transmitted to external decentralized storage. \
288 Verify this is intentional.",
289 tool_name
290 ));
291 }
292
293 if args_str.contains(".env") || args_str.contains("credentials")
295 || args_str.contains("secret") || args_str.contains(".ssh")
296 || args_str.contains("private_key") {
297 return Some(format!(
298 "🧹 Custodian Notice: Operation '{}' involves potentially sensitive files. \
299 Please verify this access is authorized.",
300 tool_name
301 ));
302 }
303
304 if (tool_name.contains("write") || tool_name.contains("edit"))
306 && (args_str.contains("http://") || args_str.contains("https://")) {
307 return Some(format!(
308 "🧹 Custodian Notice: Write operation '{}' contains external URLs. \
309 Verify the destination is trusted.",
310 tool_name
311 ));
312 }
313 }
314
315 if tool_name == "smart_edit" || tool_name == "write_file" {
317 tracing::debug!("🧹 Custodian: Recording write operation - {}", tool_name);
319 }
320
321 None
322}
323
324pub async fn mcp_sse_handler(
336 State(_state): State<SharedMcpContext>,
337) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
338 let session_id = uuid::Uuid::new_v4().to_string();
340
341 let events = stream::iter(vec![
344 Ok(Event::default()
346 .event("endpoint")
347 .data(format!("http://localhost:28428/mcp/message?session_id={}", session_id))),
348 Ok(Event::default()
350 .event("message")
351 .data(serde_json::to_string(&json!({
352 "jsonrpc": "2.0",
353 "method": "notifications/initialized",
354 "params": {
355 "_custodian": "🧹 The Custodian is watching. Welcome to Smart Tree MCP!",
356 "serverInfo": {
357 "name": "smart-tree",
358 "version": env!("CARGO_PKG_VERSION")
359 }
360 }
361 })).unwrap_or_default())),
362 ]);
363
364 Sse::new(events).keep_alive(KeepAlive::default())
365}
366
367pub async fn mcp_message_handler(
369 State(state): State<SharedMcpContext>,
370 Json(request): Json<Value>,
371) -> impl IntoResponse {
372 let ctx = ensure_mcp_context(&state).await;
373
374 let method = request["method"].as_str().unwrap_or("");
376 let id = request.get("id").cloned();
377 let params = request.get("params").cloned();
378
379 if let Some(name) = request["params"]["name"].as_str() {
381 if let Some(alert) = evaluate_operation(name, ¶ms) {
382 tracing::warn!("🧹 Custodian Alert: {}", alert);
383 }
384 }
385
386 let result = match method {
388 "initialize" => {
389 json!({
390 "protocolVersion": "2024-11-05",
391 "serverInfo": {
392 "name": "smart-tree",
393 "version": env!("CARGO_PKG_VERSION")
394 },
395 "capabilities": {
396 "tools": { "listChanged": true },
397 "resources": { "listChanged": true },
398 "prompts": { "listChanged": true }
399 },
400 "_custodian": "🧹 The Custodian is watching all operations."
401 })
402 }
403 "tools/list" => {
404 let tools = crate::mcp::tools_consolidated_enhanced::get_enhanced_consolidated_tools();
405 json!({ "tools": tools })
406 }
407 "tools/call" => {
408 let tool_name = request["params"]["name"].as_str().unwrap_or("");
409 let arguments = request["params"]["arguments"].clone();
410
411 match crate::mcp::tools_consolidated_enhanced::dispatch_consolidated_tool(
412 tool_name,
413 Some(arguments),
414 ctx,
415 ).await {
416 Ok(result) => result,
417 Err(e) => json!({
418 "isError": true,
419 "content": [{ "type": "text", "text": e.to_string() }]
420 })
421 }
422 }
423 "resources/list" => json!({ "resources": [] }),
424 "prompts/list" => json!({ "prompts": [] }),
425 _ => json!({
426 "error": {
427 "code": -32601,
428 "message": format!("Method not found: {}", method)
429 }
430 })
431 };
432
433 let response = if let Some(id) = id {
435 json!({
436 "jsonrpc": "2.0",
437 "id": id,
438 "result": result
439 })
440 } else {
441 return (StatusCode::NO_CONTENT, Json(json!({})));
443 };
444
445 (StatusCode::OK, Json(response))
446}
447
448use axum::{routing::{get, post}, Router};
453
454pub fn mcp_router(state: SharedMcpContext) -> Router {
456 Router::new()
457 .route("/sse", get(mcp_sse_handler))
459 .route("/message", post(mcp_message_handler))
460 .route("/initialize", post(mcp_initialize))
462 .route("/tools/list", get(mcp_tools_list))
463 .route("/tools/call", post(mcp_tools_call))
464 .route("/resources/list", get(mcp_resources_list))
465 .route("/prompts/list", get(mcp_prompts_list))
466 .with_state(state)
467}