use clap::Parser;
use std::net::SocketAddr;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
pub endpoint: String,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub allow_trading: bool,
pub max_order_usd: Option<u64>,
pub transport: Transport,
pub http_listen: SocketAddr,
pub http_bearer_token: Option<String>,
pub log_format: LogFormat,
pub order_transport: OrderTransport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transport {
Stdio,
Http,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Text,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderTransport {
Http,
Fix,
}
fn network_to_endpoint(value: &str) -> Option<String> {
match value.trim().to_ascii_lowercase().as_str() {
"testnet" | "test" => Some("https://test.deribit.com".to_string()),
"mainnet" | "main" | "production" | "prod" => Some("https://www.deribit.com".to_string()),
_ => None,
}
}
impl OrderTransport {
fn parse(s: &str) -> Option<Self> {
match s {
"http" => Some(Self::Http),
"fix" => Some(Self::Fix),
_ => None,
}
}
}
impl Config {
pub fn load() -> anyhow::Result<Self> {
let args = Args::parse();
if let Some(ref env_file) = args.env_file {
dotenvy::from_path(env_file).ok(); } else if std::path::Path::new(".env").exists() {
dotenvy::dotenv().ok();
}
let endpoint = args
.endpoint()
.or_else(|| {
std::env::var("DERIBIT_NETWORK")
.ok()
.and_then(|v| network_to_endpoint(&v))
})
.or_else(|| std::env::var("DERIBIT_ENDPOINT").ok())
.unwrap_or_else(|| "https://test.deribit.com".to_string());
let client_id = args
.client_id
.clone()
.or_else(|| std::env::var("DERIBIT_CLIENT_ID").ok());
let client_secret = std::env::var("DERIBIT_CLIENT_SECRET").ok();
let allow_trading = args.allow_trading
|| std::env::var("DERIBIT_ALLOW_TRADING")
.map(|v| v == "1")
.unwrap_or(false);
let max_order_usd = args.max_order_usd.or_else(|| {
std::env::var("DERIBIT_MAX_ORDER_USD")
.ok()
.and_then(|v| v.parse().ok())
});
let transport = args
.transport()
.or_else(|| {
std::env::var("DERIBIT_MCP_TRANSPORT")
.ok()
.and_then(|v| match v.as_str() {
"stdio" => Some(Transport::Stdio),
"http" => Some(Transport::Http),
_ => None,
})
})
.unwrap_or(Transport::Stdio);
let http_listen = args
.listen
.or_else(|| {
std::env::var("DERIBIT_HTTP_LISTEN")
.ok()
.and_then(|v| v.parse().ok())
})
.unwrap_or_else(|| {
"127.0.0.1:8723"
.parse()
.expect("invalid default listen addr")
});
let http_bearer_token = std::env::var("DERIBIT_HTTP_BEARER_TOKEN")
.ok()
.filter(|v| !v.is_empty());
let order_transport = args
.order_transport()
.or_else(|| {
std::env::var("DERIBIT_ORDER_TRANSPORT")
.ok()
.and_then(|v| OrderTransport::parse(&v))
})
.unwrap_or(OrderTransport::Http);
match order_transport {
OrderTransport::Fix if !allow_trading => {
anyhow::bail!(
"`--order-transport=fix` (or DERIBIT_ORDER_TRANSPORT=fix) requires \
`--allow-trading` (or DERIBIT_ALLOW_TRADING=1) — without trading \
the FIX session would never be reached"
);
}
OrderTransport::Fix | OrderTransport::Http => {}
}
#[allow(clippy::unnecessary_lazy_evaluations)]
let log_format = args
.log_format()
.or_else(|| {
std::env::var("DERIBIT_LOG_FORMAT")
.ok()
.and_then(|v| match v.as_str() {
"text" => Some(LogFormat::Text),
"json" => Some(LogFormat::Json),
_ => None,
})
})
.unwrap_or_else(|| match transport {
Transport::Stdio => LogFormat::Text,
Transport::Http => LogFormat::Json,
});
Ok(Self {
endpoint,
client_id,
client_secret,
allow_trading,
max_order_usd,
transport,
http_listen,
http_bearer_token,
log_format,
order_transport,
})
}
}
#[derive(Debug, Parser)]
#[command(name = "deribit-mcp")]
#[command(about = "Model Context Protocol server for Deribit")]
#[command(version)]
struct Args {
#[arg(long, help = "Use testnet endpoint (default)")]
testnet: bool,
#[arg(long, help = "Use mainnet endpoint")]
mainnet: bool,
#[arg(long, help = "Client ID for OAuth")]
client_id: Option<String>,
#[arg(long, help = "Enable trading tools")]
allow_trading: bool,
#[arg(long, help = "Max order notional in USD")]
max_order_usd: Option<u64>,
#[arg(long, help = "Transport: stdio or http")]
transport: Option<String>,
#[arg(long, help = "HTTP listen address")]
listen: Option<SocketAddr>,
#[arg(long, help = "Log format: text or json")]
log_format: Option<String>,
#[arg(long, help = "Order transport: http or fix")]
order_transport: Option<String>,
#[arg(long, help = "Path to .env file")]
env_file: Option<PathBuf>,
}
impl Args {
fn parse() -> Self {
<Self as Parser>::parse()
}
fn endpoint(&self) -> Option<String> {
if self.mainnet {
Some("https://www.deribit.com".to_string())
} else if self.testnet {
Some("https://test.deribit.com".to_string())
} else {
None
}
}
fn transport(&self) -> Option<Transport> {
self.transport.as_ref().and_then(|t| match t.as_str() {
"stdio" => Some(Transport::Stdio),
"http" => Some(Transport::Http),
_ => None,
})
}
fn log_format(&self) -> Option<LogFormat> {
self.log_format.as_ref().and_then(|f| match f.as_str() {
"text" => Some(LogFormat::Text),
"json" => Some(LogFormat::Json),
_ => None,
})
}
fn order_transport(&self) -> Option<OrderTransport> {
self.order_transport
.as_ref()
.and_then(|t| OrderTransport::parse(t))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_format_matches_transport() {
let stdio_default = match Transport::Stdio {
Transport::Stdio => LogFormat::Text,
Transport::Http => LogFormat::Json,
};
let http_default = match Transport::Http {
Transport::Stdio => LogFormat::Text,
Transport::Http => LogFormat::Json,
};
assert_eq!(stdio_default, LogFormat::Text);
assert_eq!(http_default, LogFormat::Json);
}
fn fix_requires_trading_guard(
order_transport: OrderTransport,
allow_trading: bool,
) -> Result<(), &'static str> {
match order_transport {
OrderTransport::Fix if !allow_trading => Err(
"`--order-transport=fix` (or DERIBIT_ORDER_TRANSPORT=fix) requires `--allow-trading`",
),
OrderTransport::Fix | OrderTransport::Http => Ok(()),
}
}
#[test]
fn fix_without_allow_trading_is_rejected() {
assert!(fix_requires_trading_guard(OrderTransport::Fix, false).is_err());
}
#[test]
fn fix_with_allow_trading_is_accepted() {
fix_requires_trading_guard(OrderTransport::Fix, true).unwrap();
}
#[test]
fn http_default_does_not_require_trading() {
fix_requires_trading_guard(OrderTransport::Http, false).unwrap();
}
#[test]
fn network_env_var_resolves_to_endpoint() {
assert_eq!(
network_to_endpoint("testnet").as_deref(),
Some("https://test.deribit.com")
);
assert_eq!(
network_to_endpoint("MAINNET").as_deref(),
Some("https://www.deribit.com")
);
assert_eq!(
network_to_endpoint(" Test ").as_deref(),
Some("https://test.deribit.com")
);
assert_eq!(
network_to_endpoint("production").as_deref(),
Some("https://www.deribit.com")
);
assert_eq!(network_to_endpoint("staging"), None);
assert_eq!(network_to_endpoint(""), None);
}
#[test]
fn order_transport_parse_round_trip() {
assert_eq!(OrderTransport::parse("http"), Some(OrderTransport::Http));
assert_eq!(OrderTransport::parse("fix"), Some(OrderTransport::Fix));
assert_eq!(OrderTransport::parse("HTTP"), None);
assert_eq!(OrderTransport::parse(""), None);
assert_eq!(OrderTransport::parse("rest"), None);
}
}