Skip to main content

systemprompt_cli/commands/plugins/mcp/
logs_db.rs

1//! Database-log patterns for `plugins mcp logs`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::sync::Arc;
7
8use anyhow::{Context, Result, anyhow};
9use systemprompt_database::DbPool;
10use systemprompt_loader::ConfigLoader;
11use systemprompt_logging::LoggingRepository;
12use systemprompt_runtime::AppContext;
13
14use super::logs::{LogLevel, LogsArgs};
15use super::types::McpLogsOutput;
16use crate::CliConfig;
17use crate::shared::CommandOutput;
18
19pub(super) async fn execute_db_mode(args: &LogsArgs, config: &CliConfig) -> Result<CommandOutput> {
20    let ctx = Arc::new(
21        AppContext::new()
22            .await
23            .context("Failed to initialize app context")?,
24    );
25    execute_db_mode_with_pool(args, ctx.db_pool(), config).await
26}
27
28pub async fn execute_db_mode_with_pool(
29    args: &LogsArgs,
30    pool: &DbPool,
31    _config: &CliConfig,
32) -> Result<CommandOutput> {
33    let repo = LoggingRepository::new(pool)?;
34
35    let patterns = match &args.server {
36        Some(service) => build_service_patterns(service),
37        None => build_all_mcp_patterns()?,
38    };
39
40    let fetch_limit = if args.level.is_some() {
41        (args.lines * 5) as i64
42    } else {
43        args.lines as i64
44    };
45
46    let entries = repo
47        .get_logs_by_module_patterns(&patterns, fetch_limit)
48        .await
49        .context("Failed to query logs from database")?;
50
51    if entries.is_empty() {
52        return Err(anyhow!("No logs found in database for MCP services"));
53    }
54
55    let logs: Vec<String> = entries
56        .iter()
57        .filter(|e| {
58            args.level
59                .is_none_or(|level: LogLevel| level.matches(&e.level.to_string()))
60        })
61        .take(args.lines)
62        .map(|e| {
63            format!(
64                "{} {} [{}] {}",
65                e.timestamp.format("%Y-%m-%d %H:%M:%S"),
66                e.level,
67                e.module,
68                e.message
69            )
70        })
71        .collect();
72
73    let service_label = args.server.clone().unwrap_or_else(|| "all".to_owned());
74    let level_label = args.level.map_or_else(String::new, |l| {
75        format!(" [{}+]", format!("{:?}", l).to_uppercase())
76    });
77
78    let output = McpLogsOutput {
79        service: Some(service_label.clone()),
80        source: "database".to_owned(),
81        logs,
82        log_files: vec![],
83    };
84
85    Ok(CommandOutput::card_value(
86        format!("MCP Logs (DB): {}{}", service_label, level_label),
87        &output,
88    ))
89}
90
91fn build_service_patterns(service: &str) -> Vec<String> {
92    vec![format!("%{}%", service), "%rmcp%".to_owned()]
93}
94
95fn build_all_mcp_patterns() -> Result<Vec<String>> {
96    let services_config = ConfigLoader::load().context("Failed to load services config")?;
97
98    let mut patterns: Vec<String> = services_config
99        .mcp_servers
100        .keys()
101        .flat_map(|name| vec![format!("%{}%", name)])
102        .collect();
103
104    patterns.push("%rmcp%".to_owned());
105    patterns.push("%mcp%".to_owned());
106
107    Ok(patterns)
108}