from __future__ import annotations import os from fastapi import Depends, HTTPException from mcp_common.auth import Principal, TokenStore, require_principal from mcp_common.environment import EnvironmentInfo from mcp_common.mcp_bridge import mount_mcp_endpoint from mcp_common.server import build_app from pydantic import BaseModel from mcp_bybit.client import BybitClient from mcp_bybit.leverage_cap import enforce_leverage as _enforce_leverage from mcp_bybit.leverage_cap import get_max_leverage # --- Body models: reads --- class TickerReq(BaseModel): symbol: str category: str = "linear" class TickerBatchReq(BaseModel): symbols: list[str] category: str = "linear" class OrderbookReq(BaseModel): symbol: str category: str = "linear" limit: int = 50 class HistoricalReq(BaseModel): symbol: str category: str = "linear" interval: str = "60" start: int | None = None end: int | None = None limit: int = 1000 class IndicatorsReq(BaseModel): symbol: str category: str = "linear" indicators: list[str] = ["rsi", "atr", "macd", "adx"] interval: str = "60" start: int | None = None end: int | None = None class FundingRateReq(BaseModel): symbol: str category: str = "linear" class FundingHistoryReq(BaseModel): symbol: str category: str = "linear" limit: int = 100 class OpenInterestReq(BaseModel): symbol: str category: str = "linear" interval: str = "5min" limit: int = 288 class InstrumentsReq(BaseModel): category: str = "linear" symbol: str | None = None class OptionChainReq(BaseModel): base_coin: str expiry: str | None = None class PositionsReq(BaseModel): category: str = "linear" class AccountSummaryReq(BaseModel): pass class TradeHistoryReq(BaseModel): category: str = "linear" limit: int = 50 class OpenOrdersReq(BaseModel): category: str = "linear" symbol: str | None = None class BasisSpotPerpReq(BaseModel): asset: str # --- Body models: writes --- class PlaceOrderReq(BaseModel): category: str symbol: str side: str qty: float order_type: str = "Limit" price: float | None = None tif: str = "GTC" reduce_only: bool = False position_idx: int | None = None class AmendOrderReq(BaseModel): category: str symbol: str order_id: str new_qty: float | None = None new_price: float | None = None class CancelOrderReq(BaseModel): category: str symbol: str order_id: str class CancelAllReq(BaseModel): category: str symbol: str | None = None class SetStopLossReq(BaseModel): category: str symbol: str stop_loss: float position_idx: int = 0 class SetTakeProfitReq(BaseModel): category: str symbol: str take_profit: float position_idx: int = 0 class ClosePositionReq(BaseModel): category: str symbol: str class SetLeverageReq(BaseModel): category: str symbol: str leverage: int class SwitchModeReq(BaseModel): category: str symbol: str mode: str class TransferReq(BaseModel): coin: str amount: float from_type: str to_type: str # --- ACL helper --- def _check(principal: Principal, *, core: bool = False, observer: bool = False) -> None: allowed: set[str] = set() if core: allowed.add("core") if observer: allowed.add("observer") if not (principal.capabilities & allowed): raise HTTPException(status_code=403, detail="forbidden") def create_app( *, client: BybitClient, token_store: TokenStore, creds: dict | None = None, env_info: "EnvironmentInfo | None" = None, ): creds = creds or {} app = build_app(name="mcp-bybit", version="0.1.0", token_store=token_store) # ── Reads ────────────────────────────────────────────── @app.post("/tools/environment_info", tags=["reads"]) async def t_environment_info(principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) if env_info is None: return { "exchange": "bybit", "environment": "testnet" if client.testnet else "mainnet", "source": "credentials", "env_value": None, "base_url": getattr(client, "base_url", None), "max_leverage": get_max_leverage(creds), } return { "exchange": env_info.exchange, "environment": env_info.environment, "source": env_info.source, "env_value": env_info.env_value, "base_url": env_info.base_url, "max_leverage": get_max_leverage(creds), } @app.post("/tools/get_ticker", tags=["reads"]) async def t_get_ticker(body: TickerReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_ticker(body.symbol, body.category) @app.post("/tools/get_ticker_batch", tags=["reads"]) async def t_get_ticker_batch(body: TickerBatchReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_ticker_batch(body.symbols, body.category) @app.post("/tools/get_orderbook", tags=["reads"]) async def t_get_orderbook(body: OrderbookReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_orderbook(body.symbol, body.category, body.limit) @app.post("/tools/get_historical", tags=["reads"]) async def t_get_historical(body: HistoricalReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_historical( body.symbol, body.category, body.interval, body.start, body.end, body.limit, ) @app.post("/tools/get_indicators", tags=["reads"]) async def t_get_indicators(body: IndicatorsReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_indicators( body.symbol, body.category, body.indicators, body.interval, body.start, body.end, ) @app.post("/tools/get_funding_rate", tags=["reads"]) async def t_get_funding_rate(body: FundingRateReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_funding_rate(body.symbol, body.category) @app.post("/tools/get_funding_history", tags=["reads"]) async def t_get_funding_history(body: FundingHistoryReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_funding_history(body.symbol, body.category, body.limit) @app.post("/tools/get_open_interest", tags=["reads"]) async def t_get_open_interest(body: OpenInterestReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_open_interest(body.symbol, body.category, body.interval, body.limit) @app.post("/tools/get_instruments", tags=["reads"]) async def t_get_instruments(body: InstrumentsReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_instruments(body.category, body.symbol) @app.post("/tools/get_option_chain", tags=["reads"]) async def t_get_option_chain(body: OptionChainReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_option_chain(body.base_coin, body.expiry) @app.post("/tools/get_positions", tags=["reads"]) async def t_get_positions(body: PositionsReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return {"positions": await client.get_positions(body.category)} @app.post("/tools/get_account_summary", tags=["reads"]) async def t_get_account_summary(body: AccountSummaryReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_account_summary() @app.post("/tools/get_trade_history", tags=["reads"]) async def t_get_trade_history(body: TradeHistoryReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return {"trades": await client.get_trade_history(body.category, body.limit)} @app.post("/tools/get_open_orders", tags=["reads"]) async def t_get_open_orders(body: OpenOrdersReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return {"orders": await client.get_open_orders(body.category, body.symbol)} @app.post("/tools/get_basis_spot_perp", tags=["reads"]) async def t_get_basis_spot_perp(body: BasisSpotPerpReq, principal: Principal = Depends(require_principal)): _check(principal, core=True, observer=True) return await client.get_basis_spot_perp(body.asset) # ── Writes ───────────────────────────────────────────── @app.post("/tools/place_order", tags=["writes"]) async def t_place_order(body: PlaceOrderReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.place_order( body.category, body.symbol, body.side, body.qty, body.order_type, body.price, body.tif, body.reduce_only, body.position_idx, ) @app.post("/tools/amend_order", tags=["writes"]) async def t_amend_order(body: AmendOrderReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.amend_order( body.category, body.symbol, body.order_id, body.new_qty, body.new_price, ) @app.post("/tools/cancel_order", tags=["writes"]) async def t_cancel_order(body: CancelOrderReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.cancel_order(body.category, body.symbol, body.order_id) @app.post("/tools/cancel_all_orders", tags=["writes"]) async def t_cancel_all(body: CancelAllReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.cancel_all_orders(body.category, body.symbol) @app.post("/tools/set_stop_loss", tags=["writes"]) async def t_set_sl(body: SetStopLossReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.set_stop_loss(body.category, body.symbol, body.stop_loss, body.position_idx) @app.post("/tools/set_take_profit", tags=["writes"]) async def t_set_tp(body: SetTakeProfitReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.set_take_profit(body.category, body.symbol, body.take_profit, body.position_idx) @app.post("/tools/close_position", tags=["writes"]) async def t_close(body: ClosePositionReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.close_position(body.category, body.symbol) @app.post("/tools/set_leverage", tags=["writes"]) async def t_set_leverage(body: SetLeverageReq, principal: Principal = Depends(require_principal)): _enforce_leverage(body.leverage, creds=creds, exchange="bybit") _check(principal, core=True) return await client.set_leverage(body.category, body.symbol, body.leverage) @app.post("/tools/switch_position_mode", tags=["writes"]) async def t_switch_mode(body: SwitchModeReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.switch_position_mode(body.category, body.symbol, body.mode) @app.post("/tools/transfer_asset", tags=["writes"]) async def t_transfer(body: TransferReq, principal: Principal = Depends(require_principal)): _check(principal, core=True) return await client.transfer_asset(body.coin, body.amount, body.from_type, body.to_type) # ── MCP mount ────────────────────────────────────────── port = int(os.environ.get("PORT", "9019")) mount_mcp_endpoint( app, name="cerbero-bybit", version="0.1.0", token_store=token_store, internal_base_url=f"http://localhost:{port}", tools=[ {"name": "environment_info", "description": "Ambiente operativo (testnet/mainnet), source, base_url, max_leverage cap."}, {"name": "get_ticker", "description": "Ticker Bybit (spot/linear/inverse/option)."}, {"name": "get_ticker_batch", "description": "Ticker per più simboli."}, {"name": "get_orderbook", "description": "Orderbook profondità N."}, {"name": "get_historical", "description": "OHLCV candles Bybit."}, {"name": "get_indicators", "description": "Indicatori tecnici (RSI, ATR, MACD, ADX)."}, {"name": "get_funding_rate", "description": "Funding corrente perp."}, {"name": "get_funding_history", "description": "Funding storico perp."}, {"name": "get_open_interest", "description": "Open interest history perp."}, {"name": "get_instruments", "description": "Specs contratti."}, {"name": "get_option_chain", "description": "Option chain BTC/ETH/SOL."}, {"name": "get_positions", "description": "Posizioni aperte."}, {"name": "get_account_summary", "description": "Wallet balance e margine."}, {"name": "get_trade_history", "description": "Fills recenti."}, {"name": "get_open_orders", "description": "Ordini pending."}, {"name": "get_basis_spot_perp", "description": "Basis spot vs linear perp."}, {"name": "place_order", "description": "Invia ordine (CORE only)."}, {"name": "amend_order", "description": "Modifica ordine esistente."}, {"name": "cancel_order", "description": "Cancella ordine."}, {"name": "cancel_all_orders", "description": "Cancella tutti ordini."}, {"name": "set_stop_loss", "description": "Setta stop loss su posizione."}, {"name": "set_take_profit", "description": "Setta take profit su posizione."}, {"name": "close_position", "description": "Chiude posizione aperta."}, {"name": "set_leverage", "description": "Leva buy+sell uniforme."}, {"name": "switch_position_mode", "description": "Hedge vs one-way."}, {"name": "transfer_asset", "description": "Trasferimento interno tra account types."}, ], ) return app