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