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