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