1use crate::error::IntentError;
11use crate::events::EventManager;
12use crate::plan::{PlanExecutor, PlanRequest};
13use crate::project::ProjectContext;
14use crate::report::ReportManager;
15use crate::tasks::TaskManager;
16use crate::workspace::WorkspaceManager;
17use serde::{Deserialize, Serialize};
18use serde_json::{json, Value};
19use std::io;
20
21#[derive(Debug, Deserialize)]
22struct JsonRpcRequest {
23 jsonrpc: String,
24 id: Option<Value>,
25 method: String,
26 params: Option<Value>,
27}
28
29#[derive(Debug, Serialize)]
30struct JsonRpcResponse {
31 jsonrpc: String,
32 id: Option<Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 result: Option<Value>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 error: Option<JsonRpcError>,
37}
38
39#[derive(Debug, Serialize)]
40struct JsonRpcError {
41 code: i32,
42 message: String,
43}
44
45#[derive(Debug, Deserialize)]
46struct ToolCallParams {
47 name: String,
48 arguments: Value,
49}
50
51const MCP_TOOLS: &str = include_str!("../../mcp-server.json");
53
54pub async fn run() -> io::Result<()> {
57 let ctx = match ProjectContext::load().await {
60 Ok(ctx) => ctx,
61 Err(IntentError::NotAProject) => {
62 return Err(io::Error::other(
65 "MCP server must be run within an intent-engine project directory. Run 'ie workspace init' to create a project, or cd to an existing project.".to_string(),
66 ));
67 },
68 Err(e) => {
69 return Err(io::Error::other(format!(
70 "Failed to load project context: {}",
71 e
72 )));
73 },
74 };
75
76 let skip_dashboard = std::env::var("INTENT_ENGINE_NO_DASHBOARD_AUTOSTART").is_ok();
80
81 let normalized_path = ctx.root.canonicalize().unwrap_or_else(|_| ctx.root.clone());
83 let temp_dir = std::env::temp_dir()
85 .canonicalize()
86 .unwrap_or_else(|_| std::env::temp_dir());
87 let is_temp_path = normalized_path.starts_with(&temp_dir);
88
89 if !skip_dashboard && !is_temp_path && !is_dashboard_running().await {
90 tokio::spawn(async {
92 let _ = start_dashboard_background().await;
93 });
95 }
96
97 let project_root = ctx.root.clone();
99 tokio::task::spawn_blocking(move || {
100 let _ = register_mcp_connection(&project_root);
101 });
103
104 if !skip_dashboard {
107 let ws_root = ctx.root.clone();
108 let ws_db_path = ctx.db_path.clone();
109 tokio::spawn(async move {
110 if let Err(e) = crate::mcp::ws_client::connect_to_dashboard(
111 ws_root,
112 ws_db_path,
113 Some("mcp-client".to_string()),
114 )
115 .await
116 {
117 tracing::debug!("Failed to connect to Dashboard WebSocket: {}", e);
118 }
120 });
121 }
122
123 let project_path = ctx.root.clone();
125 let heartbeat_handle = tokio::spawn(async move {
126 heartbeat_task(project_path).await;
127 });
128
129 let result = run_server().await;
131
132 let _ = unregister_mcp_connection(&ctx.root);
134 heartbeat_handle.abort();
138
139 result
140}
141
142async fn run_server() -> io::Result<()> {
143 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
144
145 let stdin = tokio::io::stdin();
146 let mut stdout = tokio::io::stdout();
147 let reader = BufReader::new(stdin);
148 let mut lines = reader.lines();
149
150 while let Some(line) = lines.next_line().await? {
151 if line.trim().is_empty() {
152 continue;
153 }
154
155 let response = match serde_json::from_str::<JsonRpcRequest>(&line) {
156 Ok(request) => {
157 if request.id.is_none() {
159 handle_notification(&request).await;
160 continue; }
162 handle_request(request).await
163 },
164 Err(e) => JsonRpcResponse {
165 jsonrpc: "2.0".to_string(),
166 id: None,
167 result: None,
168 error: Some(JsonRpcError {
169 code: -32700,
170 message: format!("Parse error: {}", e),
171 }),
172 },
173 };
174
175 let response_json = serde_json::to_string(&response)?;
176 stdout.write_all(response_json.as_bytes()).await?;
177 stdout.write_all(b"\n").await?;
178 stdout.flush().await?;
179 }
180
181 Ok(())
182}
183
184async fn handle_notification(request: &JsonRpcRequest) {
185 match request.method.as_str() {
188 "initialized" | "notifications/cancelled" => {
189 },
191 _ => {
192 },
194 }
195}
196
197async fn handle_request(request: JsonRpcRequest) -> JsonRpcResponse {
198 if request.jsonrpc != "2.0" {
200 return JsonRpcResponse {
201 jsonrpc: "2.0".to_string(),
202 id: request.id,
203 result: None,
204 error: Some(JsonRpcError {
205 code: -32600,
206 message: format!("Invalid JSON-RPC version: {}", request.jsonrpc),
207 }),
208 };
209 }
210
211 let result = match request.method.as_str() {
212 "initialize" => handle_initialize(request.params),
213 "ping" => Ok(json!({})), "tools/list" => handle_tools_list(),
215 "tools/call" => handle_tool_call(request.params).await,
216 _ => Err(format!("Method not found: {}", request.method)),
217 };
218
219 match result {
220 Ok(value) => JsonRpcResponse {
221 jsonrpc: "2.0".to_string(),
222 id: request.id,
223 result: Some(value),
224 error: None,
225 },
226 Err(message) => JsonRpcResponse {
227 jsonrpc: "2.0".to_string(),
228 id: request.id,
229 result: None,
230 error: Some(JsonRpcError {
231 code: -32000,
232 message,
233 }),
234 },
235 }
236}
237
238fn handle_initialize(_params: Option<Value>) -> Result<Value, String> {
239 Ok(json!({
242 "protocolVersion": "2024-11-05",
243 "capabilities": {
244 "tools": {
245 "listChanged": false }
247 },
248 "serverInfo": {
249 "name": "intent-engine",
250 "version": env!("CARGO_PKG_VERSION")
251 }
252 }))
253}
254
255fn handle_tools_list() -> Result<Value, String> {
256 let config: Value = serde_json::from_str(MCP_TOOLS)
257 .map_err(|e| format!("Failed to parse MCP tools schema: {}", e))?;
258
259 Ok(json!({
260 "tools": config.get("tools").unwrap_or(&json!([]))
261 }))
262}
263
264async fn handle_tool_call(params: Option<Value>) -> Result<Value, String> {
265 let params: ToolCallParams = serde_json::from_value(params.unwrap_or(json!({})))
266 .map_err(|e| format!("Invalid tool call parameters: {}", e))?;
267
268 let result = match params.name.as_str() {
269 "task_add" => handle_task_add(params.arguments).await,
270 "task_add_dependency" => handle_task_add_dependency(params.arguments).await,
271 "task_start" => handle_task_start(params.arguments).await,
272 "task_pick_next" => handle_task_pick_next(params.arguments).await,
273 "task_spawn_subtask" => handle_task_spawn_subtask(params.arguments).await,
274 "task_done" => handle_task_done(params.arguments).await,
275 "task_update" => handle_task_update(params.arguments).await,
276 "task_list" => handle_task_list(params.arguments).await,
277 "task_get" => handle_task_get(params.arguments).await,
278 "task_context" => handle_task_context(params.arguments).await,
279 "task_delete" => handle_task_delete(params.arguments).await,
280 "event_add" => handle_event_add(params.arguments).await,
281 "event_list" => handle_event_list(params.arguments).await,
282 "search" => handle_unified_search(params.arguments).await,
283 "current_task_get" => handle_current_task_get(params.arguments).await,
284 "report_generate" => handle_report_generate(params.arguments).await,
285 "plan" => handle_plan(params.arguments).await,
286 _ => Err(format!("Unknown tool: {}", params.name)),
287 }?;
288
289 Ok(json!({
290 "content": [{
291 "type": "text",
292 "text": serde_json::to_string_pretty(&result)
293 .unwrap_or_else(|_| "{}".to_string())
294 }]
295 }))
296}
297
298async fn handle_task_add(args: Value) -> Result<Value, String> {
301 let name = match args.get("name") {
303 None => return Err("Missing required parameter: name".to_string()),
304 Some(value) => {
305 if value.is_null() {
306 return Err("Parameter 'name' cannot be null".to_string());
307 }
308 match value.as_str() {
309 Some(s) if s.trim().is_empty() => {
310 return Err("Parameter 'name' cannot be empty".to_string());
311 },
312 Some(s) => s,
313 None => return Err(format!("Parameter 'name' must be a string, got: {}", value)),
314 }
315 },
316 };
317
318 let spec = args.get("spec").and_then(|v| v.as_str());
319 let parent_id = args.get("parent_id").and_then(|v| v.as_i64());
320
321 let ctx = ProjectContext::load_or_init()
322 .await
323 .map_err(|e| format!("Failed to load project context: {}", e))?;
324
325 let task_mgr = TaskManager::new(&ctx.pool);
326 let task = task_mgr
327 .add_task(name, spec, parent_id)
328 .await
329 .map_err(|e| format!("Failed to add task: {}", e))?;
330
331 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
332}
333
334async fn handle_task_add_dependency(args: Value) -> Result<Value, String> {
335 let blocked_task_id = args
336 .get("blocked_task_id")
337 .and_then(|v| v.as_i64())
338 .ok_or("Missing required parameter: blocked_task_id")?;
339
340 let blocking_task_id = args
341 .get("blocking_task_id")
342 .and_then(|v| v.as_i64())
343 .ok_or("Missing required parameter: blocking_task_id")?;
344
345 let ctx = ProjectContext::load_or_init()
346 .await
347 .map_err(|e| format!("Failed to load project context: {}", e))?;
348
349 let dependency =
350 crate::dependencies::add_dependency(&ctx.pool, blocking_task_id, blocked_task_id)
351 .await
352 .map_err(|e| format!("Failed to add dependency: {}", e))?;
353
354 serde_json::to_value(&dependency).map_err(|e| format!("Serialization error: {}", e))
355}
356
357async fn handle_task_start(args: Value) -> Result<Value, String> {
358 let task_id = args
359 .get("task_id")
360 .and_then(|v| v.as_i64())
361 .ok_or("Missing required parameter: task_id")?;
362
363 let with_events = args
364 .get("with_events")
365 .and_then(|v| v.as_bool())
366 .unwrap_or(true);
367
368 let ctx = ProjectContext::load_or_init()
369 .await
370 .map_err(|e| format!("Failed to load project context: {}", e))?;
371
372 let task_mgr = TaskManager::new(&ctx.pool);
373 let task = task_mgr
374 .start_task(task_id, with_events)
375 .await
376 .map_err(|e| format!("Failed to start task: {}", e))?;
377
378 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
379}
380
381async fn handle_task_pick_next(args: Value) -> Result<Value, String> {
382 let _max_count = args.get("max_count").and_then(|v| v.as_i64());
383 let _capacity = args.get("capacity").and_then(|v| v.as_i64());
384
385 let ctx = ProjectContext::load_or_init()
386 .await
387 .map_err(|e| format!("Failed to load project context: {}", e))?;
388
389 let task_mgr = TaskManager::new(&ctx.pool);
390 let response = task_mgr
391 .pick_next()
392 .await
393 .map_err(|e| format!("Failed to pick next task: {}", e))?;
394
395 serde_json::to_value(&response).map_err(|e| format!("Serialization error: {}", e))
396}
397
398async fn handle_task_spawn_subtask(args: Value) -> Result<Value, String> {
399 let name = args
400 .get("name")
401 .and_then(|v| v.as_str())
402 .ok_or("Missing required parameter: name")?;
403
404 let spec = args.get("spec").and_then(|v| v.as_str());
405
406 let ctx = ProjectContext::load_or_init()
407 .await
408 .map_err(|e| format!("Failed to load project context: {}", e))?;
409
410 let task_mgr = TaskManager::new(&ctx.pool);
411 let subtask = task_mgr
412 .spawn_subtask(name, spec)
413 .await
414 .map_err(|e| format!("Failed to spawn subtask: {}", e))?;
415
416 serde_json::to_value(&subtask).map_err(|e| format!("Serialization error: {}", e))
417}
418
419async fn handle_task_done(args: Value) -> Result<Value, String> {
420 let task_id = args.get("task_id").and_then(|v| v.as_i64());
421
422 let ctx = ProjectContext::load_or_init()
423 .await
424 .map_err(|e| format!("Failed to load project context: {}", e))?;
425
426 let task_mgr = TaskManager::new(&ctx.pool);
427
428 if let Some(id) = task_id {
430 let workspace_mgr = WorkspaceManager::new(&ctx.pool);
431 workspace_mgr
432 .set_current_task(id)
433 .await
434 .map_err(|e| format!("Failed to set current task: {}", e))?;
435 }
436
437 let task = task_mgr
438 .done_task()
439 .await
440 .map_err(|e| format!("Failed to mark task as done: {}", e))?;
441
442 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
443}
444
445async fn handle_task_update(args: Value) -> Result<Value, String> {
446 let task_id = args
447 .get("task_id")
448 .and_then(|v| v.as_i64())
449 .ok_or("Missing required parameter: task_id")?;
450
451 let name = args.get("name").and_then(|v| v.as_str());
452 let spec = args.get("spec").and_then(|v| v.as_str());
453 let status = args.get("status").and_then(|v| v.as_str());
454 let complexity = args
455 .get("complexity")
456 .and_then(|v| v.as_i64())
457 .map(|v| v as i32);
458 let priority = match args.get("priority").and_then(|v| v.as_str()) {
459 Some(p) => Some(
460 crate::priority::PriorityLevel::parse_to_int(p)
461 .map_err(|e| format!("Invalid priority: {}", e))?,
462 ),
463 None => None,
464 };
465 let parent_id = args.get("parent_id").and_then(|v| v.as_i64()).map(Some);
466
467 let ctx = ProjectContext::load_or_init()
468 .await
469 .map_err(|e| format!("Failed to load project context: {}", e))?;
470
471 let task_mgr = TaskManager::new(&ctx.pool);
472 let task = task_mgr
473 .update_task(task_id, name, spec, parent_id, status, complexity, priority)
474 .await
475 .map_err(|e| format!("Failed to update task: {}", e))?;
476
477 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
478}
479
480async fn handle_task_list(args: Value) -> Result<Value, String> {
481 let status = args.get("status").and_then(|v| v.as_str());
482 let parent = args.get("parent").and_then(|v| v.as_str());
483
484 let parent_opt = parent.map(|p| {
485 if p == "null" {
486 None
487 } else {
488 p.parse::<i64>().ok()
489 }
490 });
491
492 let ctx = ProjectContext::load()
493 .await
494 .map_err(|e| format!("Failed to load project context: {}", e))?;
495
496 let task_mgr = TaskManager::new(&ctx.pool);
497 let tasks = task_mgr
498 .find_tasks(status, parent_opt)
499 .await
500 .map_err(|e| format!("Failed to list tasks: {}", e))?;
501
502 serde_json::to_value(&tasks).map_err(|e| format!("Serialization error: {}", e))
503}
504
505async fn handle_task_get(args: Value) -> Result<Value, String> {
506 let task_id = args
507 .get("task_id")
508 .and_then(|v| v.as_i64())
509 .ok_or("Missing required parameter: task_id")?;
510
511 let with_events = args
512 .get("with_events")
513 .and_then(|v| v.as_bool())
514 .unwrap_or(false);
515
516 let ctx = ProjectContext::load()
517 .await
518 .map_err(|e| format!("Failed to load project context: {}", e))?;
519
520 let task_mgr = TaskManager::new(&ctx.pool);
521
522 if with_events {
523 let task = task_mgr
524 .get_task_with_events(task_id)
525 .await
526 .map_err(|e| format!("Failed to get task: {}", e))?;
527 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
528 } else {
529 let task = task_mgr
530 .get_task(task_id)
531 .await
532 .map_err(|e| format!("Failed to get task: {}", e))?;
533 serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
534 }
535}
536
537async fn handle_task_context(args: Value) -> Result<Value, String> {
538 let task_id = if let Some(id) = args.get("task_id").and_then(|v| v.as_i64()) {
540 id
541 } else {
542 let ctx = ProjectContext::load()
544 .await
545 .map_err(|e| format!("Failed to load project context: {}", e))?;
546
547 let current_task_id: Option<String> =
548 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
549 .fetch_optional(&ctx.pool)
550 .await
551 .map_err(|e| format!("Database error: {}", e))?;
552
553 current_task_id
554 .and_then(|s| s.parse::<i64>().ok())
555 .ok_or_else(|| {
556 "No current task is set and task_id was not provided. \
557 Use task_start to set a task first, or provide task_id parameter."
558 .to_string()
559 })?
560 };
561
562 let ctx = ProjectContext::load()
563 .await
564 .map_err(|e| format!("Failed to load project context: {}", e))?;
565
566 let task_mgr = TaskManager::new(&ctx.pool);
567 let context = task_mgr
568 .get_task_context(task_id)
569 .await
570 .map_err(|e| format!("Failed to get task context: {}", e))?;
571
572 serde_json::to_value(&context).map_err(|e| format!("Serialization error: {}", e))
573}
574
575async fn handle_task_delete(args: Value) -> Result<Value, String> {
576 let task_id = args
577 .get("task_id")
578 .and_then(|v| v.as_i64())
579 .ok_or("Missing required parameter: task_id")?;
580
581 let ctx = ProjectContext::load()
582 .await
583 .map_err(|e| format!("Failed to load project context: {}", e))?;
584
585 let task_mgr = TaskManager::new(&ctx.pool);
586 task_mgr
587 .delete_task(task_id)
588 .await
589 .map_err(|e| format!("Failed to delete task: {}", e))?;
590
591 Ok(json!({"success": true, "deleted_task_id": task_id}))
592}
593
594async fn handle_event_add(args: Value) -> Result<Value, String> {
595 let task_id = args.get("task_id").and_then(|v| v.as_i64());
596
597 let event_type = args
598 .get("event_type")
599 .and_then(|v| v.as_str())
600 .ok_or("Missing required parameter: event_type")?;
601
602 let data = args
603 .get("data")
604 .and_then(|v| v.as_str())
605 .ok_or("Missing required parameter: data")?;
606
607 let ctx = ProjectContext::load_or_init()
608 .await
609 .map_err(|e| format!("Failed to load project context: {}", e))?;
610
611 let target_task_id = if let Some(id) = task_id {
613 id
614 } else {
615 let current_task_id: Option<String> =
617 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
618 .fetch_optional(&ctx.pool)
619 .await
620 .map_err(|e| format!("Database error: {}", e))?;
621
622 current_task_id
623 .and_then(|s| s.parse::<i64>().ok())
624 .ok_or_else(|| {
625 "No current task is set and task_id was not provided. \
626 Use task_start to set a task first."
627 .to_string()
628 })?
629 };
630
631 let event_mgr = EventManager::new(&ctx.pool);
632 let event = event_mgr
633 .add_event(target_task_id, event_type, data)
634 .await
635 .map_err(|e| format!("Failed to add event: {}", e))?;
636
637 serde_json::to_value(&event).map_err(|e| format!("Serialization error: {}", e))
638}
639
640async fn handle_event_list(args: Value) -> Result<Value, String> {
641 let task_id = args.get("task_id").and_then(|v| v.as_i64());
642
643 let limit = args.get("limit").and_then(|v| v.as_i64());
644 let log_type = args
645 .get("type")
646 .and_then(|v| v.as_str())
647 .map(|s| s.to_string());
648 let since = args
649 .get("since")
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string());
652
653 let ctx = ProjectContext::load()
654 .await
655 .map_err(|e| format!("Failed to load project context: {}", e))?;
656
657 let event_mgr = EventManager::new(&ctx.pool);
658 let events = event_mgr
659 .list_events(task_id, limit, log_type, since)
660 .await
661 .map_err(|e| format!("Failed to list events: {}", e))?;
662
663 serde_json::to_value(&events).map_err(|e| format!("Serialization error: {}", e))
664}
665
666async fn handle_unified_search(args: Value) -> Result<Value, String> {
667 use crate::search::SearchManager;
668
669 let query = args
670 .get("query")
671 .and_then(|v| v.as_str())
672 .ok_or("Missing required parameter: query")?;
673
674 let include_tasks = args
675 .get("include_tasks")
676 .and_then(|v| v.as_bool())
677 .unwrap_or(true);
678
679 let include_events = args
680 .get("include_events")
681 .and_then(|v| v.as_bool())
682 .unwrap_or(true);
683
684 let limit = args.get("limit").and_then(|v| v.as_i64());
685
686 let ctx = ProjectContext::load()
687 .await
688 .map_err(|e| format!("Failed to load project context: {}", e))?;
689
690 let search_mgr = SearchManager::new(&ctx.pool);
691 let results = search_mgr
692 .unified_search(query, include_tasks, include_events, limit)
693 .await
694 .map_err(|e| format!("Failed to perform unified search: {}", e))?;
695
696 serde_json::to_value(&results).map_err(|e| format!("Serialization error: {}", e))
697}
698
699async fn handle_current_task_get(_args: Value) -> Result<Value, String> {
700 let ctx = ProjectContext::load()
701 .await
702 .map_err(|e| format!("Failed to load project context: {}", e))?;
703
704 let workspace_mgr = WorkspaceManager::new(&ctx.pool);
705 let response = workspace_mgr
706 .get_current_task()
707 .await
708 .map_err(|e| format!("Failed to get current task: {}", e))?;
709
710 serde_json::to_value(&response).map_err(|e| format!("Serialization error: {}", e))
711}
712
713async fn handle_report_generate(args: Value) -> Result<Value, String> {
714 let since = args.get("since").and_then(|v| v.as_str()).map(String::from);
715 let status = args
716 .get("status")
717 .and_then(|v| v.as_str())
718 .map(String::from);
719 let filter_name = args
720 .get("filter_name")
721 .and_then(|v| v.as_str())
722 .map(String::from);
723 let filter_spec = args
724 .get("filter_spec")
725 .and_then(|v| v.as_str())
726 .map(String::from);
727 let summary_only = args
728 .get("summary_only")
729 .and_then(|v| v.as_bool())
730 .unwrap_or(true);
731
732 let ctx = ProjectContext::load()
733 .await
734 .map_err(|e| format!("Failed to load project context: {}", e))?;
735
736 let report_mgr = ReportManager::new(&ctx.pool);
737 let report = report_mgr
738 .generate_report(since, status, filter_name, filter_spec, summary_only)
739 .await
740 .map_err(|e| format!("Failed to generate report: {}", e))?;
741
742 serde_json::to_value(&report).map_err(|e| format!("Serialization error: {}", e))
743}
744
745async fn handle_plan(args: Value) -> Result<Value, String> {
746 let request: PlanRequest =
748 serde_json::from_value(args).map_err(|e| format!("Invalid plan request: {}", e))?;
749
750 let ctx = ProjectContext::load_or_init()
751 .await
752 .map_err(|e| format!("Failed to load project context: {}", e))?;
753
754 let plan_executor = PlanExecutor::new(&ctx.pool);
755 let result = plan_executor
756 .execute(&request)
757 .await
758 .map_err(|e| format!("Failed to execute plan: {}", e))?;
759
760 serde_json::to_value(&result).map_err(|e| format!("Serialization error: {}", e))
761}
762
763fn register_mcp_connection(project_path: &std::path::Path) -> anyhow::Result<()> {
769 use crate::dashboard::registry::ProjectRegistry;
770
771 let mut registry = ProjectRegistry::load()?;
772
773 let agent_name = detect_agent_type();
775
776 let normalized_path = project_path
778 .canonicalize()
779 .unwrap_or_else(|_| project_path.to_path_buf());
780
781 let temp_dir = std::env::temp_dir()
785 .canonicalize()
786 .unwrap_or_else(|_| std::env::temp_dir());
787 if normalized_path.starts_with(&temp_dir) {
788 tracing::debug!(
789 "Skipping MCP registry registration for temporary path: {}",
790 normalized_path.display()
791 );
792 return Ok(()); }
794
795 registry.register_mcp_connection(&normalized_path, agent_name)?;
797
798 Ok(())
801}
802
803fn unregister_mcp_connection(project_path: &std::path::Path) -> anyhow::Result<()> {
805 use crate::dashboard::registry::ProjectRegistry;
806
807 let mut registry = ProjectRegistry::load()?;
808
809 let normalized_path = project_path
811 .canonicalize()
812 .unwrap_or_else(|_| project_path.to_path_buf());
813
814 registry.unregister_mcp_connection(&normalized_path)?;
815
816 Ok(())
819}
820
821async fn heartbeat_task(project_path: std::path::PathBuf) {
823 use crate::dashboard::registry::ProjectRegistry;
824
825 let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
826
827 loop {
828 interval.tick().await;
829
830 let path = project_path.clone();
832 tokio::task::spawn_blocking(move || {
833 let normalized_path = path.canonicalize().unwrap_or_else(|_| path.clone());
835
836 if let Ok(mut registry) = ProjectRegistry::load() {
837 let _ = registry.update_mcp_heartbeat(&normalized_path);
838 }
840 });
841 }
842}
843
844fn detect_agent_type() -> Option<String> {
846 if std::env::var("CLAUDE_CODE_VERSION").is_ok() {
848 return Some("claude-code".to_string());
849 }
850
851 if std::env::var("CLAUDE_DESKTOP").is_ok() {
853 return Some("claude-desktop".to_string());
854 }
855
856 Some("mcp-client".to_string())
858}
859
860async fn is_dashboard_running() -> bool {
862 match tokio::time::timeout(
864 std::time::Duration::from_millis(100), tokio::net::TcpStream::connect("127.0.0.1:11391"),
866 )
867 .await
868 {
869 Ok(Ok(_)) => true,
870 Ok(Err(_)) => false,
871 Err(_) => {
872 false
874 },
875 }
876}
877
878async fn start_dashboard_background() -> io::Result<()> {
880 use tokio::process::Command;
881
882 let current_exe = std::env::current_exe()?;
884
885 let mut child = Command::new(current_exe)
888 .arg("dashboard")
889 .arg("start")
890 .arg("--foreground")
891 .stdin(std::process::Stdio::null())
892 .stdout(std::process::Stdio::null())
893 .stderr(std::process::Stdio::null())
894 .kill_on_drop(false) .spawn()?;
896
897 for _ in 0..10 {
899 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
900 if is_dashboard_running().await {
901 tokio::spawn(async move {
904 let _ = child.wait().await;
905 });
906 return Ok(());
907 }
908 }
909
910 Err(io::Error::other(
911 "Dashboard failed to start within 5 seconds",
912 ))
913}
914
915#[cfg(test)]
916#[path = "server_tests.rs"]
917mod tests;