diff --git a/docker-compose.yml b/docker-compose.yml index fce96ef..9d9bdcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,6 +78,7 @@ services: CREDENTIALS_FILE: /run/secrets/deribit_credentials CORE_TOKEN_FILE: /run/secrets/core_token OBSERVER_TOKEN_FILE: /run/secrets/observer_token + DERIBIT_TESTNET: "true" # override secrets/deribit.json testnet flag ROOT_PATH: /mcp-deribit mcp-hyperliquid: diff --git a/services/mcp-deribit/src/mcp_deribit/__main__.py b/services/mcp-deribit/src/mcp_deribit/__main__.py index 811303b..81b2870 100644 --- a/services/mcp-deribit/src/mcp_deribit/__main__.py +++ b/services/mcp-deribit/src/mcp_deribit/__main__.py @@ -5,14 +5,15 @@ import os import uvicorn from mcp_common.auth import load_token_store_from_files +from mcp_common.environment import resolve_environment +from mcp_common.logging import configure_root_logging + +from mcp_deribit.client import DeribitClient from mcp_deribit.env_validation import ( fail_fast_if_missing, require_env, summarize, ) -from mcp_common.logging import configure_root_logging - -from mcp_deribit.client import DeribitClient from mcp_deribit.server import create_app configure_root_logging() # CER-P5-009: JSON default, env LOG_FORMAT=text per dev @@ -26,17 +27,33 @@ def main(): with open(creds_file) as f: creds = json.load(f) + # Default base URLs per backward-compat con secret schema legacy + creds.setdefault("base_url_live", "https://www.deribit.com/api/v2") + creds.setdefault("base_url_testnet", "https://test.deribit.com/api/v2") + + env_info = resolve_environment( + creds, + env_var="DERIBIT_TESTNET", + flag_key="testnet", + exchange="deribit", + ) + client = DeribitClient( client_id=creds["client_id"], client_secret=creds["client_secret"], - testnet=bool(creds.get("testnet", True)), + testnet=(env_info.environment == "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, creds=creds) + app = create_app( + client=client, + token_store=token_store, + creds=creds, + env_info=env_info, + ) uvicorn.run( app, log_config=None, # CER-P5-009: delega al root JSON logger diff --git a/services/mcp-deribit/src/mcp_deribit/server.py b/services/mcp-deribit/src/mcp_deribit/server.py index f1eb55e..4aa0757 100644 --- a/services/mcp-deribit/src/mcp_deribit/server.py +++ b/services/mcp-deribit/src/mcp_deribit/server.py @@ -4,6 +4,7 @@ import os from fastapi import Depends, FastAPI, 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_deribit.leverage_cap import enforce_leverage as _enforce_leverage from mcp_deribit.leverage_cap import get_max_leverage @@ -219,7 +220,13 @@ def _check(principal: Principal, *, core: bool = False, observer: bool = False) # --- App factory --- -def create_app(*, client: DeribitClient, token_store: TokenStore, creds: dict) -> FastAPI: +def create_app( + *, + client: DeribitClient, + token_store: TokenStore, + creds: dict, + env_info: EnvironmentInfo | None = None, +) -> FastAPI: from contextlib import asynccontextmanager cap_default = get_max_leverage(creds) @@ -248,6 +255,27 @@ def create_app(*, client: DeribitClient, token_store: TokenStore, creds: dict) - _check(principal, core=True, observer=True) return client.is_testnet() + @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": "deribit", + "environment": "testnet" if client.is_testnet().get("testnet") else "mainnet", + "source": "credentials", + "env_value": None, + "base_url": client.base_url, + "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: GetTickerReq, principal: Principal = Depends(require_principal) @@ -487,6 +515,7 @@ def create_app(*, client: DeribitClient, token_store: TokenStore, creds: dict) - internal_base_url=f"http://localhost:{port}", tools=[ {"name": "is_testnet", "description": "True se client Deribit è in modalità testnet."}, + {"name": "environment_info", "description": "Ambiente operativo (testnet/mainnet), source, base_url, max_leverage cap."}, {"name": "get_ticker", "description": "Ticker di un instrument Deribit."}, {"name": "get_ticker_batch", "description": "Ticker per N instruments in parallelo (max 20)."}, {"name": "get_instruments", "description": "Lista instruments per currency."}, diff --git a/services/mcp-deribit/tests/test_environment_info.py b/services/mcp-deribit/tests/test_environment_info.py new file mode 100644 index 0000000..5344ade --- /dev/null +++ b/services/mcp-deribit/tests/test_environment_info.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from unittest.mock import AsyncMock + +from fastapi.testclient import TestClient + +from mcp_common.auth import Principal, TokenStore +from mcp_common.environment import EnvironmentInfo +from mcp_deribit.server import create_app + + +def _make_app(env_info, creds): + c = AsyncMock() + c.set_leverage = AsyncMock(return_value={"state": "ok"}) + store = TokenStore(tokens={ + "ct": Principal("core", {"core"}), + "ot": Principal("observer", {"observer"}), + }) + return create_app(client=c, token_store=store, creds=creds, env_info=env_info) + + +def test_environment_info_full_shape(): + env = EnvironmentInfo( + exchange="deribit", + environment="testnet", + source="env", + env_value="true", + base_url="https://test.deribit.com/api/v2", + ) + app = _make_app(env, creds={"max_leverage": 3}) + c = TestClient(app) + r = c.post( + "/tools/environment_info", + headers={"Authorization": "Bearer ot"}, + ) + assert r.status_code == 200 + body = r.json() + assert body["exchange"] == "deribit" + assert body["environment"] == "testnet" + assert body["source"] == "env" + assert body["env_value"] == "true" + assert body["base_url"] == "https://test.deribit.com/api/v2" + assert body["max_leverage"] == 3 + + +def test_environment_info_default_source(): + env = EnvironmentInfo( + exchange="deribit", + environment="testnet", + source="default", + env_value=None, + base_url="https://test.deribit.com/api/v2", + ) + app = _make_app(env, creds={"max_leverage": 1}) + c = TestClient(app) + r = c.post( + "/tools/environment_info", + headers={"Authorization": "Bearer ct"}, + ) + assert r.status_code == 200 + body = r.json() + assert body["source"] == "default" + assert body["env_value"] is None + assert body["max_leverage"] == 1 + + +def test_environment_info_requires_auth(): + env = EnvironmentInfo( + exchange="deribit", + environment="testnet", + source="default", + env_value=None, + base_url="https://test.deribit.com/api/v2", + ) + app = _make_app(env, creds={"max_leverage": 3}) + c = TestClient(app) + r = c.post("/tools/environment_info") + assert r.status_code == 401