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