coinex-mcp-server 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP server for accessing CoinEx cryptocurrency exchange
4
+
5
+ Notes:
6
+ - In HTTP/SSE mode, only tags={"public"} query tools are exposed by default; enable --enable-http-auth to expose tags={"auth"} tools.
7
+ - When accessing account/trading tools (tags={"auth"}), provide in request headers:
8
+ - X-CoinEx-Access-Id: Your API Key
9
+ - X-CoinEx-Secret-Key: Your API Secret
10
+ - All tools return following CoinEx style: { code: int, message: str, data: any }
11
+ """
12
+
13
+ import sys
14
+ import logging
15
+ from typing import Any, Annotated, Literal
16
+ from pydantic import Field, validate_call
17
+
18
+ from fastmcp import FastMCP
19
+ from fastmcp.server.auth import StaticTokenVerifier
20
+ from fastmcp.server.dependencies import get_http_headers
21
+ from .coinex_client import CoinExClient, validate_environment
22
+ import os
23
+ import argparse
24
+
25
+ # Load .env (won't override externally set environment variables)
26
+ try:
27
+ from dotenv import load_dotenv, find_dotenv
28
+ # When started via MCP Inspector, CWD is usually set to project root; find_dotenv is more robust
29
+ env_path = find_dotenv(usecwd=True)
30
+ if env_path:
31
+ load_dotenv(env_path, override=False)
32
+ print(f"Loaded environment variables from {env_path}", file=sys.stderr)
33
+ else:
34
+ print(".env not found, continuing with system environment variables", file=sys.stderr)
35
+ except Exception as e:
36
+ # Give hint when python-dotenv is not installed or other exceptions occur, but don't block service
37
+ print(f"Failed to load .env: {e} (you can run pip install python-dotenv)", file=sys.stderr)
38
+
39
+
40
+ # Enum field descriptions (for reuse across tool functions)
41
+ MARKET_TYPE_DESC = "Market type: spot|futures|margin; default spot"
42
+ ORDER_SIDE_DESC = "Order side: buy|sell"
43
+ ORDER_STATUS_DESC = "Order status: pending|finished"
44
+
45
+ # Initialize FastMCP server
46
+ mcp = FastMCP("coinex-mcp-server")
47
+
48
+ # Delayed initialization: decide whether to allow reading credentials from environment based on transport and auth mode
49
+ coinex_client: CoinExClient | None = None
50
+ is_http_like: bool = False
51
+
52
+
53
+ def get_secret_client() -> CoinExClient:
54
+ """Get authenticated client based on current transport mode.
55
+
56
+ - HTTP/SSE mode: Extract credentials from request headers
57
+ - stdio mode: Use global coinex_client with environment credentials
58
+
59
+ Returns: CoinExClient configured with appropriate credentials.
60
+ """
61
+ if is_http_like:
62
+ # HTTP/SSE mode - extract credentials from request headers
63
+ headers = get_http_headers() # lowercase dictionary
64
+ access_id = headers.get("x-coinex-access-id")
65
+ secret_key = headers.get("x-coinex-secret-key")
66
+
67
+ if not access_id or not secret_key:
68
+ raise ValueError(
69
+ "Request headers must include X-CoinEx-Access-Id and X-CoinEx-Secret-Key to access account/trading interfaces"
70
+ )
71
+
72
+ return CoinExClient(access_id=access_id, secret_key=secret_key, enable_env_credentials=False)
73
+ else:
74
+ # stdio mode - use global client with environment credentials
75
+ if coinex_client is None:
76
+ raise ValueError("CoinEx client not initialized")
77
+
78
+ # Verify global client has credentials
79
+ if not hasattr(coinex_client, 'access_id') or not coinex_client.access_id:
80
+ raise ValueError(
81
+ "CoinEx API credentials not found. Please set COINEX_ACCESS_ID and COINEX_SECRET_KEY environment variables"
82
+ )
83
+
84
+ return coinex_client
85
+
86
+
87
+ # =====================
88
+ # Public Market Queries (spot/futures)
89
+ # =====================
90
+ @mcp.tool(tags={"public"})
91
+ @validate_call
92
+ async def get_ticker(
93
+ base: Annotated[str | None, Field(description="Base currency, e.g. BTC, ETH; returns top 5 when empty")] = None,
94
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
95
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
96
+ ) -> dict[str, Any]:
97
+ """Get trading pair's recent price, 24h price and volume information (spot).
98
+
99
+ Parameters:
100
+ - base: Optional, base currency like "BTC", "ETH". When not provided, returns top 5 entries.
101
+ - quote: Optional, quote currency, default "USDT".
102
+
103
+ Returns: {code, message, data}; when base is not provided, only returns top 5 items.
104
+ """
105
+ api_result = await coinex_client.get_tickers(base, quote, market_type)
106
+
107
+ if api_result.get('code') != 0 or 'data' not in api_result:
108
+ logging.error(f"get_ticker error, code:{api_result.get('code')}, message:{api_result.get('message')}")
109
+ return api_result
110
+
111
+ data = api_result['data']
112
+ if not base and isinstance(data, list):
113
+ api_result['data'] = data[:5]
114
+ return api_result
115
+
116
+
117
+ @mcp.tool(tags={"public"})
118
+ @validate_call
119
+ async def get_orderbook(
120
+ base: Annotated[str, Field(description="Required, base currency, e.g. BTC, ETH")],
121
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
122
+ limit: Annotated[int | None, Field(description="Number of price levels to return, options: 5/10/20/50; default 20")] = 20,
123
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
124
+ interval: Annotated[str | None, Field(description="Merge granularity, default 0; values according to official documentation")] = "0",
125
+ ) -> dict[str, Any]:
126
+ """Get order book (depth) information (supports spot/futures).
127
+
128
+ Parameters:
129
+ - base: Required, base currency. Example: "BTC", "ETH".
130
+ - quote: Optional, quote currency, default "USDT".
131
+ - limit: Optional, number of price levels to return, default 20; valid values: [5, 10, 20, 50].
132
+ - market_type: Optional, market type, default "spot"; valid values: "spot" | "futures".
133
+ - interval: Optional, merge granularity, default "0"; valid values include "0", "0.00000001", ..., "1", "10", "100", "1000" (according to official documentation).
134
+
135
+ Returns: {code, message, data}.
136
+ """
137
+ api_result = await coinex_client.get_depth(base, quote, market_type, limit or 20, interval or "0")
138
+
139
+ if api_result.get('code') != 0 or 'data' not in api_result:
140
+ logging.error(f"get_depth error, code:{api_result.get('code')}, message:{api_result.get('message')}")
141
+ return api_result
142
+
143
+
144
+ @mcp.tool(tags={"public"})
145
+ @validate_call
146
+ async def get_kline(
147
+ base: Annotated[str, Field(description="Required, base currency, e.g. BTC, ETH")],
148
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
149
+ period: Annotated[
150
+ Literal["1min", "5min", "15min", "30min", "1hour", "4hour", "1day", "1week"],
151
+ Field(description="K-line period; default 1hour; supports 1min/5min/15min/30min/1hour/4hour/1day/1week")
152
+ ] = "1hour",
153
+ limit: Annotated[int | None, Field(description="Number of records to return; default 100")] = 100,
154
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
155
+ ) -> dict[str, Any]:
156
+ """Get K-line data (supports spot/futures).
157
+
158
+ Parameters:
159
+ - base: Required, base currency. Example: "BTC", "ETH".
160
+ - quote: Optional, quote currency, default "USDT".
161
+ - period: Optional, K-line period, default "1hour";
162
+ - Spot/futures common period whitelist: "1min","5min","15min","30min","1hour","4hour","1day","1week".
163
+ - limit: Optional, number of records, default 100.
164
+ - market_type: Optional, market type, default "spot"; options: "spot" | "futures".
165
+
166
+ Error: When period is not in whitelist, returns {code:-1, message:"Unsupported time period"}.
167
+ Returns: {code, message, data}.
168
+ """
169
+ valid_periods = ["1min", "5min", "15min", "30min", "1hour", "4hour", "1day", "1week"]
170
+
171
+ if period not in valid_periods:
172
+ return {"code": -1, "message": f"Unsupported time period: {period}."}
173
+
174
+ api_result = await coinex_client.get_kline(str(period), base, quote, market_type, limit)
175
+
176
+ if api_result.get('code') != 0 or 'data' not in api_result:
177
+ logging.error(f"get_kline error, code:{api_result.get('code')}, message:{api_result.get('message')}")
178
+ return api_result
179
+
180
+
181
+ # ===============
182
+ # Additional Public Tools
183
+ # ===============
184
+
185
+ @mcp.tool(tags={"public"})
186
+ @validate_call
187
+ async def list_markets(
188
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
189
+ base: Annotated[str | None, Field(description="Optional; base currency to filter")] = None,
190
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
191
+ ) -> dict[str, Any]:
192
+ """List market status (spot/futures).
193
+
194
+ Parameters:
195
+ - market_type: Optional, default "spot"; options: "spot" | "futures".
196
+ - base: Optional, base currency to filter.
197
+ - quote: Optional, quote currency, default "USDT".
198
+
199
+ Returns: {code, message, data} (list).
200
+ """
201
+ api_result = await coinex_client.get_market_info(base, quote, market_type)
202
+ if api_result.get('code') != 0 or 'data' not in api_result:
203
+ logging.error(f"list_markets error, code:{api_result.get('code')}, message:{api_result.get('message')}")
204
+ return api_result
205
+
206
+
207
+ @mcp.tool(tags={"public"})
208
+ @validate_call
209
+ async def get_deals(
210
+ base: Annotated[str, Field(description="Required, base currency, e.g. BTC, ETH")],
211
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
212
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
213
+ limit: Annotated[int | None, Field(description="Return quantity, default 100, max 1000 (per official docs)")] = 100,
214
+ ) -> dict[str, Any]:
215
+ """Get recent trades (deals).
216
+
217
+ Parameters:
218
+ - base: Required, base currency.
219
+ - quote: Optional, quote currency, default "USDT".
220
+ - market_type: Optional, default "spot"; options: "spot" | "futures".
221
+ - limit: Optional, return quantity, default 100, max 1000 (per official documentation).
222
+
223
+ Returns: {code, message, data} (list).
224
+ """
225
+ api_result = await coinex_client.get_deal(base, quote, market_type, limit)
226
+
227
+ if api_result.get('code') != 0 or 'data' not in api_result:
228
+ logging.error(f"get_deals error, code:{api_result.get('code')}, message:{api_result.get('message')}")
229
+ return api_result
230
+
231
+
232
+ @mcp.tool(tags={"public"})
233
+ @validate_call
234
+ async def get_index_price(
235
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
236
+ base: Annotated[str | None, Field(description="Optional; base currency, returns multi-market index if not provided")] = None,
237
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
238
+ top_n: Annotated[int | None, Field(description="Return top N entries when base not provided; default 5")] = 5,
239
+ ) -> dict[str, Any]:
240
+ """Get market index price (spot/futures). Supports batch; returns top N entries when base not provided.
241
+
242
+ Parameters:
243
+ - market_type: Optional, default "spot"; options: "spot" | "futures".
244
+ - base: Optional, base currency; returns multi-market index when not provided.
245
+ - quote: Optional, quote currency, default "USDT".
246
+ - top_n: Optional, only effective when base not provided; default 5.
247
+
248
+ Returns: {code, message, data}.
249
+ """
250
+ api_result = await coinex_client.get_index_price(base, quote, market_type)
251
+
252
+ if api_result.get('code') != 0 or 'data' not in api_result:
253
+ logging.error(f"get_index_price error, code:{api_result.get('code')}, message:{api_result.get('message')}")
254
+ return api_result
255
+
256
+ if not base and isinstance(api_result.get('data'), list) and top_n:
257
+ api_result['data'] = api_result['data'][:top_n]
258
+ return api_result
259
+
260
+
261
+ # ====== Futures-Specific ======
262
+
263
+ @mcp.tool(tags={"public"})
264
+ async def get_funding_rate(
265
+ base: Annotated[str, Field(description="Required, futures base currency, e.g. BTC, ETH")],
266
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT"
267
+ ) -> dict[str, Any]:
268
+ """Get current funding rate (futures only).
269
+
270
+ Parameters:
271
+ - base: Required, futures base currency, e.g. "BTC", "ETH".
272
+ - quote: Optional, quote currency, default "USDT".
273
+
274
+ Returns: {code, message, data}.
275
+ """
276
+ api_result = await coinex_client.futures_get_funding_rate(base, quote)
277
+ if api_result.get('code') != 0 or 'data' not in api_result:
278
+ logging.error(f"get_funding_rate error, code:{api_result.get('code')}, message:{api_result.get('message')}")
279
+ return api_result
280
+
281
+
282
+ @mcp.tool(tags={"public"})
283
+ async def get_funding_rate_history(
284
+ base: Annotated[str, Field(description="Required, futures base currency, e.g. BTC, ETH")],
285
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
286
+ start_time: Annotated[int | None, Field(description="Start timestamp (milliseconds)")] = None,
287
+ end_time: Annotated[int | None, Field(description="End timestamp (milliseconds)")] = None,
288
+ page: Annotated[int | None, Field(description="Page number; default 1")] = 1,
289
+ limit: Annotated[int | None, Field(description="Number of records; default 100")] = 100,
290
+ ) -> dict[str, Any]:
291
+ """Get funding rate history (futures only).
292
+
293
+ Parameters:
294
+ - base: Required, futures base currency.
295
+ - quote: Optional, quote currency, default "USDT".
296
+ - start_time: Optional, start timestamp (milliseconds).
297
+ - end_time: Optional, end timestamp (milliseconds).
298
+ - page: Optional, default 1.
299
+ - limit: Optional, default 100.
300
+
301
+ Returns: {code, message, data}.
302
+ """
303
+ api_result = await coinex_client.futures_get_funding_rate_history(base, quote, start_time, end_time, page, limit)
304
+ if api_result.get('code') != 0 or 'data' not in api_result:
305
+ logging.error(f"get_funding_rate_history error, code:{api_result.get('code')}, message:{api_result.get('message')}")
306
+ return api_result
307
+
308
+
309
+ @mcp.tool(tags={"public"})
310
+ async def get_premium_index_history(
311
+ base: Annotated[str, Field(description="Required, futures base currency")],
312
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
313
+ start_time: Annotated[int | None, Field(description="Start timestamp (milliseconds)")] = None,
314
+ end_time: Annotated[int | None, Field(description="End timestamp (milliseconds)")] = None,
315
+ page: Annotated[int | None, Field(description="Page number; default 1")] = 1,
316
+ limit: Annotated[int | None, Field(description="Number of records; default 100")] = 100,
317
+ ) -> dict[str, Any]:
318
+ """Get premium index history (futures only).
319
+
320
+ Parameters:
321
+ - base: Required, futures base currency.
322
+ - quote: Optional, quote currency, default "USDT".
323
+ - start_time: Optional, start timestamp (milliseconds).
324
+ - end_time: Optional, end timestamp (milliseconds).
325
+ - page: Optional, default 1.
326
+ - limit: Optional, default 100.
327
+
328
+ Returns: {code, message, data}.
329
+ """
330
+ api_result = await coinex_client.futures_get_premium_history(base, quote, start_time, end_time, page, limit)
331
+ if api_result.get('code') != 0 or 'data' not in api_result:
332
+ logging.error(f"get_premium_index_history error, code:{api_result.get('code')}, message:{api_result.get('message')}")
333
+ return api_result
334
+
335
+
336
+ @mcp.tool(tags={"public"})
337
+ async def get_basis_history(
338
+ base: Annotated[str, Field(description="Required, futures base currency")],
339
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
340
+ start_time: Annotated[int | None, Field(description="Start timestamp (milliseconds)")] = None,
341
+ end_time: Annotated[int | None, Field(description="End timestamp (milliseconds)")] = None,
342
+ page: Annotated[int | None, Field(description="Placeholder parameter (currently unused)")] = 1,
343
+ limit: Annotated[int | None, Field(description="Placeholder parameter (currently unused)")] = 100,
344
+ ) -> dict[str, Any]:
345
+ """Get basis history (futures only).
346
+
347
+ Parameters:
348
+ - base: Required, futures base currency.
349
+ - quote: Optional, quote currency, default "USDT".
350
+ - start_time: Optional, start timestamp (milliseconds).
351
+ - end_time: Optional, end timestamp (milliseconds).
352
+ - page: Optional, placeholder parameter (currently unused by client).
353
+ - limit: Optional, placeholder parameter (currently unused by client).
354
+
355
+ Returns: {code, message, data}.
356
+ """
357
+ api_result = await coinex_client.futures_basis_index_history(base, quote, start_time, end_time, page, limit)
358
+ if api_result.get('code') != 0 or 'data' not in api_result:
359
+ logging.error(f"get_basis_history error, code:{api_result.get('code')}, message:{api_result.get('message')}")
360
+ return api_result
361
+
362
+
363
+ @mcp.tool(tags={"public"})
364
+ async def get_margin_tiers(
365
+ base: Annotated[str, Field(description="Required, futures base currency")],
366
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT"
367
+ ) -> dict[str, Any]:
368
+ """Get margin tiers/ position levels (futures only).
369
+
370
+ Parameters:
371
+ - base: Required, futures base currency.
372
+ - quote: Optional, quote currency, default "USDT".
373
+
374
+ Returns: {code, message, data}.
375
+ """
376
+ api_result = await coinex_client.futures_get_position_level(base, quote)
377
+ if api_result.get('code') != 0 or 'data' not in api_result:
378
+ logging.error(f"get_position_tiers error, code:{api_result.get('code')}, message:{api_result.get('message')}")
379
+ return api_result
380
+
381
+
382
+ @mcp.tool(tags={"public"})
383
+ async def get_liquidation_history(
384
+ base: Annotated[str, Field(description="Required, futures base currency, e.g. BTC, ETH")],
385
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
386
+ start_time: Annotated[int | None, Field(description="Start timestamp (milliseconds)")] = None,
387
+ end_time: Annotated[int | None, Field(description="End timestamp (milliseconds)")] = None,
388
+ page: Annotated[int | None, Field(description="Page number; default 1")] = 1,
389
+ limit: Annotated[int | None, Field(description="Number of records; default 100")] = 100,
390
+ ) -> dict[str, Any]:
391
+ """Get liquidation history (futures only).
392
+
393
+ Parameters:
394
+ - base: Required, futures base currency.
395
+ - quote: Optional, quote currency, default "USDT".
396
+ - start_time: Optional, start timestamp (milliseconds).
397
+ - end_time: Optional, end timestamp (milliseconds).
398
+ - page: Optional, page number, default 1.
399
+ - limit: Optional, number of records, default 100.
400
+
401
+ Returns: {code, message, data}.
402
+ """
403
+ # Note: This function doesn't exist in the new API, return a placeholder response
404
+ return {"code": -1, "message": "Liquidation history not available in current API version", "data": []}
405
+
406
+
407
+ @mcp.tool(tags={"auth"})
408
+ async def get_account_balance() -> dict[str, Any]:
409
+ """Get account balance information (requires authentication).
410
+
411
+ Usage (HTTP/SSE): Include in request headers:
412
+ - X-CoinEx-Access-Id
413
+ - X-CoinEx-Secret-Key
414
+
415
+ Returns: {code, message, data}.
416
+ """
417
+ client = get_secret_client()
418
+ api_result = await client.get_balances(CoinExClient.MarketType.SPOT)
419
+
420
+ if api_result.get('code') != 0 or 'data' not in api_result:
421
+ logging.error(f"get_balances error, code:{api_result.get('code')}, message:{api_result.get('message')}")
422
+ return api_result
423
+
424
+
425
+ @mcp.tool(tags={"auth"})
426
+ @validate_call
427
+ async def place_order(
428
+ base: Annotated[str, Field(description="Required, base currency, e.g. BTC, ETH")],
429
+ side: Annotated[CoinExClient.OrderSide, Field(description=ORDER_SIDE_DESC)],
430
+ amount: Annotated[str, Field(description="Required, order quantity (string), must meet precision and minimum volume requirements")],
431
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
432
+ price: Annotated[str | None, Field(description="Optional; if provided, creates a limit order, otherwise market order")] = None,
433
+ is_hide: Annotated[bool | None, Field(description="Optional, whether to place a hidden order")] = None,
434
+ client_id: Annotated[str | None, Field(description="Optional, custom order ID")] = None,
435
+ trigger_price: Annotated[str | None, Field(description="Optional, if provided, creates a stop order based on it")] = None,
436
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
437
+ ) -> dict[str, Any]:
438
+ """Place trading order (requires authentication, real funds).
439
+ Strong reminder: This is a real money trading operation. Please confirm with end user again before calling!
440
+
441
+ Parameters:
442
+ - base: Required, base currency.
443
+ - side: Required, direction: buy|sell.
444
+ - amount: Required, order quantity (string).
445
+ - quote: Optional, quote currency, default "USDT".
446
+ - price: Optional; if provided, creates limit order, otherwise market order.
447
+ - is_hide: Optional, whether to place a hidden order.
448
+ - client_id: Optional, custom order ID.
449
+ - trigger_price: Optional, if provided, creates a stop order.
450
+ - market_type: Optional, market type, default "spot".
451
+
452
+ Returns: {code, message, data}. Logs error on failure.
453
+ """
454
+ client = get_secret_client()
455
+
456
+ api_result = await client.place_order(
457
+ side=side,
458
+ base=base,
459
+ quote=quote,
460
+ amount=amount,
461
+ market_type=market_type,
462
+ price=price,
463
+ is_hide=is_hide,
464
+ client_id=client_id,
465
+ trigger_price=trigger_price
466
+ )
467
+
468
+ if api_result.get('code') != 0 or 'data' not in api_result:
469
+ logging.error(f"place_order error, code:{api_result.get('code')}, message:{api_result.get('message')}")
470
+ return api_result
471
+
472
+
473
+ @mcp.tool(tags={"auth"})
474
+ @validate_call
475
+ async def cancel_order(
476
+ base: Annotated[str, Field(description="Required, base currency, e.g. BTC, ETH")],
477
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
478
+ order_id: Annotated[int, Field(description="Optional, order ID to cancel, if empty, cancels all orders")] = None,
479
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
480
+ ) -> dict[str, Any]:
481
+ """Cancel order (requires authentication).
482
+
483
+ Parameters:
484
+ - base: Required, base currency.
485
+ - order_id: Optional, order ID to cancel, if empty, cancels all orders.
486
+ - quote: Optional, quote currency, default "USDT".
487
+ - market_type: Optional, market type, default "spot".
488
+
489
+ Returns: {code, message, data}.
490
+ """
491
+ client = get_secret_client()
492
+ api_result = await client.cancel_order(base, quote, market_type, order_id)
493
+
494
+ if api_result.get('code') != 0 or 'data' not in api_result:
495
+ logging.error(f"cancel_order error, code:{api_result.get('code')}, message:{api_result.get('message')}")
496
+ return api_result
497
+
498
+
499
+ @mcp.tool(tags={"auth"})
500
+ @validate_call
501
+ async def get_order_history(
502
+ base: Annotated[str | None, Field(description="Optional, base currency; query all markets if empty")] = None,
503
+ quote: Annotated[str, Field(description="Quote currency, default USDT")] = "USDT",
504
+ side: Annotated[CoinExClient.OrderSide | None, Field(description=ORDER_SIDE_DESC)] = None,
505
+ status: Annotated[CoinExClient.OrderStatus, Field(description=ORDER_STATUS_DESC)] = CoinExClient.OrderStatus.FINISHED,
506
+ is_stop: Annotated[bool | None, Field(description="Optional, whether to query stop orders; default False")] = False,
507
+ page: Annotated[int | None, Field(description="Optional, page number; default 1")] = 1,
508
+ limit: Annotated[int | None, Field(description="Optional, total return limit; default 100")] = 100,
509
+ market_type: Annotated[CoinExClient.MarketType, Field(description=MARKET_TYPE_DESC)] = CoinExClient.MarketType.SPOT,
510
+ ) -> dict[str, Any]:
511
+ """Get order history (requires authentication).
512
+
513
+ Description: Returns list of orders; limit applies to total merged count.
514
+
515
+ Parameters:
516
+ - base: Optional, base currency; queries all markets if empty.
517
+ - quote: Optional, quote currency, default "USDT".
518
+ - side: Optional, order side.
519
+ - status: Optional, order status, pending(open) or finished, default 'finished'.
520
+ - is_stop: Optional, whether to query stop orders, default False.
521
+ - page: Optional, page number, default 1.
522
+ - limit: Optional, default 100.
523
+ - market_type: Optional, market type, default "spot".
524
+
525
+ Returns: {code, message, data}, sorted by time descending (determined by server/API).
526
+ """
527
+ client = get_secret_client()
528
+
529
+ api_result = await client.get_orders(
530
+ base, quote, market_type, side, status, is_stop, page, limit
531
+ )
532
+ return api_result
533
+
534
+
535
+ if __name__ == "__main__":
536
+ parser = argparse.ArgumentParser(description="CoinEx FastMCP server startup parameters")
537
+ parser.add_argument(
538
+ "--transport",
539
+ choices=["stdio", "http", "streamable-http", "sse"],
540
+ default="stdio",
541
+ help="Transport protocol: stdio(default) | http(equivalent to streamable-http) | streamable-http | sse",
542
+ )
543
+ parser.add_argument("--host", default=None, help="HTTP service bind address (only valid in http/streamable-http mode)")
544
+ parser.add_argument("--port", type=int, default=None, help="HTTP service port (only valid in http/streamable-http mode)")
545
+ parser.add_argument(
546
+ "--path",
547
+ default=None,
548
+ help="Endpoint path: /mcp path in http/streamable-http mode; mount path in sse mode",
549
+ )
550
+ parser.add_argument(
551
+ "--enable-http-auth",
552
+ action="store_true",
553
+ help="Enable HTTP-based authentication and sensitive tools (default off, only exposes query tools)",
554
+ )
555
+ # Added: Worker processes and port reuse (only valid in HTTP/SSE mode)
556
+ parser.add_argument(
557
+ "--workers",
558
+ type=int,
559
+ default=None,
560
+ help="Number of worker processes in HTTP/SSE mode (managed by underlying uvicorn)",
561
+ )
562
+ parser.add_argument(
563
+ "--reuse-port",
564
+ action="store_true",
565
+ help="Enable SO_REUSEPORT for multi-process (use with caution, only when multiple independent processes need to share same port)",
566
+ )
567
+ args = parser.parse_args()
568
+
569
+ # Compatible with common "http" notation in documentation
570
+ transport = args.transport
571
+ if transport == "http":
572
+ transport = "streamable-http"
573
+
574
+ # Calculate HTTP auth switch: default off; only enable when explicitly turned on via command line or environment variable
575
+ env_http_auth_enabled = os.getenv("HTTP_AUTH_ENABLED", "false").lower() in ("1", "true", "yes", "on")
576
+ http_auth_enabled = args.enable_http_auth or env_http_auth_enabled
577
+
578
+ # Apply switch under HTTP/SSE transport
579
+ is_http_like = transport in ("streamable-http", "sse")
580
+
581
+ # Initialize client for public data access based on mode (will not carry credentials)
582
+ if is_http_like:
583
+ # Disable environment credential fallback in any HTTP/SSE mode
584
+ coinex_client = CoinExClient(enable_env_credentials=False)
585
+ print("HTTP/SSE mode: Environment credential fallback disabled.", file=sys.stderr)
586
+ else:
587
+ # Only non-HTTP mode allows loading from environment (common scenario for local stdio development/self-hosting)
588
+ coinex_client = CoinExClient(enable_env_credentials=True)
589
+ has_credentials = validate_environment()
590
+ if not has_credentials:
591
+ print("Error: CoinEx API credentials not found, some features will be unavailable", file=sys.stderr)
592
+
593
+ if is_http_like:
594
+ if not http_auth_enabled:
595
+ # Only expose public tag tools
596
+ mcp.include_tags = {"public"}
597
+ print("HTTP authentication disabled: Only exposing query tools (public)", file=sys.stderr)
598
+ else:
599
+ # Optional: Enable Bearer authentication based on environment variables (static Token)
600
+ API_TOKEN = os.getenv("API_TOKEN")
601
+ if API_TOKEN:
602
+ scopes_env = os.getenv("API_SCOPES", "").replace(",", " ").split()
603
+ mcp.auth = StaticTokenVerifier(
604
+ tokens={
605
+ API_TOKEN: {
606
+ "client_id": "api-token",
607
+ "scopes": scopes_env,
608
+ }
609
+ },
610
+ required_scopes=scopes_env or None,
611
+ )
612
+ print("Bearer authentication enabled (API_TOKEN)", file=sys.stderr)
613
+
614
+ # Assemble uvicorn configuration (only effective in HTTP/SSE mode)
615
+ uvicorn_config = None
616
+ if is_http_like:
617
+ uvicorn_config = {}
618
+ if args.workers is not None:
619
+ uvicorn_config["workers"] = args.workers
620
+ if args.reuse_port:
621
+ uvicorn_config["reuse_port"] = True
622
+ # If finally empty, change to None to avoid passing empty configuration
623
+ if not uvicorn_config:
624
+ uvicorn_config = None
625
+
626
+ # Start (2.x: only pass HTTP params for HTTP transports)
627
+ if transport == "stdio":
628
+ mcp.run(transport=transport)
629
+ else:
630
+ mcp.run(
631
+ transport=transport,
632
+ host=args.host,
633
+ port=args.port,
634
+ path=args.path,
635
+ # Only pass when configured, avoid affecting stdio
636
+ **({"uvicorn_config": uvicorn_config} if uvicorn_config is not None else {})
637
+ )