chasm_cli/mcp/
tools.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! MCP Tools - Expose csm functionality as MCP tools
4
5#![allow(dead_code, unused_imports)]
6
7use super::types::*;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::collections::HashMap;
11
12/// Workspace info for JSON serialization (local type to avoid conflict)
13#[derive(Debug, Clone, Serialize, Deserialize)]
14struct McpWorkspaceInfo {
15    hash: String,
16    project_path: String,
17    session_count: usize,
18    has_chats: bool,
19}
20
21/// Get the list of available tools
22pub fn list_tools() -> Vec<Tool> {
23    vec![
24        Tool {
25            name: "csm_list_workspaces".to_string(),
26            description: Some("List all VS Code workspaces with chat sessions".to_string()),
27            input_schema: json!({
28                "type": "object",
29                "properties": {},
30                "required": []
31            }),
32        },
33        Tool {
34            name: "csm_find_workspace".to_string(),
35            description: Some(
36                "Find workspaces matching a pattern (project name or path)".to_string(),
37            ),
38            input_schema: json!({
39                "type": "object",
40                "properties": {
41                    "pattern": {
42                        "type": "string",
43                        "description": "Search pattern to match workspace names or paths"
44                    }
45                },
46                "required": ["pattern"]
47            }),
48        },
49        Tool {
50            name: "csm_list_sessions".to_string(),
51            description: Some(
52                "List all chat sessions, optionally filtered by project path".to_string(),
53            ),
54            input_schema: json!({
55                "type": "object",
56                "properties": {
57                    "project_path": {
58                        "type": "string",
59                        "description": "Optional project path to filter sessions"
60                    }
61                },
62                "required": []
63            }),
64        },
65        Tool {
66            name: "csm_list_orphaned".to_string(),
67            description: Some(
68                "List sessions on disk that are not in VS Code's index (invisible sessions)"
69                    .to_string(),
70            ),
71            input_schema: json!({
72                "type": "object",
73                "properties": {
74                    "path": {
75                        "type": "string",
76                        "description": "Project path (defaults to current directory)"
77                    }
78                },
79                "required": []
80            }),
81        },
82        Tool {
83            name: "csm_register_all".to_string(),
84            description: Some(
85                "Register all sessions from a workspace into VS Code's index".to_string(),
86            ),
87            input_schema: json!({
88                "type": "object",
89                "properties": {
90                    "path": {
91                        "type": "string",
92                        "description": "Project path (defaults to current directory)"
93                    },
94                    "merge": {
95                        "type": "boolean",
96                        "description": "Merge all sessions into one before registering"
97                    },
98                    "force": {
99                        "type": "boolean",
100                        "description": "Force registration even if VS Code is running"
101                    }
102                },
103                "required": []
104            }),
105        },
106        Tool {
107            name: "csm_register_sessions".to_string(),
108            description: Some("Register specific sessions by ID or title".to_string()),
109            input_schema: json!({
110                "type": "object",
111                "properties": {
112                    "ids": {
113                        "type": "array",
114                        "items": { "type": "string" },
115                        "description": "Session IDs to register"
116                    },
117                    "titles": {
118                        "type": "array",
119                        "items": { "type": "string" },
120                        "description": "Session titles to match (partial match)"
121                    },
122                    "path": {
123                        "type": "string",
124                        "description": "Project path (defaults to current directory)"
125                    },
126                    "force": {
127                        "type": "boolean",
128                        "description": "Force registration even if VS Code is running"
129                    }
130                },
131                "required": []
132            }),
133        },
134        Tool {
135            name: "csm_show_session".to_string(),
136            description: Some("Show details of a specific chat session".to_string()),
137            input_schema: json!({
138                "type": "object",
139                "properties": {
140                    "session_id": {
141                        "type": "string",
142                        "description": "Session ID (supports partial prefix match)"
143                    }
144                },
145                "required": ["session_id"]
146            }),
147        },
148        Tool {
149            name: "csm_show_history".to_string(),
150            description: Some("Show chat history timeline for a project".to_string()),
151            input_schema: json!({
152                "type": "object",
153                "properties": {
154                    "path": {
155                        "type": "string",
156                        "description": "Project path (defaults to current directory)"
157                    }
158                },
159                "required": []
160            }),
161        },
162        Tool {
163            name: "csm_merge_sessions".to_string(),
164            description: Some("Merge multiple chat sessions into one unified history".to_string()),
165            input_schema: json!({
166                "type": "object",
167                "properties": {
168                    "path": {
169                        "type": "string",
170                        "description": "Project path to merge sessions from"
171                    },
172                    "title": {
173                        "type": "string",
174                        "description": "Title for the merged session"
175                    },
176                    "force": {
177                        "type": "boolean",
178                        "description": "Force merge even if VS Code is running"
179                    }
180                },
181                "required": []
182            }),
183        },
184        Tool {
185            name: "csm_search".to_string(),
186            description: Some("Full-text search across all harvested chat sessions".to_string()),
187            input_schema: json!({
188                "type": "object",
189                "properties": {
190                    "query": {
191                        "type": "string",
192                        "description": "Search query"
193                    },
194                    "limit": {
195                        "type": "integer",
196                        "description": "Maximum number of results (default: 20)"
197                    }
198                },
199                "required": ["query"]
200            }),
201        },
202        Tool {
203            name: "csm_detect".to_string(),
204            description: Some(
205                "Auto-detect workspace and available providers for a path".to_string(),
206            ),
207            input_schema: json!({
208                "type": "object",
209                "properties": {
210                    "path": {
211                        "type": "string",
212                        "description": "Path to detect workspace for"
213                    }
214                },
215                "required": []
216            }),
217        },
218        // CSM Database Tools (for csm-web integration)
219        Tool {
220            name: "csm_db_list_workspaces".to_string(),
221            description: Some("List all workspaces from the CSM database (csm-web)".to_string()),
222            input_schema: json!({
223                "type": "object",
224                "properties": {},
225                "required": []
226            }),
227        },
228        Tool {
229            name: "csm_db_list_sessions".to_string(),
230            description: Some("List chat sessions from the CSM database (csm-web)".to_string()),
231            input_schema: json!({
232                "type": "object",
233                "properties": {
234                    "workspace_id": {
235                        "type": "string",
236                        "description": "Filter by workspace ID"
237                    },
238                    "provider": {
239                        "type": "string",
240                        "description": "Filter by provider (e.g., 'copilot', 'ollama', 'chatgpt')"
241                    },
242                    "limit": {
243                        "type": "integer",
244                        "description": "Maximum results (default: 100)"
245                    }
246                },
247                "required": []
248            }),
249        },
250        Tool {
251            name: "csm_db_get_session".to_string(),
252            description: Some(
253                "Get a specific session with all its messages from CSM database".to_string(),
254            ),
255            input_schema: json!({
256                "type": "object",
257                "properties": {
258                    "session_id": {
259                        "type": "string",
260                        "description": "Session ID to retrieve"
261                    }
262                },
263                "required": ["session_id"]
264            }),
265        },
266        Tool {
267            name: "csm_db_search".to_string(),
268            description: Some("Search sessions in CSM database by title".to_string()),
269            input_schema: json!({
270                "type": "object",
271                "properties": {
272                    "query": {
273                        "type": "string",
274                        "description": "Search query for session titles"
275                    },
276                    "limit": {
277                        "type": "integer",
278                        "description": "Maximum results (default: 20)"
279                    }
280                },
281                "required": ["query"]
282            }),
283        },
284        Tool {
285            name: "csm_db_stats".to_string(),
286            description: Some(
287                "Get statistics about the CSM database (session counts by provider)".to_string(),
288            ),
289            input_schema: json!({
290                "type": "object",
291                "properties": {},
292                "required": []
293            }),
294        },
295    ]
296}
297
298/// Execute a tool call
299pub fn call_tool(name: &str, arguments: &HashMap<String, serde_json::Value>) -> CallToolResult {
300    let result = match name {
301        "csm_list_workspaces" => execute_list_workspaces(),
302        "csm_find_workspace" => {
303            let pattern = arguments
304                .get("pattern")
305                .and_then(|v| v.as_str())
306                .unwrap_or("");
307            execute_find_workspace(pattern)
308        }
309        "csm_list_sessions" => {
310            let project_path = arguments.get("project_path").and_then(|v| v.as_str());
311            execute_list_sessions(project_path)
312        }
313        "csm_list_orphaned" => {
314            let path = arguments.get("path").and_then(|v| v.as_str());
315            execute_list_orphaned(path)
316        }
317        "csm_register_all" => {
318            let path = arguments.get("path").and_then(|v| v.as_str());
319            let merge = arguments
320                .get("merge")
321                .and_then(|v| v.as_bool())
322                .unwrap_or(false);
323            let force = arguments
324                .get("force")
325                .and_then(|v| v.as_bool())
326                .unwrap_or(false);
327            execute_register_all(path, merge, force)
328        }
329        "csm_register_sessions" => {
330            let ids: Vec<String> = arguments
331                .get("ids")
332                .and_then(|v| v.as_array())
333                .map(|arr| {
334                    arr.iter()
335                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
336                        .collect()
337                })
338                .unwrap_or_default();
339            let titles: Option<Vec<String>> = arguments
340                .get("titles")
341                .and_then(|v| v.as_array())
342                .map(|arr| {
343                    arr.iter()
344                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
345                        .collect()
346                });
347            let path = arguments.get("path").and_then(|v| v.as_str());
348            let force = arguments
349                .get("force")
350                .and_then(|v| v.as_bool())
351                .unwrap_or(false);
352            execute_register_sessions(&ids, titles.as_deref(), path, force)
353        }
354        "csm_show_session" => {
355            let session_id = arguments
356                .get("session_id")
357                .and_then(|v| v.as_str())
358                .unwrap_or("");
359            execute_show_session(session_id)
360        }
361        "csm_show_history" => {
362            let path = arguments.get("path").and_then(|v| v.as_str());
363            execute_show_history(path)
364        }
365        "csm_merge_sessions" => {
366            let path = arguments.get("path").and_then(|v| v.as_str());
367            let title = arguments.get("title").and_then(|v| v.as_str());
368            let force = arguments
369                .get("force")
370                .and_then(|v| v.as_bool())
371                .unwrap_or(false);
372            execute_merge_sessions(path, title, force)
373        }
374        "csm_search" => {
375            let query = arguments
376                .get("query")
377                .and_then(|v| v.as_str())
378                .unwrap_or("");
379            let limit = arguments
380                .get("limit")
381                .and_then(|v| v.as_u64())
382                .map(|n| n as usize);
383            execute_search(query, limit)
384        }
385        "csm_detect" => {
386            let path = arguments.get("path").and_then(|v| v.as_str());
387            execute_detect(path)
388        }
389        // CSM Database tools (csm-web integration)
390        "csm_db_list_workspaces" => execute_db_list_workspaces(),
391        "csm_db_list_sessions" => {
392            let workspace_id = arguments.get("workspace_id").and_then(|v| v.as_str());
393            let provider = arguments.get("provider").and_then(|v| v.as_str());
394            let limit = arguments
395                .get("limit")
396                .and_then(|v| v.as_u64())
397                .map(|n| n as usize)
398                .unwrap_or(100);
399            execute_db_list_sessions(workspace_id, provider, limit)
400        }
401        "csm_db_get_session" => {
402            let session_id = arguments
403                .get("session_id")
404                .and_then(|v| v.as_str())
405                .unwrap_or("");
406            execute_db_get_session(session_id)
407        }
408        "csm_db_search" => {
409            let query = arguments
410                .get("query")
411                .and_then(|v| v.as_str())
412                .unwrap_or("");
413            let limit = arguments
414                .get("limit")
415                .and_then(|v| v.as_u64())
416                .map(|n| n as usize)
417                .unwrap_or(20);
418            execute_db_search(query, limit)
419        }
420        "csm_db_stats" => execute_db_stats(),
421        _ => CallToolResult {
422            content: vec![ToolContent::Text {
423                text: format!("Unknown tool: {}", name),
424            }],
425            is_error: Some(true),
426        },
427    };
428
429    result
430}
431
432// ============================================================================
433// Tool Implementations
434// ============================================================================
435
436fn execute_list_workspaces() -> CallToolResult {
437    use crate::workspace::discover_workspaces;
438
439    match discover_workspaces() {
440        Ok(workspaces) => {
441            let infos: Vec<McpWorkspaceInfo> = workspaces
442                .iter()
443                .map(|ws| McpWorkspaceInfo {
444                    hash: ws.hash.clone(),
445                    project_path: ws.project_path.clone().unwrap_or_default(),
446                    session_count: ws.chat_session_count,
447                    has_chats: ws.has_chat_sessions,
448                })
449                .collect();
450
451            CallToolResult {
452                content: vec![ToolContent::Text {
453                    text: serde_json::to_string_pretty(&json!({
454                        "workspaces": infos,
455                        "total": infos.len()
456                    }))
457                    .unwrap_or_default(),
458                }],
459                is_error: None,
460            }
461        }
462        Err(e) => CallToolResult {
463            content: vec![ToolContent::Text {
464                text: format!("Error listing workspaces: {}", e),
465            }],
466            is_error: Some(true),
467        },
468    }
469}
470
471fn execute_find_workspace(pattern: &str) -> CallToolResult {
472    use crate::workspace::{find_all_workspaces_for_project, get_chat_sessions_from_workspace};
473
474    match find_all_workspaces_for_project(pattern) {
475        Ok(workspaces) => {
476            let infos: Vec<serde_json::Value> = workspaces
477                .iter()
478                .map(|(hash, workspace_path, project_path, _last_modified)| {
479                    let sessions = get_chat_sessions_from_workspace(workspace_path)
480                        .unwrap_or_default()
481                        .iter()
482                        .map(|s| {
483                            json!({
484                                "id": s.session.session_id,
485                                "title": s.session.title(),
486                                "path": s.path.display().to_string()
487                            })
488                        })
489                        .collect::<Vec<_>>();
490                    json!({
491                        "hash": hash,
492                        "project_path": project_path,
493                        "workspace_path": workspace_path.display().to_string(),
494                        "session_count": sessions.len(),
495                        "sessions": sessions
496                    })
497                })
498                .collect();
499
500            CallToolResult {
501                content: vec![ToolContent::Text {
502                    text: serde_json::to_string_pretty(&json!({
503                        "pattern": pattern,
504                        "workspaces": infos,
505                        "total": infos.len()
506                    }))
507                    .unwrap_or_default(),
508                }],
509                is_error: None,
510            }
511        }
512        Err(e) => CallToolResult {
513            content: vec![ToolContent::Text {
514                text: format!("Error finding workspaces: {}", e),
515            }],
516            is_error: Some(true),
517        },
518    }
519}
520
521fn execute_list_sessions(project_path: Option<&str>) -> CallToolResult {
522    use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace};
523
524    match discover_workspaces() {
525        Ok(workspaces) => {
526            let mut all_sessions = Vec::new();
527
528            for ws in &workspaces {
529                // Filter by project path if specified
530                if let Some(filter_path) = project_path {
531                    if let Some(ref ws_path) = ws.project_path {
532                        if !ws_path.to_lowercase().contains(&filter_path.to_lowercase()) {
533                            continue;
534                        }
535                    } else {
536                        continue;
537                    }
538                }
539
540                if let Ok(sessions) = get_chat_sessions_from_workspace(&ws.workspace_path) {
541                    for s in sessions {
542                        all_sessions.push(json!({
543                            "id": s.session.session_id,
544                            "title": s.session.title(),
545                            "message_count": s.session.requests.len(),
546                            "workspace_hash": ws.hash,
547                            "project_path": ws.project_path,
548                            "file_path": s.path.display().to_string()
549                        }));
550                    }
551                }
552            }
553
554            CallToolResult {
555                content: vec![ToolContent::Text {
556                    text: serde_json::to_string_pretty(&json!({
557                        "sessions": all_sessions,
558                        "total": all_sessions.len()
559                    }))
560                    .unwrap_or_default(),
561                }],
562                is_error: None,
563            }
564        }
565        Err(e) => CallToolResult {
566            content: vec![ToolContent::Text {
567                text: format!("Error listing sessions: {}", e),
568            }],
569            is_error: Some(true),
570        },
571    }
572}
573
574fn execute_list_orphaned(path: Option<&str>) -> CallToolResult {
575    use crate::commands::list_orphaned;
576
577    // Capture stdout (list_orphaned prints to stdout)
578    // For now, we'll call it and return a simplified response
579    match list_orphaned(path) {
580        Ok(_) => CallToolResult {
581            content: vec![ToolContent::Text {
582                text: json!({
583                    "status": "success",
584                    "message": "Orphaned session check completed. See console output for details."
585                })
586                .to_string(),
587            }],
588            is_error: None,
589        },
590        Err(e) => CallToolResult {
591            content: vec![ToolContent::Text {
592                text: format!("Error listing orphaned sessions: {}", e),
593            }],
594            is_error: Some(true),
595        },
596    }
597}
598
599fn execute_register_all(path: Option<&str>, merge: bool, force: bool) -> CallToolResult {
600    use crate::commands::register_all;
601
602    match register_all(path, merge, force) {
603        Ok(_) => CallToolResult {
604            content: vec![ToolContent::Text {
605                text: json!({
606                    "status": "success",
607                    "message": "Sessions registered successfully",
608                    "merge": merge,
609                    "force": force
610                })
611                .to_string(),
612            }],
613            is_error: None,
614        },
615        Err(e) => CallToolResult {
616            content: vec![ToolContent::Text {
617                text: format!("Error registering sessions: {}", e),
618            }],
619            is_error: Some(true),
620        },
621    }
622}
623
624fn execute_register_sessions(
625    ids: &[String],
626    titles: Option<&[String]>,
627    path: Option<&str>,
628    force: bool,
629) -> CallToolResult {
630    use crate::commands::register_sessions;
631
632    match register_sessions(ids, titles, path, force) {
633        Ok(_) => CallToolResult {
634            content: vec![ToolContent::Text {
635                text: json!({
636                    "status": "success",
637                    "message": "Sessions registered successfully",
638                    "ids": ids,
639                    "titles": titles,
640                    "force": force
641                })
642                .to_string(),
643            }],
644            is_error: None,
645        },
646        Err(e) => CallToolResult {
647            content: vec![ToolContent::Text {
648                text: format!("Error registering sessions: {}", e),
649            }],
650            is_error: Some(true),
651        },
652    }
653}
654
655fn execute_show_session(session_id: &str) -> CallToolResult {
656    use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace};
657
658    match discover_workspaces() {
659        Ok(workspaces) => {
660            for ws in &workspaces {
661                if let Ok(sessions) = get_chat_sessions_from_workspace(&ws.workspace_path) {
662                    for s in sessions {
663                        let sid = s.session.session_id.clone().unwrap_or_default();
664                        if sid.starts_with(session_id) || sid == session_id {
665                            return CallToolResult {
666                                content: vec![ToolContent::Text {
667                                    text: serde_json::to_string_pretty(&json!({
668                                        "id": sid,
669                                        "title": s.session.title(),
670                                        "message_count": s.session.requests.len(),
671                                        "last_message_date": s.session.last_message_date,
672                                        "is_imported": s.session.is_imported,
673                                        "workspace_hash": ws.hash,
674                                        "project_path": ws.project_path,
675                                        "file_path": s.path.display().to_string(),
676                                        "messages": s.session.requests.iter().take(10).map(|r| {
677                                            let user_msg = r.message.as_ref()
678                                                .map(|m| m.get_text())
679                                                .unwrap_or_default();
680                                            let response_text = r.response.as_ref()
681                                                .and_then(|v| v.get("text"))
682                                                .and_then(|t| t.as_str())
683                                                .unwrap_or("");
684                                            json!({
685                                                "message": user_msg,
686                                                "response": response_text
687                                            })
688                                        }).collect::<Vec<_>>()
689                                    }))
690                                    .unwrap_or_default(),
691                                }],
692                                is_error: None,
693                            };
694                        }
695                    }
696                }
697            }
698
699            CallToolResult {
700                content: vec![ToolContent::Text {
701                    text: format!("Session not found: {}", session_id),
702                }],
703                is_error: Some(true),
704            }
705        }
706        Err(e) => CallToolResult {
707            content: vec![ToolContent::Text {
708                text: format!("Error finding session: {}", e),
709            }],
710            is_error: Some(true),
711        },
712    }
713}
714
715fn execute_show_history(path: Option<&str>) -> CallToolResult {
716    use crate::commands::history_show;
717
718    match history_show(path) {
719        Ok(_) => CallToolResult {
720            content: vec![ToolContent::Text {
721                text: json!({
722                    "status": "success",
723                    "message": "History displayed. See console output for details."
724                })
725                .to_string(),
726            }],
727            is_error: None,
728        },
729        Err(e) => CallToolResult {
730            content: vec![ToolContent::Text {
731                text: format!("Error showing history: {}", e),
732            }],
733            is_error: Some(true),
734        },
735    }
736}
737
738fn execute_merge_sessions(path: Option<&str>, title: Option<&str>, force: bool) -> CallToolResult {
739    use crate::commands::history_merge;
740
741    match history_merge(path, title, force, false) {
742        Ok(_) => CallToolResult {
743            content: vec![ToolContent::Text {
744                text: json!({
745                    "status": "success",
746                    "message": "Sessions merged successfully",
747                    "title": title,
748                    "force": force
749                })
750                .to_string(),
751            }],
752            is_error: None,
753        },
754        Err(e) => CallToolResult {
755            content: vec![ToolContent::Text {
756                text: format!("Error merging sessions: {}", e),
757            }],
758            is_error: Some(true),
759        },
760    }
761}
762
763fn execute_search(query: &str, limit: Option<usize>) -> CallToolResult {
764    use crate::commands::harvest_search;
765
766    let limit = limit.unwrap_or(20);
767
768    match harvest_search(None, query, None, limit) {
769        Ok(_) => CallToolResult {
770            content: vec![ToolContent::Text {
771                text: json!({
772                    "status": "success",
773                    "query": query,
774                    "limit": limit,
775                    "message": "Search completed. See console output for results."
776                })
777                .to_string(),
778            }],
779            is_error: None,
780        },
781        Err(e) => CallToolResult {
782            content: vec![ToolContent::Text {
783                text: format!("Error searching: {}", e),
784            }],
785            is_error: Some(true),
786        },
787    }
788}
789
790fn execute_detect(path: Option<&str>) -> CallToolResult {
791    use crate::commands::detect_all;
792
793    match detect_all(path, false) {
794        Ok(_) => CallToolResult {
795            content: vec![ToolContent::Text {
796                text: json!({
797                    "status": "success",
798                    "message": "Detection completed. See console output for details."
799                })
800                .to_string(),
801            }],
802            is_error: None,
803        },
804        Err(e) => CallToolResult {
805            content: vec![ToolContent::Text {
806                text: format!("Error detecting: {}", e),
807            }],
808            is_error: Some(true),
809        },
810    }
811}
812
813// ============================================================================
814// CSM Database Tool Implementations (csm-web integration)
815// ============================================================================
816
817fn execute_db_list_workspaces() -> CallToolResult {
818    use super::db;
819
820    if !db::csm_db_exists() {
821        return CallToolResult {
822            content: vec![ToolContent::Text {
823                text: json!({
824                    "error": "CSM database not found",
825                    "message": "The csm-web database has not been initialized. Run 'csm api' to start the API server first.",
826                    "db_path": db::get_csm_db_path().display().to_string()
827                }).to_string(),
828            }],
829            is_error: Some(true),
830        };
831    }
832
833    match db::list_db_workspaces() {
834        Ok(workspaces) => {
835            let infos: Vec<serde_json::Value> = workspaces
836                .iter()
837                .map(|ws| {
838                    json!({
839                        "id": ws.id,
840                        "name": ws.name,
841                        "path": ws.path,
842                        "provider": ws.provider,
843                        "created_at": ws.created_at,
844                        "updated_at": ws.updated_at
845                    })
846                })
847                .collect();
848
849            CallToolResult {
850                content: vec![ToolContent::Text {
851                    text: serde_json::to_string_pretty(&json!({
852                        "workspaces": infos,
853                        "total": infos.len(),
854                        "source": "csm-web database"
855                    }))
856                    .unwrap_or_default(),
857                }],
858                is_error: None,
859            }
860        }
861        Err(e) => CallToolResult {
862            content: vec![ToolContent::Text {
863                text: format!("Error listing workspaces from CSM database: {}", e),
864            }],
865            is_error: Some(true),
866        },
867    }
868}
869
870fn execute_db_list_sessions(
871    workspace_id: Option<&str>,
872    provider: Option<&str>,
873    limit: usize,
874) -> CallToolResult {
875    use super::db;
876
877    if !db::csm_db_exists() {
878        return CallToolResult {
879            content: vec![ToolContent::Text {
880                text: json!({
881                    "error": "CSM database not found",
882                    "message": "The csm-web database has not been initialized."
883                })
884                .to_string(),
885            }],
886            is_error: Some(true),
887        };
888    }
889
890    match db::list_db_sessions(workspace_id, provider, limit) {
891        Ok(sessions) => {
892            let infos: Vec<serde_json::Value> = sessions
893                .iter()
894                .map(|s| {
895                    json!({
896                        "id": s.id,
897                        "workspace_id": s.workspace_id,
898                        "provider": s.provider,
899                        "title": s.title,
900                        "model": s.model,
901                        "message_count": s.message_count,
902                        "created_at": s.created_at,
903                        "updated_at": s.updated_at,
904                        "archived": s.archived
905                    })
906                })
907                .collect();
908
909            CallToolResult {
910                content: vec![ToolContent::Text {
911                    text: serde_json::to_string_pretty(&json!({
912                        "sessions": infos,
913                        "total": infos.len(),
914                        "filters": {
915                            "workspace_id": workspace_id,
916                            "provider": provider,
917                            "limit": limit
918                        },
919                        "source": "csm-web database"
920                    }))
921                    .unwrap_or_default(),
922                }],
923                is_error: None,
924            }
925        }
926        Err(e) => CallToolResult {
927            content: vec![ToolContent::Text {
928                text: format!("Error listing sessions from CSM database: {}", e),
929            }],
930            is_error: Some(true),
931        },
932    }
933}
934
935fn execute_db_get_session(session_id: &str) -> CallToolResult {
936    use super::db;
937
938    if !db::csm_db_exists() {
939        return CallToolResult {
940            content: vec![ToolContent::Text {
941                text: json!({
942                    "error": "CSM database not found"
943                })
944                .to_string(),
945            }],
946            is_error: Some(true),
947        };
948    }
949
950    match db::get_db_session(session_id) {
951        Ok(Some(session)) => {
952            // Also fetch messages
953            let messages = db::get_db_messages(session_id).unwrap_or_default();
954
955            let message_infos: Vec<serde_json::Value> = messages
956                .iter()
957                .map(|m| {
958                    json!({
959                        "id": m.id,
960                        "role": m.role,
961                        "content": m.content,
962                        "model": m.model,
963                        "created_at": m.created_at
964                    })
965                })
966                .collect();
967
968            CallToolResult {
969                content: vec![ToolContent::Text {
970                    text: serde_json::to_string_pretty(&json!({
971                        "session": {
972                            "id": session.id,
973                            "workspace_id": session.workspace_id,
974                            "provider": session.provider,
975                            "title": session.title,
976                            "model": session.model,
977                            "message_count": session.message_count,
978                            "created_at": session.created_at,
979                            "updated_at": session.updated_at,
980                            "archived": session.archived
981                        },
982                        "messages": message_infos,
983                        "source": "csm-web database"
984                    }))
985                    .unwrap_or_default(),
986                }],
987                is_error: None,
988            }
989        }
990        Ok(None) => CallToolResult {
991            content: vec![ToolContent::Text {
992                text: format!("Session not found: {}", session_id),
993            }],
994            is_error: Some(true),
995        },
996        Err(e) => CallToolResult {
997            content: vec![ToolContent::Text {
998                text: format!("Error getting session: {}", e),
999            }],
1000            is_error: Some(true),
1001        },
1002    }
1003}
1004
1005fn execute_db_search(query: &str, limit: usize) -> CallToolResult {
1006    use super::db;
1007
1008    if !db::csm_db_exists() {
1009        return CallToolResult {
1010            content: vec![ToolContent::Text {
1011                text: json!({
1012                    "error": "CSM database not found"
1013                })
1014                .to_string(),
1015            }],
1016            is_error: Some(true),
1017        };
1018    }
1019
1020    match db::search_db_sessions(query, limit) {
1021        Ok(sessions) => {
1022            let infos: Vec<serde_json::Value> = sessions
1023                .iter()
1024                .map(|s| {
1025                    json!({
1026                        "id": s.id,
1027                        "title": s.title,
1028                        "provider": s.provider,
1029                        "message_count": s.message_count,
1030                        "updated_at": s.updated_at
1031                    })
1032                })
1033                .collect();
1034
1035            CallToolResult {
1036                content: vec![ToolContent::Text {
1037                    text: serde_json::to_string_pretty(&json!({
1038                        "query": query,
1039                        "results": infos,
1040                        "total": infos.len(),
1041                        "source": "csm-web database"
1042                    }))
1043                    .unwrap_or_default(),
1044                }],
1045                is_error: None,
1046            }
1047        }
1048        Err(e) => CallToolResult {
1049            content: vec![ToolContent::Text {
1050                text: format!("Error searching: {}", e),
1051            }],
1052            is_error: Some(true),
1053        },
1054    }
1055}
1056
1057fn execute_db_stats() -> CallToolResult {
1058    use super::db;
1059
1060    if !db::csm_db_exists() {
1061        return CallToolResult {
1062            content: vec![ToolContent::Text {
1063                text: json!({
1064                    "error": "CSM database not found",
1065                    "db_path": db::get_csm_db_path().display().to_string()
1066                })
1067                .to_string(),
1068            }],
1069            is_error: Some(true),
1070        };
1071    }
1072
1073    match db::count_sessions_by_provider() {
1074        Ok(counts) => {
1075            let provider_counts: serde_json::Value = counts
1076                .iter()
1077                .map(|(provider, count)| (provider.clone(), *count))
1078                .collect();
1079
1080            let total: i64 = counts.iter().map(|(_, c)| c).sum();
1081
1082            CallToolResult {
1083                content: vec![ToolContent::Text {
1084                    text: serde_json::to_string_pretty(&json!({
1085                        "total_sessions": total,
1086                        "by_provider": provider_counts,
1087                        "db_path": db::get_csm_db_path().display().to_string(),
1088                        "source": "csm-web database"
1089                    }))
1090                    .unwrap_or_default(),
1091                }],
1092                is_error: None,
1093            }
1094        }
1095        Err(e) => CallToolResult {
1096            content: vec![ToolContent::Text {
1097                text: format!("Error getting stats: {}", e),
1098            }],
1099            is_error: Some(true),
1100        },
1101    }
1102}