chasm_cli/mcp/
resources.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! MCP Resources - Expose csm data as MCP resources
4
5#![allow(dead_code, unused_imports)]
6
7use super::types::*;
8use serde_json::json;
9
10/// Get the list of available resources
11pub fn list_resources() -> Vec<Resource> {
12    vec![
13        // VS Code workspace resources
14        Resource {
15            uri: "csm://workspaces".to_string(),
16            name: "VS Code Workspaces".to_string(),
17            description: Some("All VS Code workspaces with chat sessions".to_string()),
18            mime_type: Some("application/json".to_string()),
19        },
20        Resource {
21            uri: "csm://sessions".to_string(),
22            name: "VS Code Sessions".to_string(),
23            description: Some("All chat sessions from VS Code workspaces".to_string()),
24            mime_type: Some("application/json".to_string()),
25        },
26        Resource {
27            uri: "csm://orphaned".to_string(),
28            name: "Orphaned Sessions".to_string(),
29            description: Some("Sessions on disk but not in VS Code's index".to_string()),
30            mime_type: Some("application/json".to_string()),
31        },
32        Resource {
33            uri: "csm://providers".to_string(),
34            name: "Providers".to_string(),
35            description: Some("Available LLM providers".to_string()),
36            mime_type: Some("application/json".to_string()),
37        },
38        // CSM Database resources (csm-web)
39        Resource {
40            uri: "csm://db/workspaces".to_string(),
41            name: "CSM-Web Workspaces".to_string(),
42            description: Some("Workspaces from the csm-web database".to_string()),
43            mime_type: Some("application/json".to_string()),
44        },
45        Resource {
46            uri: "csm://db/sessions".to_string(),
47            name: "CSM-Web Sessions".to_string(),
48            description: Some("Chat sessions from the csm-web database".to_string()),
49            mime_type: Some("application/json".to_string()),
50        },
51        Resource {
52            uri: "csm://db/stats".to_string(),
53            name: "CSM-Web Statistics".to_string(),
54            description: Some("Database statistics and session counts by provider".to_string()),
55            mime_type: Some("application/json".to_string()),
56        },
57    ]
58}
59
60/// Read a resource by URI
61pub fn read_resource(uri: &str) -> ReadResourceResult {
62    match uri {
63        // VS Code workspace resources
64        "csm://workspaces" => read_workspaces_resource(),
65        "csm://sessions" => read_sessions_resource(),
66        "csm://orphaned" => read_orphaned_resource(),
67        "csm://providers" => read_providers_resource(),
68        // CSM Database resources (csm-web)
69        "csm://db/workspaces" => read_db_workspaces_resource(),
70        "csm://db/sessions" => read_db_sessions_resource(),
71        "csm://db/stats" => read_db_stats_resource(),
72        _ => {
73            // Try to parse dynamic URIs
74            if let Some(hash) = uri.strip_prefix("csm://workspace/") {
75                read_workspace_resource(hash)
76            } else if let Some(id) = uri.strip_prefix("csm://session/") {
77                read_session_resource(id)
78            } else if let Some(id) = uri.strip_prefix("csm://db/session/") {
79                read_db_session_resource(id)
80            } else {
81                ReadResourceResult {
82                    contents: vec![ResourceContent {
83                        uri: uri.to_string(),
84                        mime_type: Some("text/plain".to_string()),
85                        text: Some(format!("Unknown resource: {}", uri)),
86                        blob: None,
87                    }],
88                }
89            }
90        }
91    }
92}
93
94fn read_workspaces_resource() -> ReadResourceResult {
95    use crate::workspace::discover_workspaces;
96
97    match discover_workspaces() {
98        Ok(workspaces) => {
99            let infos: Vec<serde_json::Value> = workspaces
100                .iter()
101                .map(|ws| {
102                    json!({
103                        "hash": ws.hash,
104                        "project_path": ws.project_path,
105                        "session_count": ws.chat_session_count,
106                        "has_chats": ws.has_chat_sessions,
107                        "workspace_path": ws.workspace_path.display().to_string()
108                    })
109                })
110                .collect();
111
112            ReadResourceResult {
113                contents: vec![ResourceContent {
114                    uri: "csm://workspaces".to_string(),
115                    mime_type: Some("application/json".to_string()),
116                    text: Some(
117                        serde_json::to_string_pretty(&json!({
118                            "workspaces": infos,
119                            "total": infos.len()
120                        }))
121                        .unwrap_or_default(),
122                    ),
123                    blob: None,
124                }],
125            }
126        }
127        Err(e) => ReadResourceResult {
128            contents: vec![ResourceContent {
129                uri: "csm://workspaces".to_string(),
130                mime_type: Some("text/plain".to_string()),
131                text: Some(format!("Error: {}", e)),
132                blob: None,
133            }],
134        },
135    }
136}
137
138fn read_sessions_resource() -> ReadResourceResult {
139    use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace};
140
141    match discover_workspaces() {
142        Ok(workspaces) => {
143            let mut all_sessions = Vec::new();
144
145            for ws in &workspaces {
146                if let Ok(sessions) = get_chat_sessions_from_workspace(&ws.workspace_path) {
147                    for s in sessions {
148                        all_sessions.push(json!({
149                            "id": s.session.session_id,
150                            "title": s.session.title(),
151                            "message_count": s.session.requests.len(),
152                            "last_message_date": s.session.last_message_date,
153                            "workspace_hash": ws.hash,
154                            "project_path": ws.project_path,
155                            "file_path": s.path.display().to_string()
156                        }));
157                    }
158                }
159            }
160
161            ReadResourceResult {
162                contents: vec![ResourceContent {
163                    uri: "csm://sessions".to_string(),
164                    mime_type: Some("application/json".to_string()),
165                    text: Some(
166                        serde_json::to_string_pretty(&json!({
167                            "sessions": all_sessions,
168                            "total": all_sessions.len()
169                        }))
170                        .unwrap_or_default(),
171                    ),
172                    blob: None,
173                }],
174            }
175        }
176        Err(e) => ReadResourceResult {
177            contents: vec![ResourceContent {
178                uri: "csm://sessions".to_string(),
179                mime_type: Some("text/plain".to_string()),
180                text: Some(format!("Error: {}", e)),
181                blob: None,
182            }],
183        },
184    }
185}
186
187fn read_orphaned_resource() -> ReadResourceResult {
188    // For now, return a placeholder - orphaned detection requires a specific workspace
189    ReadResourceResult {
190        contents: vec![ResourceContent {
191            uri: "csm://orphaned".to_string(),
192            mime_type: Some("application/json".to_string()),
193            text: Some(json!({
194                "message": "Use csm_list_orphaned tool with a specific path to find orphaned sessions"
195            }).to_string()),
196            blob: None,
197        }],
198    }
199}
200
201fn read_providers_resource() -> ReadResourceResult {
202    let providers = json!({
203        "providers": [
204            {"name": "copilot", "type": "vscode", "description": "GitHub Copilot (VS Code)"},
205            {"name": "cursor", "type": "ide", "description": "Cursor IDE"},
206            {"name": "ollama", "type": "local", "description": "Ollama local LLM"},
207            {"name": "vllm", "type": "local", "description": "vLLM server"},
208            {"name": "lm-studio", "type": "local", "description": "LM Studio"},
209            {"name": "jan", "type": "local", "description": "Jan.ai"},
210            {"name": "gpt4all", "type": "local", "description": "GPT4All"},
211            {"name": "localai", "type": "local", "description": "LocalAI"},
212            {"name": "llamafile", "type": "local", "description": "Llamafile"},
213            {"name": "text-gen-webui", "type": "local", "description": "Text Generation WebUI"},
214            {"name": "azure-foundry", "type": "cloud", "description": "Azure AI Foundry"},
215            {"name": "chatgpt", "type": "web", "description": "ChatGPT (share links)"},
216            {"name": "claude", "type": "web", "description": "Claude (share links)"},
217            {"name": "gemini", "type": "web", "description": "Gemini (share links)"},
218            {"name": "perplexity", "type": "web", "description": "Perplexity (share links)"},
219            {"name": "deepseek", "type": "web", "description": "DeepSeek (share links)"}
220        ]
221    });
222
223    ReadResourceResult {
224        contents: vec![ResourceContent {
225            uri: "csm://providers".to_string(),
226            mime_type: Some("application/json".to_string()),
227            text: Some(serde_json::to_string_pretty(&providers).unwrap_or_default()),
228            blob: None,
229        }],
230    }
231}
232
233fn read_workspace_resource(hash: &str) -> ReadResourceResult {
234    use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace};
235
236    match discover_workspaces() {
237        Ok(workspaces) => {
238            for ws in &workspaces {
239                if ws.hash.starts_with(hash) || ws.hash == hash {
240                    let sessions =
241                        get_chat_sessions_from_workspace(&ws.workspace_path).unwrap_or_default();
242
243                    let session_infos: Vec<serde_json::Value> = sessions
244                        .iter()
245                        .map(|s| {
246                            json!({
247                                "id": s.session.session_id,
248                                "title": s.session.title(),
249                                "message_count": s.session.requests.len(),
250                                "file_path": s.path.display().to_string()
251                            })
252                        })
253                        .collect();
254
255                    return ReadResourceResult {
256                        contents: vec![ResourceContent {
257                            uri: format!("csm://workspace/{}", ws.hash),
258                            mime_type: Some("application/json".to_string()),
259                            text: Some(
260                                serde_json::to_string_pretty(&json!({
261                                    "hash": ws.hash,
262                                    "project_path": ws.project_path,
263                                    "workspace_path": ws.workspace_path.display().to_string(),
264                                    "session_count": ws.chat_session_count,
265                                    "sessions": session_infos
266                                }))
267                                .unwrap_or_default(),
268                            ),
269                            blob: None,
270                        }],
271                    };
272                }
273            }
274
275            ReadResourceResult {
276                contents: vec![ResourceContent {
277                    uri: format!("csm://workspace/{}", hash),
278                    mime_type: Some("text/plain".to_string()),
279                    text: Some(format!("Workspace not found: {}", hash)),
280                    blob: None,
281                }],
282            }
283        }
284        Err(e) => ReadResourceResult {
285            contents: vec![ResourceContent {
286                uri: format!("csm://workspace/{}", hash),
287                mime_type: Some("text/plain".to_string()),
288                text: Some(format!("Error: {}", e)),
289                blob: None,
290            }],
291        },
292    }
293}
294
295fn read_session_resource(session_id: &str) -> ReadResourceResult {
296    use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace};
297
298    match discover_workspaces() {
299        Ok(workspaces) => {
300            for ws in &workspaces {
301                if let Ok(sessions) = get_chat_sessions_from_workspace(&ws.workspace_path) {
302                    for s in sessions {
303                        let sid = s.session.session_id.clone().unwrap_or_default();
304                        if sid.starts_with(session_id) || sid == session_id {
305                            // Return full session content
306                            let messages: Vec<serde_json::Value> = s
307                                .session
308                                .requests
309                                .iter()
310                                .map(|r| {
311                                    let user_msg = r
312                                        .message
313                                        .as_ref()
314                                        .map(|m| m.get_text())
315                                        .unwrap_or_default();
316                                    let response_text = r
317                                        .response
318                                        .as_ref()
319                                        .and_then(|v| v.get("text"))
320                                        .and_then(|t| t.as_str())
321                                        .unwrap_or("");
322                                    json!({
323                                        "message": user_msg,
324                                        "response": response_text,
325                                        "timestamp": r.timestamp
326                                    })
327                                })
328                                .collect();
329
330                            return ReadResourceResult {
331                                contents: vec![ResourceContent {
332                                    uri: format!("csm://session/{}", sid),
333                                    mime_type: Some("application/json".to_string()),
334                                    text: Some(
335                                        serde_json::to_string_pretty(&json!({
336                                            "id": sid,
337                                            "title": s.session.title(),
338                                            "message_count": s.session.requests.len(),
339                                            "last_message_date": s.session.last_message_date,
340                                            "is_imported": s.session.is_imported,
341                                            "workspace_hash": ws.hash,
342                                            "project_path": ws.project_path,
343                                            "file_path": s.path.display().to_string(),
344                                            "messages": messages
345                                        }))
346                                        .unwrap_or_default(),
347                                    ),
348                                    blob: None,
349                                }],
350                            };
351                        }
352                    }
353                }
354            }
355
356            ReadResourceResult {
357                contents: vec![ResourceContent {
358                    uri: format!("csm://session/{}", session_id),
359                    mime_type: Some("text/plain".to_string()),
360                    text: Some(format!("Session not found: {}", session_id)),
361                    blob: None,
362                }],
363            }
364        }
365        Err(e) => ReadResourceResult {
366            contents: vec![ResourceContent {
367                uri: format!("csm://session/{}", session_id),
368                mime_type: Some("text/plain".to_string()),
369                text: Some(format!("Error: {}", e)),
370                blob: None,
371            }],
372        },
373    }
374}
375
376// ============================================================================
377// CSM Database Resource Implementations (csm-web integration)
378// ============================================================================
379
380fn read_db_workspaces_resource() -> ReadResourceResult {
381    use super::db;
382
383    if !db::csm_db_exists() {
384        return ReadResourceResult {
385            contents: vec![ResourceContent {
386                uri: "csm://db/workspaces".to_string(),
387                mime_type: Some("application/json".to_string()),
388                text: Some(
389                    json!({
390                        "error": "CSM database not found",
391                        "message": "Initialize csm-web database first",
392                        "db_path": db::get_csm_db_path().display().to_string()
393                    })
394                    .to_string(),
395                ),
396                blob: None,
397            }],
398        };
399    }
400
401    match db::list_db_workspaces() {
402        Ok(workspaces) => {
403            let infos: Vec<serde_json::Value> = workspaces
404                .iter()
405                .map(|ws| {
406                    json!({
407                        "id": ws.id,
408                        "name": ws.name,
409                        "path": ws.path,
410                        "provider": ws.provider,
411                        "created_at": ws.created_at,
412                        "updated_at": ws.updated_at
413                    })
414                })
415                .collect();
416
417            ReadResourceResult {
418                contents: vec![ResourceContent {
419                    uri: "csm://db/workspaces".to_string(),
420                    mime_type: Some("application/json".to_string()),
421                    text: Some(
422                        serde_json::to_string_pretty(&json!({
423                            "workspaces": infos,
424                            "total": infos.len(),
425                            "source": "csm-web database"
426                        }))
427                        .unwrap_or_default(),
428                    ),
429                    blob: None,
430                }],
431            }
432        }
433        Err(e) => ReadResourceResult {
434            contents: vec![ResourceContent {
435                uri: "csm://db/workspaces".to_string(),
436                mime_type: Some("text/plain".to_string()),
437                text: Some(format!("Error: {}", e)),
438                blob: None,
439            }],
440        },
441    }
442}
443
444fn read_db_sessions_resource() -> ReadResourceResult {
445    use super::db;
446
447    if !db::csm_db_exists() {
448        return ReadResourceResult {
449            contents: vec![ResourceContent {
450                uri: "csm://db/sessions".to_string(),
451                mime_type: Some("application/json".to_string()),
452                text: Some(
453                    json!({
454                        "error": "CSM database not found"
455                    })
456                    .to_string(),
457                ),
458                blob: None,
459            }],
460        };
461    }
462
463    match db::list_db_sessions(None, None, 100) {
464        Ok(sessions) => {
465            let infos: Vec<serde_json::Value> = sessions
466                .iter()
467                .map(|s| {
468                    json!({
469                        "id": s.id,
470                        "workspace_id": s.workspace_id,
471                        "provider": s.provider,
472                        "title": s.title,
473                        "model": s.model,
474                        "message_count": s.message_count,
475                        "created_at": s.created_at,
476                        "updated_at": s.updated_at
477                    })
478                })
479                .collect();
480
481            ReadResourceResult {
482                contents: vec![ResourceContent {
483                    uri: "csm://db/sessions".to_string(),
484                    mime_type: Some("application/json".to_string()),
485                    text: Some(
486                        serde_json::to_string_pretty(&json!({
487                            "sessions": infos,
488                            "total": infos.len(),
489                            "source": "csm-web database"
490                        }))
491                        .unwrap_or_default(),
492                    ),
493                    blob: None,
494                }],
495            }
496        }
497        Err(e) => ReadResourceResult {
498            contents: vec![ResourceContent {
499                uri: "csm://db/sessions".to_string(),
500                mime_type: Some("text/plain".to_string()),
501                text: Some(format!("Error: {}", e)),
502                blob: None,
503            }],
504        },
505    }
506}
507
508fn read_db_stats_resource() -> ReadResourceResult {
509    use super::db;
510
511    if !db::csm_db_exists() {
512        return ReadResourceResult {
513            contents: vec![ResourceContent {
514                uri: "csm://db/stats".to_string(),
515                mime_type: Some("application/json".to_string()),
516                text: Some(
517                    json!({
518                        "error": "CSM database not found",
519                        "db_path": db::get_csm_db_path().display().to_string()
520                    })
521                    .to_string(),
522                ),
523                blob: None,
524            }],
525        };
526    }
527
528    match db::count_sessions_by_provider() {
529        Ok(counts) => {
530            let provider_counts: serde_json::Value = counts
531                .iter()
532                .map(|(provider, count)| (provider.clone(), *count))
533                .collect();
534
535            let total: i64 = counts.iter().map(|(_, c)| c).sum();
536
537            ReadResourceResult {
538                contents: vec![ResourceContent {
539                    uri: "csm://db/stats".to_string(),
540                    mime_type: Some("application/json".to_string()),
541                    text: Some(
542                        serde_json::to_string_pretty(&json!({
543                            "total_sessions": total,
544                            "by_provider": provider_counts,
545                            "db_path": db::get_csm_db_path().display().to_string(),
546                            "source": "csm-web database"
547                        }))
548                        .unwrap_or_default(),
549                    ),
550                    blob: None,
551                }],
552            }
553        }
554        Err(e) => ReadResourceResult {
555            contents: vec![ResourceContent {
556                uri: "csm://db/stats".to_string(),
557                mime_type: Some("text/plain".to_string()),
558                text: Some(format!("Error: {}", e)),
559                blob: None,
560            }],
561        },
562    }
563}
564
565fn read_db_session_resource(session_id: &str) -> ReadResourceResult {
566    use super::db;
567
568    if !db::csm_db_exists() {
569        return ReadResourceResult {
570            contents: vec![ResourceContent {
571                uri: format!("csm://db/session/{}", session_id),
572                mime_type: Some("application/json".to_string()),
573                text: Some(
574                    json!({
575                        "error": "CSM database not found"
576                    })
577                    .to_string(),
578                ),
579                blob: None,
580            }],
581        };
582    }
583
584    match db::get_db_session(session_id) {
585        Ok(Some(session)) => {
586            let messages = db::get_db_messages(session_id).unwrap_or_default();
587
588            let message_infos: Vec<serde_json::Value> = messages
589                .iter()
590                .map(|m| {
591                    json!({
592                        "id": m.id,
593                        "role": m.role,
594                        "content": m.content,
595                        "model": m.model,
596                        "created_at": m.created_at
597                    })
598                })
599                .collect();
600
601            ReadResourceResult {
602                contents: vec![ResourceContent {
603                    uri: format!("csm://db/session/{}", session_id),
604                    mime_type: Some("application/json".to_string()),
605                    text: Some(
606                        serde_json::to_string_pretty(&json!({
607                            "session": {
608                                "id": session.id,
609                                "workspace_id": session.workspace_id,
610                                "provider": session.provider,
611                                "title": session.title,
612                                "model": session.model,
613                                "message_count": session.message_count,
614                                "created_at": session.created_at,
615                                "updated_at": session.updated_at
616                            },
617                            "messages": message_infos,
618                            "source": "csm-web database"
619                        }))
620                        .unwrap_or_default(),
621                    ),
622                    blob: None,
623                }],
624            }
625        }
626        Ok(None) => ReadResourceResult {
627            contents: vec![ResourceContent {
628                uri: format!("csm://db/session/{}", session_id),
629                mime_type: Some("text/plain".to_string()),
630                text: Some(format!("Session not found: {}", session_id)),
631                blob: None,
632            }],
633        },
634        Err(e) => ReadResourceResult {
635            contents: vec![ResourceContent {
636                uri: format!("csm://db/session/{}", session_id),
637                mime_type: Some("text/plain".to_string()),
638                text: Some(format!("Error: {}", e)),
639                blob: None,
640            }],
641        },
642    }
643}