1use clap::Parser;
10use std::net::SocketAddr;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone)]
15pub struct Config {
16 pub endpoint: String,
18 pub client_id: Option<String>,
20 pub client_secret: Option<String>,
22 pub allow_trading: bool,
24 pub max_order_usd: Option<u64>,
26 pub transport: Transport,
28 pub http_listen: SocketAddr,
30 pub http_bearer_token: Option<String>,
32 pub log_format: LogFormat,
34 pub order_transport: OrderTransport,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Transport {
42 Stdio,
44 Http,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum LogFormat {
51 Text,
53 Json,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum OrderTransport {
66 Http,
68 Fix,
70}
71
72fn network_to_endpoint(value: &str) -> Option<String> {
77 match value.trim().to_ascii_lowercase().as_str() {
78 "testnet" | "test" => Some("https://test.deribit.com".to_string()),
79 "mainnet" | "main" | "production" | "prod" => Some("https://www.deribit.com".to_string()),
80 _ => None,
81 }
82}
83
84impl OrderTransport {
85 fn parse(s: &str) -> Option<Self> {
89 match s {
90 "http" => Some(Self::Http),
91 "fix" => Some(Self::Fix),
92 _ => None,
93 }
94 }
95}
96
97impl Config {
98 pub fn load() -> anyhow::Result<Self> {
104 let args = Args::parse();
105
106 if let Some(ref env_file) = args.env_file {
108 dotenvy::from_path(env_file).ok(); } else if std::path::Path::new(".env").exists() {
110 dotenvy::dotenv().ok();
111 }
112
113 let endpoint = args
123 .endpoint()
124 .or_else(|| {
125 std::env::var("DERIBIT_NETWORK")
126 .ok()
127 .and_then(|v| network_to_endpoint(&v))
128 })
129 .or_else(|| std::env::var("DERIBIT_ENDPOINT").ok())
130 .unwrap_or_else(|| "https://test.deribit.com".to_string());
131
132 let client_id = args
133 .client_id
134 .clone()
135 .or_else(|| std::env::var("DERIBIT_CLIENT_ID").ok());
136
137 let client_secret = std::env::var("DERIBIT_CLIENT_SECRET").ok();
138
139 let allow_trading = args.allow_trading
140 || std::env::var("DERIBIT_ALLOW_TRADING")
141 .map(|v| v == "1")
142 .unwrap_or(false);
143
144 let max_order_usd = args.max_order_usd.or_else(|| {
145 std::env::var("DERIBIT_MAX_ORDER_USD")
146 .ok()
147 .and_then(|v| v.parse().ok())
148 });
149
150 let transport = args
151 .transport()
152 .or_else(|| {
153 std::env::var("DERIBIT_MCP_TRANSPORT")
154 .ok()
155 .and_then(|v| match v.as_str() {
156 "stdio" => Some(Transport::Stdio),
157 "http" => Some(Transport::Http),
158 _ => None,
159 })
160 })
161 .unwrap_or(Transport::Stdio);
162
163 let http_listen = args
164 .listen
165 .or_else(|| {
166 std::env::var("DERIBIT_HTTP_LISTEN")
167 .ok()
168 .and_then(|v| v.parse().ok())
169 })
170 .unwrap_or_else(|| {
171 "127.0.0.1:8723"
172 .parse()
173 .expect("invalid default listen addr")
174 });
175
176 let http_bearer_token = std::env::var("DERIBIT_HTTP_BEARER_TOKEN")
182 .ok()
183 .filter(|v| !v.is_empty());
184
185 let order_transport = args
186 .order_transport()
187 .or_else(|| {
188 std::env::var("DERIBIT_ORDER_TRANSPORT")
189 .ok()
190 .and_then(|v| OrderTransport::parse(&v))
191 })
192 .unwrap_or(OrderTransport::Http);
193
194 match order_transport {
199 OrderTransport::Fix if !allow_trading => {
200 anyhow::bail!(
201 "`--order-transport=fix` (or DERIBIT_ORDER_TRANSPORT=fix) requires \
202 `--allow-trading` (or DERIBIT_ALLOW_TRADING=1) — without trading \
203 the FIX session would never be reached"
204 );
205 }
206 OrderTransport::Fix | OrderTransport::Http => {}
207 }
208
209 #[allow(clippy::unnecessary_lazy_evaluations)]
210 let log_format = args
211 .log_format()
212 .or_else(|| {
213 std::env::var("DERIBIT_LOG_FORMAT")
214 .ok()
215 .and_then(|v| match v.as_str() {
216 "text" => Some(LogFormat::Text),
217 "json" => Some(LogFormat::Json),
218 _ => None,
219 })
220 })
221 .unwrap_or_else(|| match transport {
222 Transport::Stdio => LogFormat::Text,
223 Transport::Http => LogFormat::Json,
224 });
225
226 Ok(Self {
227 endpoint,
228 client_id,
229 client_secret,
230 allow_trading,
231 max_order_usd,
232 transport,
233 http_listen,
234 http_bearer_token,
235 log_format,
236 order_transport,
237 })
238 }
239}
240
241#[derive(Debug, Parser)]
243#[command(name = "deribit-mcp")]
244#[command(about = "Model Context Protocol server for Deribit")]
245#[command(version)]
246struct Args {
247 #[arg(long, help = "Use testnet endpoint (default)")]
249 testnet: bool,
250
251 #[arg(long, help = "Use mainnet endpoint")]
253 mainnet: bool,
254
255 #[arg(long, help = "Client ID for OAuth")]
257 client_id: Option<String>,
258
259 #[arg(long, help = "Enable trading tools")]
261 allow_trading: bool,
262
263 #[arg(long, help = "Max order notional in USD")]
265 max_order_usd: Option<u64>,
266
267 #[arg(long, help = "Transport: stdio or http")]
269 transport: Option<String>,
270
271 #[arg(long, help = "HTTP listen address")]
273 listen: Option<SocketAddr>,
274
275 #[arg(long, help = "Log format: text or json")]
277 log_format: Option<String>,
278
279 #[arg(long, help = "Order transport: http or fix")]
281 order_transport: Option<String>,
282
283 #[arg(long, help = "Path to .env file")]
285 env_file: Option<PathBuf>,
286}
287
288impl Args {
289 fn parse() -> Self {
291 <Self as Parser>::parse()
292 }
293
294 fn endpoint(&self) -> Option<String> {
296 if self.mainnet {
297 Some("https://www.deribit.com".to_string())
298 } else if self.testnet {
299 Some("https://test.deribit.com".to_string())
300 } else {
301 None
302 }
303 }
304
305 fn transport(&self) -> Option<Transport> {
307 self.transport.as_ref().and_then(|t| match t.as_str() {
308 "stdio" => Some(Transport::Stdio),
309 "http" => Some(Transport::Http),
310 _ => None,
311 })
312 }
313
314 fn log_format(&self) -> Option<LogFormat> {
316 self.log_format.as_ref().and_then(|f| match f.as_str() {
317 "text" => Some(LogFormat::Text),
318 "json" => Some(LogFormat::Json),
319 _ => None,
320 })
321 }
322
323 fn order_transport(&self) -> Option<OrderTransport> {
325 self.order_transport
326 .as_ref()
327 .and_then(|t| OrderTransport::parse(t))
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn log_format_matches_transport() {
337 let stdio_default = match Transport::Stdio {
339 Transport::Stdio => LogFormat::Text,
340 Transport::Http => LogFormat::Json,
341 };
342 let http_default = match Transport::Http {
343 Transport::Stdio => LogFormat::Text,
344 Transport::Http => LogFormat::Json,
345 };
346 assert_eq!(stdio_default, LogFormat::Text);
347 assert_eq!(http_default, LogFormat::Json);
348 }
349
350 fn fix_requires_trading_guard(
357 order_transport: OrderTransport,
358 allow_trading: bool,
359 ) -> Result<(), &'static str> {
360 match order_transport {
361 OrderTransport::Fix if !allow_trading => Err(
362 "`--order-transport=fix` (or DERIBIT_ORDER_TRANSPORT=fix) requires `--allow-trading`",
363 ),
364 OrderTransport::Fix | OrderTransport::Http => Ok(()),
365 }
366 }
367
368 #[test]
369 fn fix_without_allow_trading_is_rejected() {
370 assert!(fix_requires_trading_guard(OrderTransport::Fix, false).is_err());
371 }
372
373 #[test]
374 fn fix_with_allow_trading_is_accepted() {
375 fix_requires_trading_guard(OrderTransport::Fix, true).unwrap();
376 }
377
378 #[test]
379 fn http_default_does_not_require_trading() {
380 fix_requires_trading_guard(OrderTransport::Http, false).unwrap();
381 }
382
383 #[test]
384 fn network_env_var_resolves_to_endpoint() {
385 assert_eq!(
386 network_to_endpoint("testnet").as_deref(),
387 Some("https://test.deribit.com")
388 );
389 assert_eq!(
390 network_to_endpoint("MAINNET").as_deref(),
391 Some("https://www.deribit.com")
392 );
393 assert_eq!(
394 network_to_endpoint(" Test ").as_deref(),
395 Some("https://test.deribit.com")
396 );
397 assert_eq!(
398 network_to_endpoint("production").as_deref(),
399 Some("https://www.deribit.com")
400 );
401 assert_eq!(network_to_endpoint("staging"), None);
402 assert_eq!(network_to_endpoint(""), None);
403 }
404
405 #[test]
406 fn order_transport_parse_round_trip() {
407 assert_eq!(OrderTransport::parse("http"), Some(OrderTransport::Http));
408 assert_eq!(OrderTransport::parse("fix"), Some(OrderTransport::Fix));
409 assert_eq!(OrderTransport::parse("HTTP"), None);
410 assert_eq!(OrderTransport::parse(""), None);
411 assert_eq!(OrderTransport::parse("rest"), None);
412 }
413}