use async_trait::async_trait;
use pmcp::server::{Server, ToolHandler};
use pmcp::types::*;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct CurrencyServer {
supported_currencies: Vec<String>,
cache: HashMap<String, (Value, std::time::SystemTime)>,
}
impl CurrencyServer {
fn new() -> Self {
Self {
supported_currencies: vec![
"EUR".to_string(),
"USD".to_string(),
"GBP".to_string(),
"CHF".to_string(),
"JPY".to_string(),
"CAD".to_string(),
"AUD".to_string(),
"SEK".to_string(),
"NOK".to_string(),
"DKK".to_string(),
"PLN".to_string(),
"CZK".to_string(),
"HUF".to_string(),
"BGN".to_string(),
"RON".to_string(),
],
cache: HashMap::new(),
}
}
fn validate_currency(&self, currency: &str) -> Result<(), String> {
if self.supported_currencies.contains(¤cy.to_uppercase()) {
Ok(())
} else {
Err(format!(
"Unsupported currency: {}. Supported: {:?}",
currency, self.supported_currencies
))
}
}
async fn fetch_current_rates(
&mut self,
base: &str,
symbols: Option<&str>,
) -> Result<Value, String> {
let cache_key = format!("current_{}_{}", base, symbols.unwrap_or("all"));
if let Some((data, timestamp)) = self.cache.get(&cache_key) {
if timestamp.elapsed().unwrap_or_default().as_secs() < 86400 {
return Ok(data.clone());
}
}
let rates = match base {
"EUR" => json!({
"USD": 1.0847,
"GBP": 0.8312,
"CHF": 0.9521,
"JPY": 164.32,
"CAD": 1.5123,
"AUD": 1.6234,
"SEK": 11.2345,
"NOK": 11.8765,
"DKK": 7.4567,
"PLN": 4.2789,
"CZK": 25.1234,
"HUF": 412.34,
"BGN": 1.9558,
"RON": 4.9876
}),
"USD" => json!({
"EUR": 0.9219,
"GBP": 0.7662,
"CHF": 0.8778,
"JPY": 151.45,
"CAD": 1.3945,
"AUD": 1.4967,
"SEK": 10.3567,
"NOK": 10.9456,
"DKK": 6.8789,
"PLN": 3.9445,
"CZK": 23.1678,
"HUF": 380.12,
"BGN": 1.8034,
"RON": 4.5987
}),
_ => return Err("Base currency not supported for demo".to_string()),
};
let result = json!({
"amount": 1.0,
"base": base,
"date": "2025-01-26",
"rates": rates
});
self.cache
.insert(cache_key, (result.clone(), std::time::SystemTime::now()));
Ok(result)
}
async fn fetch_historical_rates(
&mut self,
base: &str,
start_date: &str,
end_date: &str,
_symbols: Option<&str>,
) -> Result<Value, String> {
let cache_key = format!("historical_{}_{}_{}", base, start_date, end_date);
if let Some((data, timestamp)) = self.cache.get(&cache_key) {
if timestamp.elapsed().unwrap_or_default().as_secs() < 86400 {
return Ok(data.clone());
}
}
let mut historical_data = HashMap::new();
let base_date = chrono::NaiveDate::parse_from_str("2025-01-26", "%Y-%m-%d")
.map_err(|e| format!("Date parsing error: {}", e))?;
for i in 0..30 {
let date = base_date - chrono::Duration::days(i);
let date_str = date.format("%Y-%m-%d").to_string();
let variation = (i as f64 * 0.001) + ((i % 3) as f64) * 0.002;
let rates = match base {
"EUR" => json!({
"USD": 1.0847 + variation,
"GBP": 0.8312 - variation * 0.5,
"CHF": 0.9521 + variation * 0.3,
"JPY": 164.32 + variation * 10.0
}),
"USD" => json!({
"EUR": 0.9219 - variation,
"GBP": 0.7662 + variation * 0.4,
"CHF": 0.8778 - variation * 0.2,
"JPY": 151.45 - variation * 8.0
}),
_ => return Err("Base currency not supported for demo".to_string()),
};
historical_data.insert(date_str, rates);
}
let result = json!({
"amount": 1.0,
"base": base,
"start_date": start_date,
"end_date": end_date,
"rates": historical_data
});
self.cache
.insert(cache_key, (result.clone(), std::time::SystemTime::now()));
Ok(result)
}
fn calculate_moving_average(&self, rates: &[f64], window: usize) -> Vec<f64> {
if rates.len() < window {
return vec![];
}
let mut moving_averages = Vec::new();
for i in window..=rates.len() {
let sum: f64 = rates[i - window..i].iter().sum();
moving_averages.push(sum / window as f64);
}
moving_averages
}
fn predict_future_rates(&self, rates: &[f64], days: usize) -> Vec<f64> {
if rates.len() < 2 {
return vec![];
}
let n = rates.len() as f64;
let x_sum: f64 = (0..rates.len()).map(|i| i as f64).sum();
let y_sum: f64 = rates.iter().sum();
let xy_sum: f64 = rates.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
let x2_sum: f64 = (0..rates.len()).map(|i| (i as f64).powi(2)).sum();
let slope = (n * xy_sum - x_sum * y_sum) / (n * x2_sum - x_sum.powi(2));
let intercept = (y_sum - slope * x_sum) / n;
let mut predictions = Vec::new();
for i in 0..days {
let x = rates.len() as f64 + i as f64;
predictions.push(slope * x + intercept);
}
predictions
}
fn generate_sparkline(&self, rates: &[f64]) -> String {
if rates.is_empty() {
return String::new();
}
let min_rate = rates.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_rate = rates.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let range = max_rate - min_rate;
if range == 0.0 {
return "─".repeat(rates.len());
}
let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
rates
.iter()
.map(|&rate| {
let normalized = (rate - min_rate) / range;
let index =
((normalized * (chars.len() - 1) as f64).round() as usize).min(chars.len() - 1);
chars[index]
})
.collect()
}
}
#[derive(Debug, Deserialize)]
struct GetRatesArgs {
#[serde(default = "default_base")]
base: String,
symbols: Option<String>,
}
#[derive(Debug, Deserialize)]
struct AnalyzeTrendArgs {
#[serde(default = "default_base")]
base: String,
#[serde(default = "default_target")]
target: String,
#[serde(default = "default_days")]
days: usize,
#[serde(default = "default_predict_days")]
predict_days: usize,
}
#[derive(Debug, Deserialize)]
struct GetHistoricalArgs {
#[serde(default = "default_base")]
base: String,
#[serde(default = "default_days")]
days: usize,
symbols: Option<String>,
}
fn default_base() -> String {
"EUR".to_string()
}
fn default_target() -> String {
"USD".to_string()
}
fn default_days() -> usize {
30
}
fn default_predict_days() -> usize {
7
}
struct GetRatesTool {
server: CurrencyServer,
}
#[async_trait]
impl ToolHandler for GetRatesTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let mut server = self.server.clone();
let params: GetRatesArgs = serde_json::from_value(args)
.map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {}", e)))?;
server
.validate_currency(¶ms.base)
.map_err(pmcp::Error::invalid_params)?;
let rates = server
.fetch_current_rates(¶ms.base, params.symbols.as_deref())
.await
.map_err(|e| pmcp::Error::internal(format!("Failed to fetch rates: {}", e)))?;
let result = CallToolResult::new(vec![Content::text(format!(
"Current exchange rates for {} on {}:\n\n{}",
params.base,
rates["date"].as_str().unwrap_or("unknown"),
serde_json::to_string_pretty(&rates["rates"])
.unwrap_or_else(|_| "Error formatting rates".to_string())
))]);
Ok(serde_json::to_value(result)?)
}
}
struct AnalyzeTrendTool {
server: CurrencyServer,
}
#[async_trait]
impl ToolHandler for AnalyzeTrendTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let mut server = self.server.clone();
let params: AnalyzeTrendArgs = serde_json::from_value(args)
.map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {}", e)))?;
server
.validate_currency(¶ms.base)
.map_err(pmcp::Error::invalid_params)?;
server
.validate_currency(¶ms.target)
.map_err(pmcp::Error::invalid_params)?;
let start_date =
chrono::Utc::now().date_naive() - chrono::Duration::days(params.days as i64);
let end_date = chrono::Utc::now().date_naive();
let historical = server
.fetch_historical_rates(
¶ms.base,
&start_date.format("%Y-%m-%d").to_string(),
&end_date.format("%Y-%m-%d").to_string(),
Some(¶ms.target),
)
.await
.map_err(|e| {
pmcp::Error::internal(format!("Failed to fetch historical data: {}", e))
})?;
let mut rates = Vec::new();
if let Some(historical_rates) = historical["rates"].as_object() {
for (_date, rate_data) in historical_rates {
if let Some(target_rate) = rate_data.get(¶ms.target).and_then(|v| v.as_f64()) {
rates.push(target_rate);
}
}
}
let moving_avg_7 = server.calculate_moving_average(&rates, 7);
let moving_avg_14 = server.calculate_moving_average(&rates, 14);
let predictions = server.predict_future_rates(&rates, params.predict_days);
let sparkline = server.generate_sparkline(&rates);
let current_rate = rates.last().copied().unwrap_or(0.0);
let trend_direction = if rates.len() >= 2 {
let previous = rates[rates.len() - 2];
if current_rate > previous {
"↗️ Rising"
} else if current_rate < previous {
"↘️ Falling"
} else {
"→ Stable"
}
} else {
"→ Insufficient data"
};
let analysis = format!(
"Currency Trend Analysis: {} → {}\n\
==========================================\n\
\n\
📊 Current Rate: {:.4}\n\
📈 Trend: {}\n\
📅 Analysis Period: {} days\n\
\n\
📉 Rate Visualization:\n\
{}\n\
\n\
📋 Moving Averages:\n\
• 7-day MA: {:.4}\n\
• 14-day MA: {:.4}\n\
\n\
🔮 Predictions (next {} days):\n\
{}\n\
\n\
💡 Analysis:\n\
• Total data points: {}\n\
• Rate range: {:.4} - {:.4}\n\
• Volatility: {:.4}%",
params.base,
params.target,
current_rate,
trend_direction,
params.days,
sparkline,
moving_avg_7.last().copied().unwrap_or(0.0),
moving_avg_14.last().copied().unwrap_or(0.0),
params.predict_days,
predictions
.iter()
.enumerate()
.map(|(i, &pred)| format!("Day {}: {:.4}", i + 1, pred))
.collect::<Vec<_>>()
.join("\n"),
rates.len(),
rates.iter().fold(f64::INFINITY, |a, &b| a.min(b)),
rates.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)),
if rates.len() > 1 {
let mean = rates.iter().sum::<f64>() / rates.len() as f64;
let variance =
rates.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / rates.len() as f64;
(variance.sqrt() / mean) * 100.0
} else {
0.0
}
);
let result = CallToolResult::new(vec![Content::text(analysis)]);
Ok(serde_json::to_value(result)?)
}
}
struct ListCurrenciesTool {
server: CurrencyServer,
}
#[async_trait]
impl ToolHandler for ListCurrenciesTool {
async fn handle(&self, _args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let server = self.server.clone();
let result = CallToolResult::new(vec![Content::text(format!(
"Supported Currencies ({} total):\n\n{}",
server.supported_currencies.len(),
server.supported_currencies.join(", ")
))]);
Ok(serde_json::to_value(result)?)
}
}
struct GetHistoricalTool {
server: CurrencyServer,
}
#[async_trait]
impl ToolHandler for GetHistoricalTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let mut server = self.server.clone();
let params: GetHistoricalArgs = serde_json::from_value(args)
.map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {}", e)))?;
server
.validate_currency(¶ms.base)
.map_err(pmcp::Error::invalid_params)?;
let start_date =
chrono::Utc::now().date_naive() - chrono::Duration::days(params.days as i64);
let end_date = chrono::Utc::now().date_naive();
let historical = server
.fetch_historical_rates(
¶ms.base,
&start_date.format("%Y-%m-%d").to_string(),
&end_date.format("%Y-%m-%d").to_string(),
params.symbols.as_deref(),
)
.await
.map_err(|e| {
pmcp::Error::internal(format!("Failed to fetch historical data: {}", e))
})?;
let result = CallToolResult::new(vec![Content::text(format!(
"Historical exchange rates for {} (last {} days):\n\n{}",
params.base,
params.days,
serde_json::to_string_pretty(&historical)
.unwrap_or_else(|_| "Error formatting historical data".to_string())
))]);
Ok(serde_json::to_value(result)?)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let currency_server = CurrencyServer::new();
let server = Server::builder()
.name("EU Currency Server")
.version("1.0.0")
.capabilities(ServerCapabilities::tools_only())
.tool(
"get_rates",
GetRatesTool {
server: currency_server.clone(),
},
)
.tool(
"analyze_trend",
AnalyzeTrendTool {
server: currency_server.clone(),
},
)
.tool(
"list_currencies",
ListCurrenciesTool {
server: currency_server.clone(),
},
)
.tool(
"get_historical",
GetHistoricalTool {
server: currency_server,
},
)
.build()?;
println!("🏦 EU Currency MCP Server starting...");
println!("💱 Providing real-time currency analysis and predictions");
println!("📊 Tools available: get_rates, analyze_trend, list_currencies, get_historical");
println!("💾 Smart caching enabled (24-hour cache)");
println!();
server.run_stdio().await?;
Ok(())
}