gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! MCP tool definitions for GDELT.

#![cfg(feature = "mcp")]

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Search news articles using the DOC API
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocSearchInput {
    /// Search query (supports GDELT query syntax)
    pub query: String,
    /// Time span (e.g., "24h", "7d", "30d")
    #[serde(default = "default_timespan")]
    pub timespan: String,
    /// Maximum number of results
    #[serde(default = "default_max_records")]
    pub max_records: u32,
    /// Source language (ISO 639-1)
    pub lang: Option<String>,
    /// Source country (FIPS code)
    pub country: Option<String>,
}

fn default_timespan() -> String {
    "24h".to_string()
}

fn default_max_records() -> u32 {
    25
}

/// Query events from the local database
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EventsQueryInput {
    /// Actor code filter
    pub actor: Option<String>,
    /// Event code filter (supports wildcards like "14*")
    pub event_code: Option<String>,
    /// Country filter
    pub country: Option<String>,
    /// Start date (YYYY-MM-DD)
    pub start: Option<String>,
    /// End date (YYYY-MM-DD)
    pub end: Option<String>,
    /// Maximum results
    #[serde(default = "default_limit")]
    pub limit: u32,
}

fn default_limit() -> u32 {
    100
}

/// Query GKG data
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GkgQueryInput {
    /// Theme filter
    pub theme: Option<String>,
    /// Person filter
    pub person: Option<String>,
    /// Organization filter
    pub organization: Option<String>,
    /// Start date (YYYY-MM-DD)
    pub start: Option<String>,
    /// End date (YYYY-MM-DD)
    pub end: Option<String>,
    /// Maximum results
    #[serde(default = "default_limit")]
    pub limit: u32,
}

/// Run trend analysis
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AnalyticsTrendsInput {
    /// Topics to analyze
    pub topics: Vec<String>,
    /// Time span (e.g., "7d", "30d")
    #[serde(default = "default_analytics_timespan")]
    pub timespan: String,
    /// Detect anomalies
    #[serde(default)]
    pub detect_anomalies: bool,
}

fn default_analytics_timespan() -> String {
    "30d".to_string()
}

/// Run sentiment analysis
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AnalyticsSentimentInput {
    /// Topic to analyze
    pub topic: String,
    /// Time span
    #[serde(default = "default_analytics_timespan")]
    pub timespan: String,
}

/// Geographic search
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GeoSearchInput {
    /// Search query
    pub query: String,
    /// Time span
    #[serde(default = "default_geo_timespan")]
    pub timespan: String,
    /// Country filter
    pub country: Option<String>,
    /// Maximum points
    #[serde(default = "default_geo_max_points")]
    pub max_points: u32,
}

fn default_geo_timespan() -> String {
    "7d".to_string()
}

fn default_geo_max_points() -> u32 {
    250
}

/// TV search input
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TvSearchInput {
    /// Search query
    pub query: String,
    /// Time span
    #[serde(default = "default_geo_timespan")]
    pub timespan: String,
    /// Station filter
    pub station: Option<String>,
}

/// Tool result wrapper
#[derive(Debug, Clone, Serialize)]
pub struct ToolResult<T> {
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<T>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

impl<T> ToolResult<T> {
    pub fn success(data: T) -> Self {
        Self {
            success: true,
            data: Some(data),
            error: None,
        }
    }

    pub fn error(msg: impl Into<String>) -> Self {
        Self {
            success: false,
            data: None,
            error: Some(msg.into()),
        }
    }
}

/// List of available tools
pub fn list_tools() -> Vec<ToolInfo> {
    vec![
        ToolInfo {
            name: "gdelt_doc_search".to_string(),
            description: "Search news articles using the GDELT DOC 2.0 API".to_string(),
            input_schema: schemars::schema_for!(DocSearchInput),
        },
        ToolInfo {
            name: "gdelt_events_query".to_string(),
            description: "Query events from the local GDELT database".to_string(),
            input_schema: schemars::schema_for!(EventsQueryInput),
        },
        ToolInfo {
            name: "gdelt_gkg_query".to_string(),
            description: "Query Global Knowledge Graph data".to_string(),
            input_schema: schemars::schema_for!(GkgQueryInput),
        },
        ToolInfo {
            name: "gdelt_analytics_trends".to_string(),
            description: "Analyze trends for topics over time".to_string(),
            input_schema: schemars::schema_for!(AnalyticsTrendsInput),
        },
        ToolInfo {
            name: "gdelt_analytics_sentiment".to_string(),
            description: "Analyze sentiment for a topic".to_string(),
            input_schema: schemars::schema_for!(AnalyticsSentimentInput),
        },
        ToolInfo {
            name: "gdelt_geo_search".to_string(),
            description: "Search with geographic context".to_string(),
            input_schema: schemars::schema_for!(GeoSearchInput),
        },
        ToolInfo {
            name: "gdelt_tv_search".to_string(),
            description: "Search television news coverage".to_string(),
            input_schema: schemars::schema_for!(TvSearchInput),
        },
    ]
}

/// Tool information
#[derive(Debug, Clone, Serialize)]
pub struct ToolInfo {
    pub name: String,
    pub description: String,
    pub input_schema: schemars::schema::RootSchema,
}