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