intent_engine/mcp/
server.rs

1//! Intent-Engine MCP Server (Rust Implementation)
2//!
3//! This is a native Rust implementation of the MCP (Model Context Protocol) server
4//! that provides a JSON-RPC 2.0 interface for AI assistants to interact with
5//! intent-engine's task management capabilities.
6//!
7//! Unlike the Python wrapper (mcp-server.py), this implementation directly uses
8//! the Rust library functions, avoiding subprocess overhead and improving performance.
9
10use crate::events::EventManager;
11use crate::project::ProjectContext;
12use crate::report::ReportManager;
13use crate::tasks::TaskManager;
14use crate::workspace::WorkspaceManager;
15use serde::{Deserialize, Serialize};
16use serde_json::{json, Value};
17use std::io::{self, BufRead, Write};
18
19#[derive(Debug, Deserialize)]
20struct JsonRpcRequest {
21    jsonrpc: String,
22    id: Option<Value>,
23    method: String,
24    params: Option<Value>,
25}
26
27#[derive(Debug, Serialize)]
28struct JsonRpcResponse {
29    jsonrpc: String,
30    id: Option<Value>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    result: Option<Value>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    error: Option<JsonRpcError>,
35}
36
37#[derive(Debug, Serialize)]
38struct JsonRpcError {
39    code: i32,
40    message: String,
41}
42
43#[derive(Debug, Deserialize)]
44struct ToolCallParams {
45    name: String,
46    arguments: Value,
47}
48
49/// MCP Tool Schema
50const MCP_TOOLS: &str = include_str!("../../mcp-server.json");
51
52/// Run the MCP server
53/// This is the main entry point for MCP server mode
54pub async fn run() -> io::Result<()> {
55    run_server().await
56}
57
58async fn run_server() -> io::Result<()> {
59    let stdin = io::stdin();
60    let mut stdout = io::stdout();
61    let reader = stdin.lock();
62
63    for line in reader.lines() {
64        let line = line?;
65        if line.trim().is_empty() {
66            continue;
67        }
68
69        let response = match serde_json::from_str::<JsonRpcRequest>(&line) {
70            Ok(request) => {
71                // Handle notifications (no id = no response needed)
72                if request.id.is_none() {
73                    handle_notification(&request).await;
74                    continue; // Skip sending response for notifications
75                }
76                handle_request(request).await
77            },
78            Err(e) => JsonRpcResponse {
79                jsonrpc: "2.0".to_string(),
80                id: None,
81                result: None,
82                error: Some(JsonRpcError {
83                    code: -32700,
84                    message: format!("Parse error: {}", e),
85                }),
86            },
87        };
88
89        let response_json = serde_json::to_string(&response)?;
90        writeln!(stdout, "{}", response_json)?;
91        stdout.flush()?;
92    }
93
94    Ok(())
95}
96
97async fn handle_notification(request: &JsonRpcRequest) {
98    // Handle MCP notifications (no response required)
99    match request.method.as_str() {
100        "initialized" => {
101            eprintln!("✓ MCP client initialized");
102        },
103        "notifications/cancelled" => {
104            eprintln!("⚠ Request cancelled");
105        },
106        _ => {
107            eprintln!("⚠ Unknown notification: {}", request.method);
108        },
109    }
110}
111
112async fn handle_request(request: JsonRpcRequest) -> JsonRpcResponse {
113    // Validate JSON-RPC version
114    if request.jsonrpc != "2.0" {
115        return JsonRpcResponse {
116            jsonrpc: "2.0".to_string(),
117            id: request.id,
118            result: None,
119            error: Some(JsonRpcError {
120                code: -32600,
121                message: format!("Invalid JSON-RPC version: {}", request.jsonrpc),
122            }),
123        };
124    }
125
126    let result = match request.method.as_str() {
127        "initialize" => handle_initialize(request.params),
128        "ping" => Ok(json!({})), // Ping response for connection keep-alive
129        "tools/list" => handle_tools_list(),
130        "tools/call" => handle_tool_call(request.params).await,
131        _ => Err(format!("Method not found: {}", request.method)),
132    };
133
134    match result {
135        Ok(value) => JsonRpcResponse {
136            jsonrpc: "2.0".to_string(),
137            id: request.id,
138            result: Some(value),
139            error: None,
140        },
141        Err(message) => JsonRpcResponse {
142            jsonrpc: "2.0".to_string(),
143            id: request.id,
144            result: None,
145            error: Some(JsonRpcError {
146                code: -32000,
147                message,
148            }),
149        },
150    }
151}
152
153fn handle_initialize(_params: Option<Value>) -> Result<Value, String> {
154    // MCP initialize handshake
155    // Return server capabilities and info per MCP specification
156    Ok(json!({
157        "protocolVersion": "2024-11-05",
158        "capabilities": {
159            "tools": {
160                "listChanged": false  // Static tool list, no dynamic changes
161            }
162        },
163        "serverInfo": {
164            "name": "intent-engine",
165            "version": env!("CARGO_PKG_VERSION")
166        }
167    }))
168}
169
170fn handle_tools_list() -> Result<Value, String> {
171    let config: Value = serde_json::from_str(MCP_TOOLS)
172        .map_err(|e| format!("Failed to parse MCP tools schema: {}", e))?;
173
174    Ok(json!({
175        "tools": config.get("tools").unwrap_or(&json!([]))
176    }))
177}
178
179async fn handle_tool_call(params: Option<Value>) -> Result<Value, String> {
180    let params: ToolCallParams = serde_json::from_value(params.unwrap_or(json!({})))
181        .map_err(|e| format!("Invalid tool call parameters: {}", e))?;
182
183    let result = match params.name.as_str() {
184        "task_add" => handle_task_add(params.arguments).await,
185        "task_add_dependency" => handle_task_add_dependency(params.arguments).await,
186        "task_start" => handle_task_start(params.arguments).await,
187        "task_pick_next" => handle_task_pick_next(params.arguments).await,
188        "task_spawn_subtask" => handle_task_spawn_subtask(params.arguments).await,
189        "task_switch" => handle_task_switch(params.arguments).await,
190        "task_done" => handle_task_done(params.arguments).await,
191        "task_update" => handle_task_update(params.arguments).await,
192        "task_list" => handle_task_list(params.arguments).await,
193        "task_find" => handle_task_find(params.arguments).await,
194        "task_search" => handle_task_search(params.arguments).await,
195        "task_get" => handle_task_get(params.arguments).await,
196        "task_context" => handle_task_context(params.arguments).await,
197        "task_delete" => handle_task_delete(params.arguments).await,
198        "event_add" => handle_event_add(params.arguments).await,
199        "event_list" => handle_event_list(params.arguments).await,
200        "current_task_get" => handle_current_task_get(params.arguments).await,
201        "report_generate" => handle_report_generate(params.arguments).await,
202        _ => Err(format!("Unknown tool: {}", params.name)),
203    }?;
204
205    Ok(json!({
206        "content": [{
207            "type": "text",
208            "text": serde_json::to_string_pretty(&result)
209                .unwrap_or_else(|_| "{}".to_string())
210        }]
211    }))
212}
213
214// Tool Handlers
215
216async fn handle_task_add(args: Value) -> Result<Value, String> {
217    let name = args
218        .get("name")
219        .and_then(|v| v.as_str())
220        .ok_or("Missing required parameter: name")?;
221
222    let spec = args.get("spec").and_then(|v| v.as_str());
223    let parent_id = args.get("parent_id").and_then(|v| v.as_i64());
224
225    let ctx = ProjectContext::load_or_init()
226        .await
227        .map_err(|e| format!("Failed to load project context: {}", e))?;
228
229    let task_mgr = TaskManager::new(&ctx.pool);
230    let task = task_mgr
231        .add_task(name, spec, parent_id)
232        .await
233        .map_err(|e| format!("Failed to add task: {}", e))?;
234
235    serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
236}
237
238async fn handle_task_add_dependency(args: Value) -> Result<Value, String> {
239    let blocked_task_id = args
240        .get("blocked_task_id")
241        .and_then(|v| v.as_i64())
242        .ok_or("Missing required parameter: blocked_task_id")?;
243
244    let blocking_task_id = args
245        .get("blocking_task_id")
246        .and_then(|v| v.as_i64())
247        .ok_or("Missing required parameter: blocking_task_id")?;
248
249    let ctx = ProjectContext::load_or_init()
250        .await
251        .map_err(|e| format!("Failed to load project context: {}", e))?;
252
253    let dependency =
254        crate::dependencies::add_dependency(&ctx.pool, blocking_task_id, blocked_task_id)
255            .await
256            .map_err(|e| format!("Failed to add dependency: {}", e))?;
257
258    serde_json::to_value(&dependency).map_err(|e| format!("Serialization error: {}", e))
259}
260
261async fn handle_task_start(args: Value) -> Result<Value, String> {
262    let task_id = args
263        .get("task_id")
264        .and_then(|v| v.as_i64())
265        .ok_or("Missing required parameter: task_id")?;
266
267    let with_events = args
268        .get("with_events")
269        .and_then(|v| v.as_bool())
270        .unwrap_or(true);
271
272    let ctx = ProjectContext::load_or_init()
273        .await
274        .map_err(|e| format!("Failed to load project context: {}", e))?;
275
276    let task_mgr = TaskManager::new(&ctx.pool);
277    let task = task_mgr
278        .start_task(task_id, with_events)
279        .await
280        .map_err(|e| format!("Failed to start task: {}", e))?;
281
282    serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
283}
284
285async fn handle_task_pick_next(args: Value) -> Result<Value, String> {
286    let _max_count = args.get("max_count").and_then(|v| v.as_i64());
287    let _capacity = args.get("capacity").and_then(|v| v.as_i64());
288
289    let ctx = ProjectContext::load_or_init()
290        .await
291        .map_err(|e| format!("Failed to load project context: {}", e))?;
292
293    let task_mgr = TaskManager::new(&ctx.pool);
294    let response = task_mgr
295        .pick_next()
296        .await
297        .map_err(|e| format!("Failed to pick next task: {}", e))?;
298
299    serde_json::to_value(&response).map_err(|e| format!("Serialization error: {}", e))
300}
301
302async fn handle_task_spawn_subtask(args: Value) -> Result<Value, String> {
303    let name = args
304        .get("name")
305        .and_then(|v| v.as_str())
306        .ok_or("Missing required parameter: name")?;
307
308    let spec = args.get("spec").and_then(|v| v.as_str());
309
310    let ctx = ProjectContext::load_or_init()
311        .await
312        .map_err(|e| format!("Failed to load project context: {}", e))?;
313
314    let task_mgr = TaskManager::new(&ctx.pool);
315    let subtask = task_mgr
316        .spawn_subtask(name, spec)
317        .await
318        .map_err(|e| format!("Failed to spawn subtask: {}", e))?;
319
320    serde_json::to_value(&subtask).map_err(|e| format!("Serialization error: {}", e))
321}
322
323async fn handle_task_switch(args: Value) -> Result<Value, String> {
324    let task_id = args
325        .get("task_id")
326        .and_then(|v| v.as_i64())
327        .ok_or("Missing required parameter: task_id")?;
328
329    let ctx = ProjectContext::load_or_init()
330        .await
331        .map_err(|e| format!("Failed to load project context: {}", e))?;
332
333    let task_mgr = TaskManager::new(&ctx.pool);
334    let task = task_mgr
335        .switch_to_task(task_id)
336        .await
337        .map_err(|e| format!("Failed to switch task: {}", e))?;
338
339    serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
340}
341
342async fn handle_task_done(args: Value) -> Result<Value, String> {
343    let task_id = args.get("task_id").and_then(|v| v.as_i64());
344
345    let ctx = ProjectContext::load_or_init()
346        .await
347        .map_err(|e| format!("Failed to load project context: {}", e))?;
348
349    let task_mgr = TaskManager::new(&ctx.pool);
350
351    // If task_id is provided, set it as current first
352    if let Some(id) = task_id {
353        let workspace_mgr = WorkspaceManager::new(&ctx.pool);
354        workspace_mgr
355            .set_current_task(id)
356            .await
357            .map_err(|e| format!("Failed to set current task: {}", e))?;
358    }
359
360    let task = task_mgr
361        .done_task()
362        .await
363        .map_err(|e| format!("Failed to mark task as done: {}", e))?;
364
365    serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
366}
367
368async fn handle_task_update(args: Value) -> Result<Value, String> {
369    let task_id = args
370        .get("task_id")
371        .and_then(|v| v.as_i64())
372        .ok_or("Missing required parameter: task_id")?;
373
374    let name = args.get("name").and_then(|v| v.as_str());
375    let spec = args.get("spec").and_then(|v| v.as_str());
376    let status = args.get("status").and_then(|v| v.as_str());
377    let complexity = args
378        .get("complexity")
379        .and_then(|v| v.as_i64())
380        .map(|v| v as i32);
381    let priority = match args.get("priority").and_then(|v| v.as_str()) {
382        Some(p) => Some(
383            crate::priority::PriorityLevel::parse_to_int(p)
384                .map_err(|e| format!("Invalid priority: {}", e))?,
385        ),
386        None => None,
387    };
388    let parent_id = args.get("parent_id").and_then(|v| v.as_i64()).map(Some);
389
390    let ctx = ProjectContext::load_or_init()
391        .await
392        .map_err(|e| format!("Failed to load project context: {}", e))?;
393
394    let task_mgr = TaskManager::new(&ctx.pool);
395    let task = task_mgr
396        .update_task(task_id, name, spec, parent_id, status, complexity, priority)
397        .await
398        .map_err(|e| format!("Failed to update task: {}", e))?;
399
400    serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
401}
402
403async fn handle_task_list(args: Value) -> Result<Value, String> {
404    let status = args.get("status").and_then(|v| v.as_str());
405    let parent = args.get("parent").and_then(|v| v.as_str());
406
407    let parent_opt = parent.map(|p| {
408        if p == "null" {
409            None
410        } else {
411            p.parse::<i64>().ok()
412        }
413    });
414
415    let ctx = ProjectContext::load()
416        .await
417        .map_err(|e| format!("Failed to load project context: {}", e))?;
418
419    let task_mgr = TaskManager::new(&ctx.pool);
420    let tasks = task_mgr
421        .find_tasks(status, parent_opt)
422        .await
423        .map_err(|e| format!("Failed to list tasks: {}", e))?;
424
425    serde_json::to_value(&tasks).map_err(|e| format!("Serialization error: {}", e))
426}
427
428async fn handle_task_find(args: Value) -> Result<Value, String> {
429    eprintln!("⚠️  Warning: 'task_find' is deprecated. Please use 'task_list' instead.");
430    let status = args.get("status").and_then(|v| v.as_str());
431    let parent = args.get("parent").and_then(|v| v.as_str());
432
433    let parent_opt = parent.map(|p| {
434        if p == "null" {
435            None
436        } else {
437            p.parse::<i64>().ok()
438        }
439    });
440
441    let ctx = ProjectContext::load()
442        .await
443        .map_err(|e| format!("Failed to load project context: {}", e))?;
444
445    let task_mgr = TaskManager::new(&ctx.pool);
446    let tasks = task_mgr
447        .find_tasks(status, parent_opt)
448        .await
449        .map_err(|e| format!("Failed to find tasks: {}", e))?;
450
451    serde_json::to_value(&tasks).map_err(|e| format!("Serialization error: {}", e))
452}
453
454async fn handle_task_search(args: Value) -> Result<Value, String> {
455    let query = args
456        .get("query")
457        .and_then(|v| v.as_str())
458        .ok_or("Missing required parameter: query")?;
459
460    let ctx = ProjectContext::load()
461        .await
462        .map_err(|e| format!("Failed to load project context: {}", e))?;
463
464    let task_mgr = TaskManager::new(&ctx.pool);
465    let results = task_mgr
466        .search_tasks(query)
467        .await
468        .map_err(|e| format!("Failed to search tasks: {}", e))?;
469
470    serde_json::to_value(&results).map_err(|e| format!("Serialization error: {}", e))
471}
472
473async fn handle_task_get(args: Value) -> Result<Value, String> {
474    let task_id = args
475        .get("task_id")
476        .and_then(|v| v.as_i64())
477        .ok_or("Missing required parameter: task_id")?;
478
479    let with_events = args
480        .get("with_events")
481        .and_then(|v| v.as_bool())
482        .unwrap_or(false);
483
484    let ctx = ProjectContext::load()
485        .await
486        .map_err(|e| format!("Failed to load project context: {}", e))?;
487
488    let task_mgr = TaskManager::new(&ctx.pool);
489
490    if with_events {
491        let task = task_mgr
492            .get_task_with_events(task_id)
493            .await
494            .map_err(|e| format!("Failed to get task: {}", e))?;
495        serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
496    } else {
497        let task = task_mgr
498            .get_task(task_id)
499            .await
500            .map_err(|e| format!("Failed to get task: {}", e))?;
501        serde_json::to_value(&task).map_err(|e| format!("Serialization error: {}", e))
502    }
503}
504
505async fn handle_task_context(args: Value) -> Result<Value, String> {
506    // Get task_id from args, or fall back to current task
507    let task_id = if let Some(id) = args.get("task_id").and_then(|v| v.as_i64()) {
508        id
509    } else {
510        // Fall back to current_task_id if no task_id provided
511        let ctx = ProjectContext::load()
512            .await
513            .map_err(|e| format!("Failed to load project context: {}", e))?;
514
515        let current_task_id: Option<String> =
516            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
517                .fetch_optional(&ctx.pool)
518                .await
519                .map_err(|e| format!("Database error: {}", e))?;
520
521        current_task_id
522            .and_then(|s| s.parse::<i64>().ok())
523            .ok_or_else(|| {
524                "No current task is set and task_id was not provided. \
525                 Use task_start or task_switch to set a task first, or provide task_id parameter."
526                    .to_string()
527            })?
528    };
529
530    let ctx = ProjectContext::load()
531        .await
532        .map_err(|e| format!("Failed to load project context: {}", e))?;
533
534    let task_mgr = TaskManager::new(&ctx.pool);
535    let context = task_mgr
536        .get_task_context(task_id)
537        .await
538        .map_err(|e| format!("Failed to get task context: {}", e))?;
539
540    serde_json::to_value(&context).map_err(|e| format!("Serialization error: {}", e))
541}
542
543async fn handle_task_delete(args: Value) -> Result<Value, String> {
544    let task_id = args
545        .get("task_id")
546        .and_then(|v| v.as_i64())
547        .ok_or("Missing required parameter: task_id")?;
548
549    let ctx = ProjectContext::load()
550        .await
551        .map_err(|e| format!("Failed to load project context: {}", e))?;
552
553    let task_mgr = TaskManager::new(&ctx.pool);
554    task_mgr
555        .delete_task(task_id)
556        .await
557        .map_err(|e| format!("Failed to delete task: {}", e))?;
558
559    Ok(json!({"success": true, "deleted_task_id": task_id}))
560}
561
562async fn handle_event_add(args: Value) -> Result<Value, String> {
563    let task_id = args.get("task_id").and_then(|v| v.as_i64());
564
565    let event_type = args
566        .get("event_type")
567        .and_then(|v| v.as_str())
568        .ok_or("Missing required parameter: event_type")?;
569
570    let data = args
571        .get("data")
572        .and_then(|v| v.as_str())
573        .ok_or("Missing required parameter: data")?;
574
575    let ctx = ProjectContext::load_or_init()
576        .await
577        .map_err(|e| format!("Failed to load project context: {}", e))?;
578
579    // Determine the target task ID
580    let target_task_id = if let Some(id) = task_id {
581        id
582    } else {
583        // Fall back to current_task_id
584        let current_task_id: Option<String> =
585            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
586                .fetch_optional(&ctx.pool)
587                .await
588                .map_err(|e| format!("Database error: {}", e))?;
589
590        current_task_id
591            .and_then(|s| s.parse::<i64>().ok())
592            .ok_or_else(|| {
593                "No current task is set and task_id was not provided. \
594                 Use task_start or task_switch to set a task first."
595                    .to_string()
596            })?
597    };
598
599    let event_mgr = EventManager::new(&ctx.pool);
600    let event = event_mgr
601        .add_event(target_task_id, event_type, data)
602        .await
603        .map_err(|e| format!("Failed to add event: {}", e))?;
604
605    serde_json::to_value(&event).map_err(|e| format!("Serialization error: {}", e))
606}
607
608async fn handle_event_list(args: Value) -> Result<Value, String> {
609    let task_id = args
610        .get("task_id")
611        .and_then(|v| v.as_i64())
612        .ok_or("Missing required parameter: task_id")?;
613
614    let limit = args.get("limit").and_then(|v| v.as_i64());
615    let log_type = args
616        .get("type")
617        .and_then(|v| v.as_str())
618        .map(|s| s.to_string());
619    let since = args
620        .get("since")
621        .and_then(|v| v.as_str())
622        .map(|s| s.to_string());
623
624    let ctx = ProjectContext::load()
625        .await
626        .map_err(|e| format!("Failed to load project context: {}", e))?;
627
628    let event_mgr = EventManager::new(&ctx.pool);
629    let events = event_mgr
630        .list_events(task_id, limit, log_type, since)
631        .await
632        .map_err(|e| format!("Failed to list events: {}", e))?;
633
634    serde_json::to_value(&events).map_err(|e| format!("Serialization error: {}", e))
635}
636
637async fn handle_current_task_get(_args: Value) -> Result<Value, String> {
638    let ctx = ProjectContext::load()
639        .await
640        .map_err(|e| format!("Failed to load project context: {}", e))?;
641
642    let workspace_mgr = WorkspaceManager::new(&ctx.pool);
643    let response = workspace_mgr
644        .get_current_task()
645        .await
646        .map_err(|e| format!("Failed to get current task: {}", e))?;
647
648    serde_json::to_value(&response).map_err(|e| format!("Serialization error: {}", e))
649}
650
651async fn handle_report_generate(args: Value) -> Result<Value, String> {
652    let since = args.get("since").and_then(|v| v.as_str()).map(String::from);
653    let status = args
654        .get("status")
655        .and_then(|v| v.as_str())
656        .map(String::from);
657    let filter_name = args
658        .get("filter_name")
659        .and_then(|v| v.as_str())
660        .map(String::from);
661    let filter_spec = args
662        .get("filter_spec")
663        .and_then(|v| v.as_str())
664        .map(String::from);
665    let summary_only = args
666        .get("summary_only")
667        .and_then(|v| v.as_bool())
668        .unwrap_or(true);
669
670    let ctx = ProjectContext::load()
671        .await
672        .map_err(|e| format!("Failed to load project context: {}", e))?;
673
674    let report_mgr = ReportManager::new(&ctx.pool);
675    let report = report_mgr
676        .generate_report(since, status, filter_name, filter_spec, summary_only)
677        .await
678        .map_err(|e| format!("Failed to generate report: {}", e))?;
679
680    serde_json::to_value(&report).map_err(|e| format!("Serialization error: {}", e))
681}