feat: import 6 MCP services + common workspace

This commit is contained in:
AdrianoDev
2026-04-27 17:34:14 +02:00
parent 9676f22a8e
commit 6fc3d1d94f
67 changed files with 10693 additions and 0 deletions
@@ -0,0 +1,45 @@
from __future__ import annotations
import json
import os
import uvicorn
from option_mcp_common.auth import load_token_store_from_files
from option_mcp_common.logging import configure_root_logging
from mcp_bybit.client import BybitClient
from mcp_bybit.server import create_app
configure_root_logging()
def main():
creds_file = os.environ["BYBIT_CREDENTIALS_FILE"]
with open(creds_file) as f:
creds = json.load(f)
testnet_env = os.environ.get("BYBIT_TESTNET", "true").lower()
testnet = testnet_env not in ("0", "false", "no")
client = BybitClient(
api_key=creds["api_key"],
api_secret=creds["api_secret"],
testnet=testnet,
)
token_store = load_token_store_from_files(
core_token_file=os.environ.get("CORE_TOKEN_FILE"),
observer_token_file=os.environ.get("OBSERVER_TOKEN_FILE"),
)
app = create_app(client=client, token_store=token_store)
uvicorn.run(
app,
log_config=None,
host=os.environ.get("HOST", "0.0.0.0"),
port=int(os.environ.get("PORT", "9019")),
)
if __name__ == "__main__":
main()
+558
View File
@@ -0,0 +1,558 @@
from __future__ import annotations
import asyncio
from typing import Any
from option_mcp_common import indicators as ind
from pybit.unified_trading import HTTP
def _f(v: Any) -> float | None:
try:
return float(v)
except (TypeError, ValueError):
return None
def _i(v: Any) -> int | None:
try:
return int(v)
except (TypeError, ValueError):
return None
class BybitClient:
def __init__(
self,
api_key: str,
api_secret: str,
testnet: bool = True,
http: Any | None = None,
) -> None:
self.api_key = api_key
self.api_secret = api_secret
self.testnet = testnet
self._http = http or HTTP(
api_key=api_key,
api_secret=api_secret,
testnet=testnet,
)
async def _run(self, fn, /, **kwargs):
return await asyncio.to_thread(fn, **kwargs)
@staticmethod
def _parse_ticker(row: dict) -> dict:
return {
"symbol": row.get("symbol"),
"last_price": _f(row.get("lastPrice")),
"mark_price": _f(row.get("markPrice")),
"bid": _f(row.get("bid1Price")),
"ask": _f(row.get("ask1Price")),
"volume_24h": _f(row.get("volume24h")),
"turnover_24h": _f(row.get("turnover24h")),
"funding_rate": _f(row.get("fundingRate")),
"open_interest": _f(row.get("openInterest")),
}
async def get_ticker(self, symbol: str, category: str = "linear") -> dict:
resp = await self._run(
self._http.get_tickers, category=category, symbol=symbol
)
rows = (resp.get("result") or {}).get("list") or []
if not rows:
return {"symbol": symbol, "error": "not_found"}
return self._parse_ticker(rows[0])
async def get_ticker_batch(
self, symbols: list[str], category: str = "linear"
) -> dict[str, dict]:
out: dict[str, dict] = {}
for sym in symbols:
out[sym] = await self.get_ticker(sym, category=category)
return out
async def get_orderbook(
self, symbol: str, category: str = "linear", limit: int = 50
) -> dict:
resp = await self._run(
self._http.get_orderbook, category=category, symbol=symbol, limit=limit
)
r = resp.get("result") or {}
return {
"symbol": r.get("s"),
"bids": [[float(p), float(q)] for p, q in (r.get("b") or [])],
"asks": [[float(p), float(q)] for p, q in (r.get("a") or [])],
"timestamp": r.get("ts"),
}
async def get_historical(
self,
symbol: str,
category: str = "linear",
interval: str = "60",
start: int | None = None,
end: int | None = None,
limit: int = 1000,
) -> dict:
kwargs = dict(
category=category,
symbol=symbol,
interval=interval,
limit=limit,
)
if start is not None:
kwargs["start"] = start
if end is not None:
kwargs["end"] = end
resp = await self._run(self._http.get_kline, **kwargs)
rows = (resp.get("result") or {}).get("list") or []
rows_sorted = sorted(rows, key=lambda r: int(r[0]))
candles = [
{
"timestamp": int(r[0]),
"open": float(r[1]),
"high": float(r[2]),
"low": float(r[3]),
"close": float(r[4]),
"volume": float(r[5]),
}
for r in rows_sorted
]
return {"symbol": symbol, "candles": candles}
async def get_indicators(
self,
symbol: str,
category: str = "linear",
indicators: list[str] | None = None,
interval: str = "60",
start: int | None = None,
end: int | None = None,
) -> dict:
indicators = indicators or ["rsi", "atr", "macd", "adx"]
historical = await self.get_historical(
symbol, category=category, interval=interval, start=start, end=end
)
candles = historical.get("candles", [])
closes = [c["close"] for c in candles]
highs = [c["high"] for c in candles]
lows = [c["low"] for c in candles]
out: dict[str, Any] = {"symbol": symbol, "category": category}
for name in indicators:
n = name.lower()
if n == "sma":
out["sma"] = ind.sma(closes, 20)
elif n == "rsi":
out["rsi"] = ind.rsi(closes)
elif n == "atr":
out["atr"] = ind.atr(highs, lows, closes)
elif n == "macd":
out["macd"] = ind.macd(closes)
elif n == "adx":
out["adx"] = ind.adx(highs, lows, closes)
else:
out[n] = None
return out
async def get_funding_rate(self, symbol: str, category: str = "linear") -> dict:
resp = await self._run(
self._http.get_tickers, category=category, symbol=symbol
)
rows = (resp.get("result") or {}).get("list") or []
if not rows:
return {"symbol": symbol, "error": "not_found"}
row = rows[0]
return {
"symbol": row.get("symbol"),
"funding_rate": _f(row.get("fundingRate")),
"next_funding_time": _i(row.get("nextFundingTime")),
}
async def get_funding_history(
self, symbol: str, category: str = "linear", limit: int = 100
) -> dict:
resp = await self._run(
self._http.get_funding_rate_history,
category=category, symbol=symbol, limit=limit,
)
rows = (resp.get("result") or {}).get("list") or []
hist = [
{
"timestamp": int(r.get("fundingRateTimestamp", 0)),
"rate": float(r.get("fundingRate", 0)),
}
for r in rows
]
return {"symbol": symbol, "history": hist}
async def get_open_interest(
self,
symbol: str,
category: str = "linear",
interval: str = "5min",
limit: int = 288,
) -> dict:
resp = await self._run(
self._http.get_open_interest,
category=category, symbol=symbol, intervalTime=interval, limit=limit,
)
rows = (resp.get("result") or {}).get("list") or []
points = [
{
"timestamp": int(r.get("timestamp", 0)),
"oi": float(r.get("openInterest", 0)),
}
for r in rows
]
current_oi = points[0]["oi"] if points else None
return {
"symbol": symbol,
"category": category,
"interval": interval,
"current_oi": current_oi,
"points": points,
}
async def get_instruments(self, category: str = "linear", symbol: str | None = None) -> dict:
kwargs: dict[str, Any] = {"category": category}
if symbol:
kwargs["symbol"] = symbol
resp = await self._run(self._http.get_instruments_info, **kwargs)
rows = (resp.get("result") or {}).get("list") or []
instruments = []
for r in rows:
pf = r.get("priceFilter") or {}
lf = r.get("lotSizeFilter") or {}
instruments.append({
"symbol": r.get("symbol"),
"status": r.get("status"),
"base_coin": r.get("baseCoin"),
"quote_coin": r.get("quoteCoin"),
"tick_size": _f(pf.get("tickSize")),
"qty_step": _f(lf.get("qtyStep")),
"min_qty": _f(lf.get("minOrderQty")),
})
return {"category": category, "instruments": instruments}
async def get_option_chain(self, base_coin: str, expiry: str | None = None) -> dict:
kwargs: dict[str, Any] = {"category": "option", "baseCoin": base_coin.upper()}
resp = await self._run(self._http.get_instruments_info, **kwargs)
rows = (resp.get("result") or {}).get("list") or []
options = []
for r in rows:
delivery = r.get("deliveryTime")
if expiry and expiry not in r.get("symbol", ""):
continue
options.append({
"symbol": r.get("symbol"),
"base_coin": r.get("baseCoin"),
"settle_coin": r.get("settleCoin"),
"type": r.get("optionsType"),
"launch_time": int(r.get("launchTime", 0)),
"delivery_time": int(delivery) if delivery else None,
})
return {"base_coin": base_coin.upper(), "options": options}
async def get_positions(
self, category: str = "linear", settle_coin: str = "USDT"
) -> list[dict]:
kwargs: dict[str, Any] = {"category": category}
if category in ("linear", "inverse"):
kwargs["settleCoin"] = settle_coin
resp = await self._run(self._http.get_positions, **kwargs)
rows = (resp.get("result") or {}).get("list") or []
out = []
for r in rows:
out.append({
"symbol": r.get("symbol"),
"side": r.get("side"),
"size": _f(r.get("size")),
"entry_price": _f(r.get("avgPrice")),
"unrealized_pnl": _f(r.get("unrealisedPnl")),
"leverage": _f(r.get("leverage")),
"liquidation_price": _f(r.get("liqPrice")),
"position_value": _f(r.get("positionValue")),
})
return out
async def get_account_summary(self, account_type: str = "UNIFIED") -> dict:
resp = await self._run(
self._http.get_wallet_balance, accountType=account_type
)
rows = (resp.get("result") or {}).get("list") or []
if not rows:
return {"error": "no_account"}
a = rows[0]
coins = []
for c in a.get("coin") or []:
coins.append({
"coin": c.get("coin"),
"wallet_balance": _f(c.get("walletBalance")),
"equity": _f(c.get("equity")),
})
return {
"account_type": a.get("accountType"),
"equity": _f(a.get("totalEquity")),
"wallet_balance": _f(a.get("totalWalletBalance")),
"margin_balance": _f(a.get("totalMarginBalance")),
"available_balance": _f(a.get("totalAvailableBalance")),
"unrealized_pnl": _f(a.get("totalPerpUPL")),
"coins": coins,
}
async def get_trade_history(
self, category: str = "linear", limit: int = 50
) -> list[dict]:
resp = await self._run(
self._http.get_executions, category=category, limit=limit
)
rows = (resp.get("result") or {}).get("list") or []
return [
{
"symbol": r.get("symbol"),
"side": r.get("side"),
"size": _f(r.get("execQty")),
"price": _f(r.get("execPrice")),
"fee": _f(r.get("execFee")),
"timestamp": _i(r.get("execTime")),
"order_id": r.get("orderId"),
}
for r in rows
]
async def get_open_orders(
self,
category: str = "linear",
symbol: str | None = None,
settle_coin: str = "USDT",
) -> list[dict]:
kwargs: dict[str, Any] = {"category": category}
if category in ("linear", "inverse") and not symbol:
kwargs["settleCoin"] = settle_coin
if symbol:
kwargs["symbol"] = symbol
resp = await self._run(self._http.get_open_orders, **kwargs)
rows = (resp.get("result") or {}).get("list") or []
return [
{
"order_id": r.get("orderId"),
"symbol": r.get("symbol"),
"side": r.get("side"),
"qty": _f(r.get("qty")),
"price": _f(r.get("price")),
"type": r.get("orderType"),
"status": r.get("orderStatus"),
"reduce_only": bool(r.get("reduceOnly")),
}
for r in rows
]
async def get_basis_spot_perp(self, asset: str) -> dict:
asset = asset.upper()
symbol = f"{asset}USDT"
spot = await self.get_ticker(symbol, category="spot")
perp = await self.get_ticker(symbol, category="linear")
sp = spot.get("last_price")
pp = perp.get("last_price")
basis_abs = basis_pct = None
if sp and pp:
basis_abs = pp - sp
basis_pct = 100.0 * basis_abs / sp
return {
"asset": asset,
"symbol": symbol,
"spot_price": sp,
"perp_price": pp,
"basis_abs": basis_abs,
"basis_pct": basis_pct,
"funding_rate": perp.get("funding_rate"),
}
def _envelope(self, resp: dict, payload: dict) -> dict:
code = resp.get("retCode", 0)
if code != 0:
return {"error": resp.get("retMsg", "bybit_error"), "code": code}
return payload
async def place_order(
self,
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,
) -> dict:
kwargs: dict[str, Any] = {
"category": category,
"symbol": symbol,
"side": side,
"qty": str(qty),
"orderType": order_type,
"timeInForce": tif,
"reduceOnly": reduce_only,
}
if price is not None:
kwargs["price"] = str(price)
if position_idx is not None:
kwargs["positionIdx"] = position_idx
if category == "option":
import uuid
kwargs["orderLinkId"] = f"cerbero-{uuid.uuid4().hex[:16]}"
resp = await self._run(self._http.place_order, **kwargs)
r = resp.get("result") or {}
return self._envelope(resp, {
"order_id": r.get("orderId"),
"order_link_id": r.get("orderLinkId"),
"status": "submitted",
})
async def amend_order(
self,
category: str,
symbol: str,
order_id: str,
new_qty: float | None = None,
new_price: float | None = None,
) -> dict:
kwargs: dict[str, Any] = {
"category": category,
"symbol": symbol,
"orderId": order_id,
}
if new_qty is not None:
kwargs["qty"] = str(new_qty)
if new_price is not None:
kwargs["price"] = str(new_price)
resp = await self._run(self._http.amend_order, **kwargs)
r = resp.get("result") or {}
return self._envelope(resp, {
"order_id": r.get("orderId", order_id),
"status": "amended",
})
async def cancel_order(
self, category: str, symbol: str, order_id: str
) -> dict:
resp = await self._run(
self._http.cancel_order,
category=category, symbol=symbol, orderId=order_id,
)
r = resp.get("result") or {}
return self._envelope(resp, {
"order_id": r.get("orderId", order_id),
"status": "cancelled",
})
async def cancel_all_orders(
self, category: str, symbol: str | None = None
) -> dict:
kwargs: dict[str, Any] = {"category": category}
if symbol:
kwargs["symbol"] = symbol
resp = await self._run(self._http.cancel_all_orders, **kwargs)
r = resp.get("result") or {}
ids = [x.get("orderId") for x in (r.get("list") or [])]
return self._envelope(resp, {
"cancelled_ids": ids,
"count": len(ids),
})
async def set_stop_loss(
self, category: str, symbol: str, stop_loss: float,
position_idx: int = 0,
) -> dict:
resp = await self._run(
self._http.set_trading_stop,
category=category, symbol=symbol,
stopLoss=str(stop_loss), positionIdx=position_idx,
)
return self._envelope(resp, {
"symbol": symbol, "stop_loss": stop_loss,
"status": "stop_loss_set",
})
async def set_take_profit(
self, category: str, symbol: str, take_profit: float,
position_idx: int = 0,
) -> dict:
resp = await self._run(
self._http.set_trading_stop,
category=category, symbol=symbol,
takeProfit=str(take_profit), positionIdx=position_idx,
)
return self._envelope(resp, {
"symbol": symbol, "take_profit": take_profit,
"status": "take_profit_set",
})
async def close_position(self, category: str, symbol: str) -> dict:
positions = await self.get_positions(category=category)
target = next((p for p in positions if p["symbol"] == symbol and (p["size"] or 0) > 0), None)
if not target:
return {"error": "no_open_position", "symbol": symbol}
close_side = "Sell" if target["side"] == "Buy" else "Buy"
return await self.place_order(
category=category,
symbol=symbol,
side=close_side,
qty=target["size"],
order_type="Market",
reduce_only=True,
tif="IOC",
)
async def set_leverage(
self, category: str, symbol: str, leverage: int
) -> dict:
resp = await self._run(
self._http.set_leverage,
category=category, symbol=symbol,
buyLeverage=str(leverage), sellLeverage=str(leverage),
)
return self._envelope(resp, {
"symbol": symbol, "leverage": leverage,
"status": "leverage_set",
})
async def switch_position_mode(
self, category: str, symbol: str, mode: str
) -> dict:
mode_code = 3 if mode.lower() == "hedge" else 0
resp = await self._run(
self._http.switch_position_mode,
category=category, symbol=symbol, mode=mode_code,
)
return self._envelope(resp, {
"symbol": symbol, "mode": mode,
"status": "mode_switched",
})
async def transfer_asset(
self,
coin: str,
amount: float,
from_type: str,
to_type: str,
) -> dict:
import uuid
resp = await self._run(
self._http.create_internal_transfer,
transferId=str(uuid.uuid4()),
coin=coin,
amount=str(amount),
fromAccountType=from_type,
toAccountType=to_type,
)
r = resp.get("result") or {}
return self._envelope(resp, {
"transfer_id": r.get("transferId"),
"coin": coin,
"amount": amount,
"status": "submitted",
})
+363
View File
@@ -0,0 +1,363 @@
from __future__ import annotations
import os
from fastapi import Depends, HTTPException
from option_mcp_common.auth import Principal, TokenStore, require_principal
from option_mcp_common.mcp_bridge import mount_mcp_endpoint
from option_mcp_common.server import build_app
from pydantic import BaseModel
from mcp_bybit.client import BybitClient
# --- 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):
app = build_app(name="mcp-bybit", version="0.1.0", token_store=token_store)
# ── Reads ──────────────────────────────────────────────
@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)):
_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": "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