feat(mcp-macro): expose COT report tools via MCP endpoint
This commit is contained in:
@@ -6,11 +6,14 @@ from fastapi import Depends, FastAPI, HTTPException
|
||||
from mcp_common.auth import Principal, TokenStore, require_principal
|
||||
from mcp_common.mcp_bridge import mount_mcp_endpoint
|
||||
from mcp_common.server import build_app
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp_macro.fetchers import (
|
||||
fetch_asset_price,
|
||||
fetch_breakeven_inflation,
|
||||
fetch_cot_disaggregated,
|
||||
fetch_cot_extreme_positioning,
|
||||
fetch_cot_tff,
|
||||
fetch_economic_indicators,
|
||||
fetch_equity_futures,
|
||||
fetch_macro_calendar,
|
||||
@@ -57,6 +60,20 @@ class GetBreakevenInflationReq(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class GetCotTffReq(BaseModel):
|
||||
symbol: str
|
||||
lookback_weeks: int = Field(default=52, ge=4, le=520)
|
||||
|
||||
|
||||
class GetCotDisaggregatedReq(BaseModel):
|
||||
symbol: str
|
||||
lookback_weeks: int = Field(default=52, ge=4, le=520)
|
||||
|
||||
|
||||
class GetCotExtremeReq(BaseModel):
|
||||
lookback_weeks: int = Field(default=156, ge=4, le=520)
|
||||
|
||||
|
||||
# --- ACL helper ---
|
||||
|
||||
def _check(principal: Principal, *, core: bool = False, observer: bool = False) -> None:
|
||||
@@ -139,6 +156,27 @@ def create_app(*, fred_api_key: str = "", finnhub_api_key: str = "", token_store
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_breakeven_inflation(fred_api_key=fred_api_key)
|
||||
|
||||
@app.post("/tools/get_cot_tff", tags=["reads"])
|
||||
async def t_get_cot_tff(
|
||||
body: GetCotTffReq, principal: Principal = Depends(require_principal)
|
||||
):
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_cot_tff(body.symbol, body.lookback_weeks)
|
||||
|
||||
@app.post("/tools/get_cot_disaggregated", tags=["reads"])
|
||||
async def t_get_cot_disaggregated(
|
||||
body: GetCotDisaggregatedReq, principal: Principal = Depends(require_principal)
|
||||
):
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_cot_disaggregated(body.symbol, body.lookback_weeks)
|
||||
|
||||
@app.post("/tools/get_cot_extreme_positioning", tags=["reads"])
|
||||
async def t_get_cot_extreme(
|
||||
body: GetCotExtremeReq, principal: Principal = Depends(require_principal)
|
||||
):
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_cot_extreme_positioning(body.lookback_weeks)
|
||||
|
||||
# ───── MCP endpoint (/mcp) — bridge verso /tools/* ─────
|
||||
port = int(os.environ.get("PORT", "9013"))
|
||||
mount_mcp_endpoint(
|
||||
@@ -156,6 +194,9 @@ def create_app(*, fred_api_key: str = "", finnhub_api_key: str = "", token_store
|
||||
{"name": "get_equity_futures", "description": "Futures ES/NQ/YM/RTY con session status."},
|
||||
{"name": "get_yield_curve_slope", "description": "Slope 2y10y/5y30y + butterfly + regime (steep/normal/flat/inverted)."},
|
||||
{"name": "get_breakeven_inflation", "description": "Breakeven inflation 5Y/10Y + 5y5y forward (FRED T5YIE/T10YIE/T5YIFR)."},
|
||||
{"name": "get_cot_tff", "description": "COT TFF report (CFTC) per equity/financial: ES/NQ/RTY/ZN/ZB/6E/6J/DX. Roles: dealer, asset manager, leveraged funds, other."},
|
||||
{"name": "get_cot_disaggregated", "description": "COT Disaggregated report (CFTC) per commodities: CL/GC/SI/HG/ZW/ZC/ZS. Roles: producer/merchant, swap dealer, managed money, other."},
|
||||
{"name": "get_cot_extreme_positioning", "description": "Scanner posizionamento estremo (percentile ≤5 o ≥95) sui simboli watchlist."},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -125,3 +125,79 @@ def test_get_market_overview_observer_ok(http):
|
||||
def test_get_market_overview_no_auth_401(http):
|
||||
r = http.post("/tools/get_market_overview", json={})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
|
||||
def test_get_cot_tff_core_ok(http):
|
||||
with patch(
|
||||
"mcp_macro.server.fetch_cot_tff",
|
||||
new=AsyncMock(return_value={"symbol": "ES", "rows": []}),
|
||||
):
|
||||
r = http.post(
|
||||
"/tools/get_cot_tff",
|
||||
headers={"Authorization": "Bearer ct"},
|
||||
json={"symbol": "ES"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["symbol"] == "ES"
|
||||
|
||||
|
||||
def test_get_cot_tff_observer_ok(http):
|
||||
with patch(
|
||||
"mcp_macro.server.fetch_cot_tff",
|
||||
new=AsyncMock(return_value={"symbol": "ES", "rows": []}),
|
||||
):
|
||||
r = http.post(
|
||||
"/tools/get_cot_tff",
|
||||
headers={"Authorization": "Bearer ot"},
|
||||
json={"symbol": "ES"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_get_cot_tff_no_auth_401(http):
|
||||
r = http.post("/tools/get_cot_tff", json={"symbol": "ES"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_get_cot_disagg_observer_ok(http):
|
||||
with patch(
|
||||
"mcp_macro.server.fetch_cot_disaggregated",
|
||||
new=AsyncMock(return_value={"symbol": "CL", "rows": []}),
|
||||
):
|
||||
r = http.post(
|
||||
"/tools/get_cot_disaggregated",
|
||||
headers={"Authorization": "Bearer ot"},
|
||||
json={"symbol": "CL"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_get_cot_disagg_no_auth_401(http):
|
||||
r = http.post("/tools/get_cot_disaggregated", json={"symbol": "CL"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_get_cot_extreme_positioning_ok(http):
|
||||
with patch(
|
||||
"mcp_macro.server.fetch_cot_extreme_positioning",
|
||||
new=AsyncMock(return_value={"extremes": []}),
|
||||
):
|
||||
r = http.post(
|
||||
"/tools/get_cot_extreme_positioning",
|
||||
headers={"Authorization": "Bearer ot"},
|
||||
json={},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_get_cot_extreme_positioning_lookback_too_short(http):
|
||||
"""Pydantic validation: lookback_weeks < 4 → 422."""
|
||||
r = http.post(
|
||||
"/tools/get_cot_extreme_positioning",
|
||||
headers={"Authorization": "Bearer ct"},
|
||||
json={"lookback_weeks": 2},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user