everestapi 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {everestapi-0.1.2/src/everestapi.egg-info → everestapi-0.2.0}/PKG-INFO +3 -1
- {everestapi-0.1.2 → everestapi-0.2.0}/pyproject.toml +3 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi/__init__.py +1 -1
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi/client.py +92 -5
- everestapi-0.2.0/src/everestapi/mcp/__init__.py +1 -0
- everestapi-0.2.0/src/everestapi/mcp/__main__.py +4 -0
- everestapi-0.2.0/src/everestapi/mcp/server.py +844 -0
- {everestapi-0.1.2 → everestapi-0.2.0/src/everestapi.egg-info}/PKG-INFO +3 -1
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi.egg-info/SOURCES.txt +5 -1
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi.egg-info/requires.txt +3 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/tests/test_client.py +46 -0
- everestapi-0.2.0/tests/test_mcp_and_models.py +120 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/LICENSE +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/README.md +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/setup.cfg +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi/__main__.py +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi/cli.py +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi/types.py +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi.egg-info/dependency_links.txt +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi.egg-info/entry_points.txt +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/src/everestapi.egg-info/top_level.txt +0 -0
- {everestapi-0.1.2 → everestapi-0.2.0}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: everestapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python SDK for the EverestQuant prediction tournament platform
|
|
5
5
|
Author-email: EverestQuant <engineering@everestquant.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,6 +24,8 @@ Requires-Dist: click>=8.0
|
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pytest-httpx>=0.34.0; extra == "dev"
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
27
29
|
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# everestapi
|
|
@@ -28,6 +28,9 @@ dependencies = [
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = ["pytest>=8.0", "pytest-httpx>=0.34.0"]
|
|
31
|
+
# MCP server (`python -m everestapi.mcp`). Optional — the server falls back to a
|
|
32
|
+
# built-in JSON-RPC stdio loop when the official `mcp` SDK isn't installed.
|
|
33
|
+
mcp = ["mcp>=1.0"]
|
|
31
34
|
|
|
32
35
|
[project.scripts]
|
|
33
36
|
everestapi = "everestapi.cli:cli"
|
|
@@ -133,6 +133,8 @@ class EverestAPI:
|
|
|
133
133
|
timeout: float = 30.0,
|
|
134
134
|
tournament: str = "equities",
|
|
135
135
|
basic_auth: tuple[str, str] | None = None,
|
|
136
|
+
cf_access_client_id: str | None = None,
|
|
137
|
+
cf_access_client_secret: str | None = None,
|
|
136
138
|
) -> None:
|
|
137
139
|
self.api_key = api_key or os.getenv("EVEREST_API_KEY", "")
|
|
138
140
|
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
@@ -153,6 +155,17 @@ class EverestAPI:
|
|
|
153
155
|
# server logs.
|
|
154
156
|
headers["X-API-Key"] = self.api_key
|
|
155
157
|
|
|
158
|
+
# Cloudflare Access service token: when the API host sits behind CF
|
|
159
|
+
# Access (e.g. staging), a service token clears the gate without
|
|
160
|
+
# interactive SSO. Supplied explicitly or via CF_ACCESS_CLIENT_ID /
|
|
161
|
+
# CF_ACCESS_CLIENT_SECRET. Rides alongside X-API-Key — it replaces the
|
|
162
|
+
# CF login, not the app's auth. Both halves required.
|
|
163
|
+
cf_id = cf_access_client_id or os.getenv("CF_ACCESS_CLIENT_ID")
|
|
164
|
+
cf_token = cf_access_client_secret or os.getenv("CF_ACCESS_CLIENT_SECRET")
|
|
165
|
+
if cf_id and cf_token:
|
|
166
|
+
headers["CF-Access-Client-Id"] = cf_id
|
|
167
|
+
headers["CF-Access-Client-Secret"] = cf_token
|
|
168
|
+
|
|
156
169
|
client_kwargs: dict[str, Any] = {
|
|
157
170
|
"base_url": self.base_url,
|
|
158
171
|
"headers": headers,
|
|
@@ -338,12 +351,51 @@ class EverestAPI:
|
|
|
338
351
|
universe: str = "futures",
|
|
339
352
|
split: str = "train",
|
|
340
353
|
output_path: str | None = None,
|
|
341
|
-
version: str = "
|
|
354
|
+
version: str = "latest",
|
|
342
355
|
) -> str:
|
|
343
|
-
"""Download a parquet dataset to a local file.
|
|
356
|
+
"""Download a parquet dataset to a local file.
|
|
357
|
+
|
|
358
|
+
The download route is version-scoped
|
|
359
|
+
(``/api/v1/data/download/{version}/{universe}/{split}``). Pass
|
|
360
|
+
``version="latest"`` (the default) to resolve the current version from
|
|
361
|
+
``/api/v1/data/versions`` automatically, or an explicit version
|
|
362
|
+
(e.g. ``"bregen"``) to pin it. A stale pinned version self-heals: on a
|
|
363
|
+
404 the current version is resolved and the download retried once.
|
|
364
|
+
|
|
365
|
+
Splits: ``train`` / ``validation`` (features + targets), ``live``
|
|
366
|
+
(current round features only).
|
|
367
|
+
"""
|
|
344
368
|
if output_path is None:
|
|
345
369
|
output_path = f"{universe}_{split}.parquet"
|
|
346
|
-
|
|
370
|
+
if version in (None, "latest", "current"):
|
|
371
|
+
version = self._current_dataset_version(universe) or "v0"
|
|
372
|
+
path = f"/api/v1/data/download/{version}/{universe}/{split}"
|
|
373
|
+
try:
|
|
374
|
+
return self._download(path, output_path)
|
|
375
|
+
except EverestError as exc:
|
|
376
|
+
if exc.status_code == 404:
|
|
377
|
+
current = self._current_dataset_version(universe)
|
|
378
|
+
if current and current != version:
|
|
379
|
+
return self._download(
|
|
380
|
+
f"/api/v1/data/download/{current}/{universe}/{split}",
|
|
381
|
+
output_path,
|
|
382
|
+
)
|
|
383
|
+
raise
|
|
384
|
+
|
|
385
|
+
def _current_dataset_version(self, universe: str) -> str | None:
|
|
386
|
+
"""Return the current dataset version for ``universe`` (or ``None``).
|
|
387
|
+
|
|
388
|
+
Reads ``/api/v1/data/versions`` so callers never hardcode a version.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
data = self.list_versions()
|
|
392
|
+
except Exception:
|
|
393
|
+
return None
|
|
394
|
+
for v in data.get("versions", []):
|
|
395
|
+
if universe in (v.get("universes") or []):
|
|
396
|
+
return v.get("version")
|
|
397
|
+
versions = data.get("versions") or []
|
|
398
|
+
return versions[0].get("version") if versions else None
|
|
347
399
|
|
|
348
400
|
def get_dataset_info(self, universe: str = "futures") -> dict:
|
|
349
401
|
"""Get metadata about a dataset universe."""
|
|
@@ -450,12 +502,14 @@ class EverestAPI:
|
|
|
450
502
|
self,
|
|
451
503
|
universe: str = "futures",
|
|
452
504
|
split: str = "validation",
|
|
453
|
-
version: str = "
|
|
505
|
+
version: str = "latest",
|
|
454
506
|
output_path: str | None = None,
|
|
455
507
|
) -> str:
|
|
456
|
-
"""Download benchmark model predictions."""
|
|
508
|
+
"""Download benchmark model predictions (version-scoped, ``latest`` by default)."""
|
|
457
509
|
if output_path is None:
|
|
458
510
|
output_path = f"benchmark_{universe}_{split}.parquet"
|
|
511
|
+
if version in (None, "latest", "current"):
|
|
512
|
+
version = self._current_dataset_version(universe) or "v0"
|
|
459
513
|
return self._download(
|
|
460
514
|
f"/api/v1/data/{version}/benchmark/{universe}/{split}", output_path,
|
|
461
515
|
)
|
|
@@ -585,6 +639,39 @@ class EverestAPI:
|
|
|
585
639
|
raise EverestError(resp.status_code, detail)
|
|
586
640
|
return resp.json()
|
|
587
641
|
|
|
642
|
+
def create_model(
|
|
643
|
+
self,
|
|
644
|
+
name: str,
|
|
645
|
+
description: str | None = None,
|
|
646
|
+
retrain_schedule: str | None = None,
|
|
647
|
+
) -> dict:
|
|
648
|
+
"""POST /api/v1/agents/me/models — register a model under your agent.
|
|
649
|
+
|
|
650
|
+
A model must exist before you can submit predictions or upload a
|
|
651
|
+
``.pkl`` for it — the platform never auto-creates one. The model's
|
|
652
|
+
``name`` is the ``model_id`` you pass to
|
|
653
|
+
:meth:`submit_futures_predictions` / :meth:`upload_model`.
|
|
654
|
+
|
|
655
|
+
Idempotent: if you already own a model with this name (HTTP 409) this
|
|
656
|
+
returns ``{"name": name, "status": "already_exists"}`` instead of
|
|
657
|
+
raising, so a create-then-submit flow is safe to re-run.
|
|
658
|
+
"""
|
|
659
|
+
body: dict[str, Any] = {"name": name}
|
|
660
|
+
if description is not None:
|
|
661
|
+
body["description"] = description
|
|
662
|
+
if retrain_schedule is not None:
|
|
663
|
+
body["retrain_schedule"] = retrain_schedule
|
|
664
|
+
try:
|
|
665
|
+
return self._request("POST", "/api/v1/agents/me/models", json=body)
|
|
666
|
+
except EverestError as exc:
|
|
667
|
+
if exc.status_code == 409:
|
|
668
|
+
return {"name": name, "status": "already_exists"}
|
|
669
|
+
raise
|
|
670
|
+
|
|
671
|
+
def get_models(self) -> dict:
|
|
672
|
+
"""GET /api/v1/agents/me/models — list the models under your agent."""
|
|
673
|
+
return self._request("GET", "/api/v1/agents/me/models")
|
|
674
|
+
|
|
588
675
|
# -- rounds -----------------------------------------------------------
|
|
589
676
|
|
|
590
677
|
def get_rounds(self, tournament: str = "equities", limit: int = 25) -> dict:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""EverestAPI MCP server — exposes the tournament SDK as MCP tools."""
|
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EverestQuant MCP Server — Model Context Protocol server for the EverestQuant
|
|
3
|
+
(EIQ) tournament platform.
|
|
4
|
+
|
|
5
|
+
Exposes the tournament SDK as MCP tools so AI agents (Claude Code, etc.) can
|
|
6
|
+
download data, train, create models and submit predictions natively.
|
|
7
|
+
|
|
8
|
+
Run::
|
|
9
|
+
|
|
10
|
+
python -m everestapi.mcp
|
|
11
|
+
|
|
12
|
+
Requires environment variables:
|
|
13
|
+
EVEREST_API_KEY — your EverestQuant API key (also accepts EIQ_API_KEY)
|
|
14
|
+
EIQ_BASE_URL — API base URL (default: https://everestquant.ai)
|
|
15
|
+
CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET — Cloudflare Access service
|
|
16
|
+
token, required only when the host sits behind CF Access (e.g. staging)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Try to import the MCP SDK. If unavailable, fall back to a minimal
|
|
30
|
+
# stdin/stdout JSON-RPC 2.0 implementation so the server is still usable.
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from mcp.server import Server
|
|
35
|
+
from mcp.server.stdio import stdio_server
|
|
36
|
+
from mcp.types import CallToolResult, TextContent, Tool
|
|
37
|
+
|
|
38
|
+
_HAS_MCP = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
_HAS_MCP = False
|
|
41
|
+
|
|
42
|
+
# We import the SDK lazily inside handlers so the module can load even
|
|
43
|
+
# without httpx installed (e.g. for --help).
|
|
44
|
+
_client = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_client():
|
|
48
|
+
"""Lazily build (and cache) the EverestAPI SDK client.
|
|
49
|
+
|
|
50
|
+
Reads ``EVEREST_API_KEY`` (or ``EIQ_API_KEY``) and ``EIQ_BASE_URL`` (or
|
|
51
|
+
``EVEREST_API_URL``) from the environment. Cloudflare Access service tokens
|
|
52
|
+
(``CF_ACCESS_CLIENT_ID`` / ``CF_ACCESS_CLIENT_SECRET``) are honoured by the
|
|
53
|
+
EverestAPI constructor, so a host behind CF Access (e.g. staging) works
|
|
54
|
+
with no extra wiring.
|
|
55
|
+
"""
|
|
56
|
+
global _client
|
|
57
|
+
if _client is None:
|
|
58
|
+
from everestapi import EverestAPI
|
|
59
|
+
|
|
60
|
+
api_key = os.environ.get("EVEREST_API_KEY") or os.environ.get("EIQ_API_KEY", "")
|
|
61
|
+
base_url = (
|
|
62
|
+
os.environ.get("EIQ_BASE_URL")
|
|
63
|
+
or os.environ.get("EVEREST_API_URL")
|
|
64
|
+
or "https://everestquant.ai"
|
|
65
|
+
)
|
|
66
|
+
if not api_key:
|
|
67
|
+
raise RuntimeError("EVEREST_API_KEY environment variable is required")
|
|
68
|
+
# EverestAPI reads CF_ACCESS_CLIENT_ID/SECRET from the environment in its
|
|
69
|
+
# own constructor, so staging (behind Cloudflare Access) works with no
|
|
70
|
+
# extra wiring here.
|
|
71
|
+
_client = EverestAPI(api_key=api_key, base_url=base_url)
|
|
72
|
+
return _client
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Tool definitions (shared by both MCP-SDK and fallback paths)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
TOOLS = [
|
|
80
|
+
{
|
|
81
|
+
"name": "get_universe",
|
|
82
|
+
"description": "Get the current EIQ tournament universe of instruments (tickers, sectors, asset classes).",
|
|
83
|
+
"inputSchema": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {},
|
|
86
|
+
"required": [],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "get_features",
|
|
91
|
+
"description": "Get obfuscated features for a specific date. Features are quintile-binned integers {0, 1, 2, 3, 4}.",
|
|
92
|
+
"inputSchema": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"date": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Date in YYYY-MM-DD format. Omit for latest.",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
"required": [],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "submit_predictions",
|
|
105
|
+
"description": "Submit daily predictions for the tournament. Scores must be in [-1, 1] and cover all instruments.",
|
|
106
|
+
"inputSchema": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {
|
|
109
|
+
"model_id": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Unique model identifier.",
|
|
112
|
+
},
|
|
113
|
+
"predictions": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"description": "Array of {ticker, score} objects.",
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"ticker": {"type": "string"},
|
|
120
|
+
"score": {"type": "number"},
|
|
121
|
+
},
|
|
122
|
+
"required": ["ticker", "score"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"required": ["model_id", "predictions"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "submit_futures_predictions",
|
|
131
|
+
"description": "Submit futures predictions (v2 single-prediction format) for the Himalayas tournament. Each instrument maps to a single float prediction for the primary target (target_everest_20). Final score = 0.75*CORR + 2.25*AIMC on a 20-day horizon. EAC (Exposure-Adjusted Correlation) is also computed as a diagnostic metric. PREREQS: (1) the model must already exist — call create_model first (submissions never auto-create one). (2) `exped` must be the LITERAL open-round exped, e.g. the value in the `exped` column of the 'live' dataset split (download_dataset universe=futures split=live) — do NOT pass 'current'. (3) predictions must cover ALL instrument ids in that live split (the dict keys are the live parquet's index ids).",
|
|
132
|
+
"inputSchema": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"model_id": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "Unique model identifier.",
|
|
138
|
+
},
|
|
139
|
+
"predictions": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"description": "Dict of {instrument_id: prediction} — single float per instrument for target_everest_20. Must cover all instruments in the universe.",
|
|
142
|
+
"additionalProperties": {"type": "number"},
|
|
143
|
+
},
|
|
144
|
+
"exped": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Exped identifier (e.g. 'exped_20260408'). Defaults to current round if omitted.",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"required": ["model_id", "predictions"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "get_scores",
|
|
154
|
+
"description": "Get scoring results (CORR, AIMC, EAC, payout) for a model over a look-back window. Each daily score entry includes corr, aimc, eac (Exposure-Adjusted Correlation), and payout.",
|
|
155
|
+
"inputSchema": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": {
|
|
158
|
+
"model_id": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "Model to query.",
|
|
161
|
+
},
|
|
162
|
+
"days": {
|
|
163
|
+
"type": "integer",
|
|
164
|
+
"description": "Look-back days (default 30).",
|
|
165
|
+
"default": 30,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
"required": ["model_id"],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "get_leaderboard",
|
|
173
|
+
"description": "Get the tournament leaderboard ranked by payout. Each entry includes mean_corr, mean_aimc, mean_eac (Exposure-Adjusted Correlation), and total_payout.",
|
|
174
|
+
"inputSchema": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"properties": {
|
|
177
|
+
"period": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"description": "Period filter: '7d', '30d', or 'all'. Default '30d'.",
|
|
180
|
+
"default": "30d",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
"required": [],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "get_dataset_schema",
|
|
188
|
+
"description": "Get the complete dataset schema: feature groups with descriptions, target definitions, exped structure, and feature sets (small/medium/all). Call this first to understand EIQ's data.",
|
|
189
|
+
"inputSchema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"version": {"type": "string", "description": "Dataset version (default: bregen)", "default": "bregen"},
|
|
193
|
+
},
|
|
194
|
+
"required": [],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "download_dataset",
|
|
199
|
+
"description": "Download a dataset split (train/validation/live) as a parquet file. Returns the local file path.",
|
|
200
|
+
"inputSchema": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"universe": {"type": "string", "description": "Universe: 'futures' or 'equities'"},
|
|
204
|
+
"split": {"type": "string", "description": "Split: 'train', 'validation', or 'live'"},
|
|
205
|
+
"output_path": {"type": "string", "description": "Local path to save (optional)"},
|
|
206
|
+
},
|
|
207
|
+
"required": ["universe", "split"],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "download_benchmark",
|
|
212
|
+
"description": "Download benchmark model predictions for comparison and AIMC computation.",
|
|
213
|
+
"inputSchema": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"properties": {
|
|
216
|
+
"universe": {"type": "string", "default": "futures"},
|
|
217
|
+
"split": {"type": "string", "default": "validation", "description": "'validation' or 'live'"},
|
|
218
|
+
"output_path": {"type": "string", "description": "Local path (optional)"},
|
|
219
|
+
},
|
|
220
|
+
"required": [],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "quick_train",
|
|
225
|
+
"description": "Train a model on EIQ data using serverless GPU. Returns job_id. Poll with get_job_status(). Models: lightgbm, xgboost, ridge, mlp. Costs $0.005-0.05. Futures payout target: target_everest_20 (20-day forward return). Final score = 0.75*CORR + 2.25*AIMC.",
|
|
226
|
+
"inputSchema": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"properties": {
|
|
229
|
+
"model": {"type": "string", "enum": ["lightgbm", "xgboost", "ridge", "mlp"]},
|
|
230
|
+
"features": {"type": "string", "enum": ["small", "medium", "all"], "default": "small"},
|
|
231
|
+
"target": {"type": "string", "default": "target_everest_20", "description": "Target column. Futures payout uses target_everest_20 (20-day forward return). Multiple target types available. See get_dataset_schema for full list."},
|
|
232
|
+
"universe": {"type": "string", "default": "futures"},
|
|
233
|
+
"params": {"type": "object", "description": "Model hyperparameters (optional)"},
|
|
234
|
+
},
|
|
235
|
+
"required": ["model"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "custom_train",
|
|
240
|
+
"description": "Run a custom Python script on a GPU pod. Pod has EverestAPI SDK + PyTorch + ML libs pre-installed. Costs $0.50-1.79/hr.",
|
|
241
|
+
"inputSchema": {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"script": {"type": "string", "description": "Python script content"},
|
|
245
|
+
"gpu": {"type": "string", "enum": ["T4", "A40", "A100"], "default": "T4"},
|
|
246
|
+
"max_hours": {"type": "number", "default": 1},
|
|
247
|
+
"requirements": {"type": "array", "items": {"type": "string"}},
|
|
248
|
+
},
|
|
249
|
+
"required": ["script"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "get_job_status",
|
|
254
|
+
"description": "Check status of a compute job. Returns status, cost, output files when complete.",
|
|
255
|
+
"inputSchema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"job_id": {"type": "string", "description": "Job ID from quick_train or custom_train"},
|
|
259
|
+
},
|
|
260
|
+
"required": ["job_id"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "get_model_download_url",
|
|
265
|
+
"description": "Get a presigned download URL for a trained model from a completed job. URL valid for 1 hour. Only you can download your own models.",
|
|
266
|
+
"inputSchema": {
|
|
267
|
+
"type": "object",
|
|
268
|
+
"properties": {
|
|
269
|
+
"job_id": {"type": "string", "description": "Job ID from quick_train or custom_train"},
|
|
270
|
+
},
|
|
271
|
+
"required": ["job_id"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "get_compute_credits",
|
|
276
|
+
"description": "Check your compute credit balance and usage. Purchase credits with USDC before using GPU resources.",
|
|
277
|
+
"inputSchema": {
|
|
278
|
+
"type": "object",
|
|
279
|
+
"properties": {},
|
|
280
|
+
"required": [],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "get_rounds",
|
|
285
|
+
"description": "Get tournament rounds and their statuses (open, closed, scoring, resolving, resolved). Rounds in 'resolving' state have estimated scores that update daily. Final scores are available after forward returns mature (~20 trading days).",
|
|
286
|
+
"inputSchema": {
|
|
287
|
+
"type": "object",
|
|
288
|
+
"properties": {
|
|
289
|
+
"tournament": {"type": "string", "description": "Tournament: 'equities' or 'futures'", "default": "equities"},
|
|
290
|
+
"limit": {"type": "integer", "description": "Max rounds to return (default 25)", "default": 25},
|
|
291
|
+
},
|
|
292
|
+
"required": [],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "get_current_round",
|
|
297
|
+
"description": "Get the current active round for a tournament.",
|
|
298
|
+
"inputSchema": {
|
|
299
|
+
"type": "object",
|
|
300
|
+
"properties": {
|
|
301
|
+
"tournament": {"type": "string", "description": "Tournament: 'equities' or 'futures'", "default": "equities"},
|
|
302
|
+
},
|
|
303
|
+
"required": [],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "get_schedule",
|
|
308
|
+
"description": "Get the submission schedule showing open/close times for upcoming rounds.",
|
|
309
|
+
"inputSchema": {
|
|
310
|
+
"type": "object",
|
|
311
|
+
"properties": {},
|
|
312
|
+
"required": [],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "upload_model",
|
|
317
|
+
"description": "Upload a trained .pkl model file for automated daily execution.",
|
|
318
|
+
"inputSchema": {
|
|
319
|
+
"type": "object",
|
|
320
|
+
"properties": {
|
|
321
|
+
"model_id": {"type": "string", "description": "Model identifier"},
|
|
322
|
+
"file_path": {"type": "string", "description": "Local path to the .pkl file"},
|
|
323
|
+
},
|
|
324
|
+
"required": ["model_id", "file_path"],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "get_upload_status",
|
|
329
|
+
"description": "Check the status of a model upload (pending, validating, validated, error).",
|
|
330
|
+
"inputSchema": {
|
|
331
|
+
"type": "object",
|
|
332
|
+
"properties": {
|
|
333
|
+
"model_id": {"type": "string", "description": "Model identifier"},
|
|
334
|
+
},
|
|
335
|
+
"required": ["model_id"],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"name": "create_model",
|
|
340
|
+
"description": "Register a model under your agent. REQUIRED before submitting predictions or uploading a .pkl — submissions reference a model by name and the platform never auto-creates one. The name you choose is the model_id you pass to submit_futures_predictions / upload_model. Idempotent: succeeds even if the model already exists.",
|
|
341
|
+
"inputSchema": {
|
|
342
|
+
"type": "object",
|
|
343
|
+
"properties": {
|
|
344
|
+
"name": {"type": "string", "description": "Model name (this becomes the model_id for submit/upload)."},
|
|
345
|
+
"description": {"type": "string", "description": "Optional human-readable description."},
|
|
346
|
+
},
|
|
347
|
+
"required": ["name"],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "get_models",
|
|
352
|
+
"description": "List the models registered under your agent (id, name, description, scores).",
|
|
353
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "set_multipliers",
|
|
357
|
+
"description": "Set CORR/AIMC payout multipliers for a model.",
|
|
358
|
+
"inputSchema": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"properties": {
|
|
361
|
+
"model_id": {"type": "string", "description": "Model identifier"},
|
|
362
|
+
"corr_multiplier": {"type": "number", "description": "CORR multiplier (0.0-5.0)"},
|
|
363
|
+
"aimc_multiplier": {"type": "number", "description": "AIMC multiplier (0.0-5.0)"},
|
|
364
|
+
},
|
|
365
|
+
"required": ["model_id", "corr_multiplier", "aimc_multiplier"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"name": "get_round_diagnostics",
|
|
370
|
+
"description": "Get per-round scoring breakdown (CORR, AIMC, EAC, payout) with round status. Each round includes corr, aimc, eac (Exposure-Adjusted Correlation), and payout. Scores in 'resolving' rounds are preliminary estimates based on partial forward returns.",
|
|
371
|
+
"inputSchema": {
|
|
372
|
+
"type": "object",
|
|
373
|
+
"properties": {
|
|
374
|
+
"model_id": {"type": "string", "description": "Model to query"},
|
|
375
|
+
},
|
|
376
|
+
"required": ["model_id"],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"name": "run_validation_diagnostics",
|
|
381
|
+
"description": "Get full validation diagnostics panel for a model: Sharpe ratio, mean CORR, mean EAC (Exposure-Adjusted Correlation), std dev, max drawdown, autocorrelation, and per-round CORR series.",
|
|
382
|
+
"inputSchema": {
|
|
383
|
+
"type": "object",
|
|
384
|
+
"properties": {
|
|
385
|
+
"model_id": {"type": "string", "description": "Model to run diagnostics on"},
|
|
386
|
+
"days": {"type": "integer", "description": "Number of days of history (default 365)", "default": 365},
|
|
387
|
+
},
|
|
388
|
+
"required": ["model_id"],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"name": "get_seasons",
|
|
393
|
+
"description": "Get tournament seasons and altitude zone rankings (summit, high_camp, climbing, basecamp).",
|
|
394
|
+
"inputSchema": {
|
|
395
|
+
"type": "object",
|
|
396
|
+
"properties": {},
|
|
397
|
+
"required": [],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"name": "get_benchmarks",
|
|
402
|
+
"description": "Get official benchmark models with per-slice validation metrics (val_sharpe_ann, full_sharpe_ann; annualised with sqrt(252) since expeds are daily). Data split: train / validation / live.",
|
|
403
|
+
"inputSchema": {
|
|
404
|
+
"type": "object",
|
|
405
|
+
"properties": {},
|
|
406
|
+
"required": [],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"name": "get_model_per_exped_breakdown",
|
|
411
|
+
"description": "Return the per-exped CORR and EAC series for a model (oldest→newest). Each exped includes corr and eac (Exposure-Adjusted Correlation). Use this to compute your own slice metrics or annualised Sharpe (sqrt(252) for daily expeds).",
|
|
412
|
+
"inputSchema": {
|
|
413
|
+
"type": "object",
|
|
414
|
+
"properties": {
|
|
415
|
+
"model_id": {"type": "string", "description": "Model to query"},
|
|
416
|
+
"days": {"type": "integer", "description": "History window in days (default 3650 = full tournament history)", "default": 3650},
|
|
417
|
+
},
|
|
418
|
+
"required": ["model_id"],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"name": "stake_on_model",
|
|
423
|
+
"description": "Stake USDC on a model to amplify payouts.",
|
|
424
|
+
"inputSchema": {
|
|
425
|
+
"type": "object",
|
|
426
|
+
"properties": {
|
|
427
|
+
"model_id": {"type": "string", "description": "Model to stake on."},
|
|
428
|
+
"amount_usdc": {"type": "number", "description": "Amount of USDC to stake."},
|
|
429
|
+
"wallet_address": {"type": "string", "description": "On-chain wallet address holding the USDC."},
|
|
430
|
+
},
|
|
431
|
+
"required": ["model_id", "amount_usdc", "wallet_address"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"name": "unstake_from_model",
|
|
436
|
+
"description": "Withdraw a stake from a model.",
|
|
437
|
+
"inputSchema": {
|
|
438
|
+
"type": "object",
|
|
439
|
+
"properties": {
|
|
440
|
+
"stake_id": {"type": "string", "description": "Stake ID returned by stake_on_model."},
|
|
441
|
+
},
|
|
442
|
+
"required": ["stake_id"],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"name": "get_stake_balance",
|
|
447
|
+
"description": "Get current stake balance and pending payouts for a model.",
|
|
448
|
+
"inputSchema": {
|
|
449
|
+
"type": "object",
|
|
450
|
+
"properties": {
|
|
451
|
+
"model_id": {"type": "string", "description": "Model to query."},
|
|
452
|
+
},
|
|
453
|
+
"required": ["model_id"],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
"name": "claim_payout",
|
|
458
|
+
"description": "Claim a resolved staking payout for a model and round.",
|
|
459
|
+
"inputSchema": {
|
|
460
|
+
"type": "object",
|
|
461
|
+
"properties": {
|
|
462
|
+
"model_id": {"type": "string", "description": "Model identifier."},
|
|
463
|
+
"round_id": {"type": "string", "description": "Round identifier for the payout to claim."},
|
|
464
|
+
},
|
|
465
|
+
"required": ["model_id", "round_id"],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "get_staking_history",
|
|
470
|
+
"description": "Get full stake/unstake/claim transaction history for a model.",
|
|
471
|
+
"inputSchema": {
|
|
472
|
+
"type": "object",
|
|
473
|
+
"properties": {
|
|
474
|
+
"model_id": {"type": "string", "description": "Model to query."},
|
|
475
|
+
},
|
|
476
|
+
"required": ["model_id"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
# -- CREATE2 staking tools -----------------------------------------------
|
|
480
|
+
{
|
|
481
|
+
"name": "get_deposit_address",
|
|
482
|
+
"description": "Get your unique USDC deposit address for staking. Send USDC to this address, then call relay_stake to forward it to the tournament.",
|
|
483
|
+
"inputSchema": {
|
|
484
|
+
"type": "object",
|
|
485
|
+
"properties": {},
|
|
486
|
+
"required": [],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"name": "relay_stake",
|
|
491
|
+
"description": "Forward USDC from your deposit address to the staking contract. You must first send USDC to your deposit address (from get_deposit_address).",
|
|
492
|
+
"inputSchema": {
|
|
493
|
+
"type": "object",
|
|
494
|
+
"properties": {
|
|
495
|
+
"model_id": {"type": "string", "description": "Model to stake on."},
|
|
496
|
+
},
|
|
497
|
+
"required": ["model_id"],
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"name": "relay_claim",
|
|
502
|
+
"description": "Claim your payout from a resolved tournament round. USDC will be sent to your deposit address.",
|
|
503
|
+
"inputSchema": {
|
|
504
|
+
"type": "object",
|
|
505
|
+
"properties": {
|
|
506
|
+
"round_id": {"type": "integer", "description": "Round identifier for the payout to claim."},
|
|
507
|
+
},
|
|
508
|
+
"required": ["round_id"],
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"name": "withdraw_usdc",
|
|
513
|
+
"description": "Withdraw USDC from your deposit address to an external wallet.",
|
|
514
|
+
"inputSchema": {
|
|
515
|
+
"type": "object",
|
|
516
|
+
"properties": {
|
|
517
|
+
"to_address": {"type": "string", "description": "Destination wallet address."},
|
|
518
|
+
"amount_usdc": {"type": "number", "description": "Amount of USDC to withdraw."},
|
|
519
|
+
},
|
|
520
|
+
"required": ["to_address", "amount_usdc"],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"name": "get_forwarder_balance",
|
|
525
|
+
"description": "Check your deposit address USDC balance and view your active/resolved stakes.",
|
|
526
|
+
"inputSchema": {
|
|
527
|
+
"type": "object",
|
|
528
|
+
"properties": {},
|
|
529
|
+
"required": [],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
# -- notifications & badges ---------------------------------------------
|
|
533
|
+
{
|
|
534
|
+
"name": "get_notifications",
|
|
535
|
+
"description": "Get your unread notifications — badge awards, altitude changes, round scores. Call this at the start of each session to check for updates.",
|
|
536
|
+
"inputSchema": {
|
|
537
|
+
"type": "object",
|
|
538
|
+
"properties": {
|
|
539
|
+
"unread_only": {
|
|
540
|
+
"type": "boolean",
|
|
541
|
+
"description": "If true, only return unread notifications",
|
|
542
|
+
"default": True,
|
|
543
|
+
},
|
|
544
|
+
"limit": {
|
|
545
|
+
"type": "integer",
|
|
546
|
+
"description": "Max notifications to return",
|
|
547
|
+
"default": 20,
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"name": "get_badges",
|
|
554
|
+
"description": "Get all achievement badges earned by your models. Badges are permanent milestones: First Ascent, Acclimatised, Iron Lungs, Death Zone, Oxygen Tank, Sherpa, Summit Push, Everest Summiteer.",
|
|
555
|
+
"inputSchema": {
|
|
556
|
+
"type": "object",
|
|
557
|
+
"properties": {
|
|
558
|
+
"model_id": {
|
|
559
|
+
"type": "string",
|
|
560
|
+
"description": "Filter badges for a specific model (optional)",
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _dispatch(name: str, arguments: dict) -> str:
|
|
569
|
+
"""Call the appropriate SDK method and return JSON string."""
|
|
570
|
+
client = _get_client()
|
|
571
|
+
|
|
572
|
+
if name == "get_universe":
|
|
573
|
+
result = client.get_universe()
|
|
574
|
+
elif name == "get_features":
|
|
575
|
+
result = client.get_features(date=arguments.get("date"))
|
|
576
|
+
elif name == "submit_predictions":
|
|
577
|
+
result = client.submit_predictions(
|
|
578
|
+
model_id=arguments["model_id"],
|
|
579
|
+
predictions=arguments["predictions"],
|
|
580
|
+
)
|
|
581
|
+
elif name == "submit_futures_predictions":
|
|
582
|
+
result = client.submit_futures_predictions(
|
|
583
|
+
model_id=arguments["model_id"],
|
|
584
|
+
predictions=arguments["predictions"],
|
|
585
|
+
exped=arguments.get("exped"),
|
|
586
|
+
)
|
|
587
|
+
elif name == "get_scores":
|
|
588
|
+
result = client.get_scores(
|
|
589
|
+
model_id=arguments["model_id"],
|
|
590
|
+
days=arguments.get("days", 30),
|
|
591
|
+
)
|
|
592
|
+
elif name == "get_leaderboard":
|
|
593
|
+
result = client.get_leaderboard(period=arguments.get("period", "30d"))
|
|
594
|
+
elif name == "get_dataset_schema":
|
|
595
|
+
result = client.get_dataset_schema(version=arguments.get("version", "bregen"))
|
|
596
|
+
elif name == "download_dataset":
|
|
597
|
+
path = client.download_dataset(
|
|
598
|
+
universe=arguments.get("universe", "futures"),
|
|
599
|
+
split=arguments.get("split", "train"),
|
|
600
|
+
output_path=arguments.get("output_path"),
|
|
601
|
+
)
|
|
602
|
+
result = {"file_path": path, "status": "downloaded"}
|
|
603
|
+
elif name == "download_benchmark":
|
|
604
|
+
path = client.download_benchmark(
|
|
605
|
+
universe=arguments.get("universe", "futures"),
|
|
606
|
+
split=arguments.get("split", "validation"),
|
|
607
|
+
output_path=arguments.get("output_path"),
|
|
608
|
+
)
|
|
609
|
+
result = {"file_path": path, "status": "downloaded"}
|
|
610
|
+
elif name == "quick_train":
|
|
611
|
+
result = client.quick_train(
|
|
612
|
+
model=arguments["model"],
|
|
613
|
+
features=arguments.get("features", "small"),
|
|
614
|
+
target=arguments.get("target", "target_everest_20"),
|
|
615
|
+
universe=arguments.get("universe", "futures"),
|
|
616
|
+
params=arguments.get("params"),
|
|
617
|
+
)
|
|
618
|
+
elif name == "custom_train":
|
|
619
|
+
result = client.custom_train(
|
|
620
|
+
script=arguments.get("script"),
|
|
621
|
+
gpu=arguments.get("gpu", "T4"),
|
|
622
|
+
max_hours=arguments.get("max_hours", 1),
|
|
623
|
+
requirements=arguments.get("requirements"),
|
|
624
|
+
)
|
|
625
|
+
elif name == "get_job_status":
|
|
626
|
+
result = client.get_job_status(job_id=arguments["job_id"])
|
|
627
|
+
elif name == "get_model_download_url":
|
|
628
|
+
result = client.get_model_download_url(job_id=arguments["job_id"])
|
|
629
|
+
elif name == "get_compute_credits":
|
|
630
|
+
result = client.get_compute_credits()
|
|
631
|
+
elif name == "get_rounds":
|
|
632
|
+
result = client.get_rounds(
|
|
633
|
+
tournament=arguments.get("tournament", "equities"),
|
|
634
|
+
limit=arguments.get("limit", 25),
|
|
635
|
+
)
|
|
636
|
+
elif name == "get_current_round":
|
|
637
|
+
result = client.get_current_round(
|
|
638
|
+
tournament=arguments.get("tournament", "equities"),
|
|
639
|
+
)
|
|
640
|
+
elif name == "get_schedule":
|
|
641
|
+
result = client.get_schedule()
|
|
642
|
+
elif name == "upload_model":
|
|
643
|
+
result = client.upload_model(
|
|
644
|
+
model_id=arguments["model_id"],
|
|
645
|
+
file_path=arguments["file_path"],
|
|
646
|
+
)
|
|
647
|
+
elif name == "get_upload_status":
|
|
648
|
+
result = client.get_upload_status(model_id=arguments["model_id"])
|
|
649
|
+
elif name == "create_model":
|
|
650
|
+
result = client.create_model(
|
|
651
|
+
name=arguments["name"],
|
|
652
|
+
description=arguments.get("description"),
|
|
653
|
+
)
|
|
654
|
+
elif name == "get_models":
|
|
655
|
+
result = client.get_models()
|
|
656
|
+
elif name == "set_multipliers":
|
|
657
|
+
result = client.set_multipliers(
|
|
658
|
+
model_id=arguments["model_id"],
|
|
659
|
+
corr_mult=arguments["corr_multiplier"],
|
|
660
|
+
aimc_mult=arguments["aimc_multiplier"],
|
|
661
|
+
)
|
|
662
|
+
elif name == "get_round_diagnostics":
|
|
663
|
+
result = client.get_round_diagnostics(model_id=arguments["model_id"])
|
|
664
|
+
elif name == "run_validation_diagnostics":
|
|
665
|
+
result = client.get_validation_diagnostics(
|
|
666
|
+
model_id=arguments["model_id"],
|
|
667
|
+
days=arguments.get("days", 365),
|
|
668
|
+
)
|
|
669
|
+
elif name == "get_seasons":
|
|
670
|
+
result = client.get_seasons()
|
|
671
|
+
elif name == "get_benchmarks":
|
|
672
|
+
result = client.get_benchmarks()
|
|
673
|
+
elif name == "get_model_per_exped_breakdown":
|
|
674
|
+
diag = client.get_validation_diagnostics(
|
|
675
|
+
model_id=arguments["model_id"],
|
|
676
|
+
days=arguments.get("days", 3650),
|
|
677
|
+
)
|
|
678
|
+
result = {
|
|
679
|
+
"model_id": arguments["model_id"],
|
|
680
|
+
"per_exped_corr": diag.get("per_exped_corr", []),
|
|
681
|
+
"per_exped_eac": diag.get("per_exped_eac", []),
|
|
682
|
+
}
|
|
683
|
+
elif name == "stake_on_model":
|
|
684
|
+
result = client.stake(
|
|
685
|
+
model_id=arguments["model_id"],
|
|
686
|
+
amount_usdc=arguments["amount_usdc"],
|
|
687
|
+
wallet_address=arguments["wallet_address"],
|
|
688
|
+
)
|
|
689
|
+
elif name == "unstake_from_model":
|
|
690
|
+
result = client.unstake(stake_id=arguments["stake_id"])
|
|
691
|
+
elif name == "get_stake_balance":
|
|
692
|
+
result = client.get_stake_balance(model_id=arguments["model_id"])
|
|
693
|
+
elif name == "claim_payout":
|
|
694
|
+
result = client.claim_payout(
|
|
695
|
+
model_id=arguments["model_id"],
|
|
696
|
+
round_id=arguments["round_id"],
|
|
697
|
+
)
|
|
698
|
+
elif name == "get_staking_history":
|
|
699
|
+
result = client.get_staking_history(model_id=arguments["model_id"])
|
|
700
|
+
# -- CREATE2 staking dispatch -----------------------------------------
|
|
701
|
+
elif name == "get_deposit_address":
|
|
702
|
+
result = client.get_deposit_address()
|
|
703
|
+
elif name == "relay_stake":
|
|
704
|
+
result = client.relay_stake(model_id=arguments["model_id"])
|
|
705
|
+
elif name == "relay_claim":
|
|
706
|
+
result = client.relay_claim(round_id=arguments["round_id"])
|
|
707
|
+
elif name == "withdraw_usdc":
|
|
708
|
+
result = client.withdraw_usdc(
|
|
709
|
+
to_address=arguments["to_address"],
|
|
710
|
+
amount_usdc=arguments["amount_usdc"],
|
|
711
|
+
)
|
|
712
|
+
elif name == "get_forwarder_balance":
|
|
713
|
+
result = client.get_forwarder_balance()
|
|
714
|
+
# -- notifications & badges dispatch ------------------------------------
|
|
715
|
+
elif name == "get_notifications":
|
|
716
|
+
result = client.get_notifications(
|
|
717
|
+
unread_only=arguments.get("unread_only", True),
|
|
718
|
+
limit=arguments.get("limit", 20),
|
|
719
|
+
)
|
|
720
|
+
elif name == "get_badges":
|
|
721
|
+
result = client.get_badges(model_id=arguments.get("model_id"))
|
|
722
|
+
else:
|
|
723
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
724
|
+
|
|
725
|
+
return json.dumps(result, indent=2)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
# ===================================================================
|
|
729
|
+
# MCP-SDK path (preferred)
|
|
730
|
+
# ===================================================================
|
|
731
|
+
|
|
732
|
+
def _build_mcp_server() -> Server:
|
|
733
|
+
"""Build and return a configured MCP Server instance."""
|
|
734
|
+
server = Server("eiq-mcp")
|
|
735
|
+
|
|
736
|
+
@server.list_tools()
|
|
737
|
+
async def list_tools() -> list[Tool]:
|
|
738
|
+
return [
|
|
739
|
+
Tool(
|
|
740
|
+
name=t["name"],
|
|
741
|
+
description=t["description"],
|
|
742
|
+
inputSchema=t["inputSchema"],
|
|
743
|
+
)
|
|
744
|
+
for t in TOOLS
|
|
745
|
+
]
|
|
746
|
+
|
|
747
|
+
@server.call_tool()
|
|
748
|
+
async def call_tool(name: str, arguments: dict):
|
|
749
|
+
try:
|
|
750
|
+
text = _dispatch(name, arguments)
|
|
751
|
+
return [TextContent(type="text", text=text)]
|
|
752
|
+
except Exception as exc:
|
|
753
|
+
return CallToolResult(
|
|
754
|
+
content=[TextContent(type="text", text=json.dumps({"error": str(exc)}))],
|
|
755
|
+
isError=True,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
return server
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
async def _run_mcp() -> None:
|
|
762
|
+
"""Run the MCP server over stdin/stdout."""
|
|
763
|
+
server = _build_mcp_server()
|
|
764
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
765
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
# ===================================================================
|
|
769
|
+
# Fallback: minimal JSON-RPC 2.0 over stdin/stdout
|
|
770
|
+
# ===================================================================
|
|
771
|
+
|
|
772
|
+
def _run_jsonrpc_fallback() -> None:
|
|
773
|
+
"""Minimal JSON-RPC 2.0 loop for environments without the ``mcp`` package."""
|
|
774
|
+
import sys
|
|
775
|
+
|
|
776
|
+
sys.stderr.write("MCP SDK not found — running JSON-RPC 2.0 fallback\n")
|
|
777
|
+
|
|
778
|
+
for line in sys.stdin:
|
|
779
|
+
line = line.strip()
|
|
780
|
+
if not line:
|
|
781
|
+
continue
|
|
782
|
+
try:
|
|
783
|
+
req = json.loads(line)
|
|
784
|
+
except json.JSONDecodeError:
|
|
785
|
+
_jsonrpc_respond(None, error={"code": -32700, "message": "Parse error"})
|
|
786
|
+
continue
|
|
787
|
+
|
|
788
|
+
req_id = req.get("id")
|
|
789
|
+
method = req.get("method", "")
|
|
790
|
+
params = req.get("params", {})
|
|
791
|
+
|
|
792
|
+
if method == "initialize":
|
|
793
|
+
_jsonrpc_respond(req_id, result={
|
|
794
|
+
"protocolVersion": "2024-11-05",
|
|
795
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
796
|
+
"serverInfo": {"name": "eiq-mcp", "version": "0.1.0"},
|
|
797
|
+
})
|
|
798
|
+
elif method == "tools/list":
|
|
799
|
+
_jsonrpc_respond(req_id, result={"tools": TOOLS})
|
|
800
|
+
elif method == "tools/call":
|
|
801
|
+
tool_name = params.get("name", "")
|
|
802
|
+
arguments = params.get("arguments", {})
|
|
803
|
+
try:
|
|
804
|
+
text = _dispatch(tool_name, arguments)
|
|
805
|
+
_jsonrpc_respond(req_id, result={
|
|
806
|
+
"content": [{"type": "text", "text": text}],
|
|
807
|
+
})
|
|
808
|
+
except Exception as exc:
|
|
809
|
+
_jsonrpc_respond(req_id, result={
|
|
810
|
+
"content": [{"type": "text", "text": json.dumps({"error": str(exc)})}],
|
|
811
|
+
"isError": True,
|
|
812
|
+
})
|
|
813
|
+
elif method == "notifications/initialized":
|
|
814
|
+
pass # no response needed for notifications
|
|
815
|
+
else:
|
|
816
|
+
_jsonrpc_respond(req_id, error={"code": -32601, "message": f"Unknown method: {method}"})
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _jsonrpc_respond(req_id, *, result=None, error=None) -> None:
|
|
820
|
+
"""Write a JSON-RPC 2.0 response to stdout."""
|
|
821
|
+
resp: dict = {"jsonrpc": "2.0", "id": req_id}
|
|
822
|
+
if error is not None:
|
|
823
|
+
resp["error"] = error
|
|
824
|
+
else:
|
|
825
|
+
resp["result"] = result
|
|
826
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
827
|
+
sys.stdout.flush()
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ===================================================================
|
|
831
|
+
# Entry point
|
|
832
|
+
# ===================================================================
|
|
833
|
+
|
|
834
|
+
def main() -> None:
|
|
835
|
+
"""Run the MCP server over stdio (the transport Claude Code / Cursor use)."""
|
|
836
|
+
if _HAS_MCP:
|
|
837
|
+
import asyncio
|
|
838
|
+
asyncio.run(_run_mcp())
|
|
839
|
+
else:
|
|
840
|
+
_run_jsonrpc_fallback()
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
if __name__ == "__main__":
|
|
844
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: everestapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python SDK for the EverestQuant prediction tournament platform
|
|
5
5
|
Author-email: EverestQuant <engineering@everestquant.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,6 +24,8 @@ Requires-Dist: click>=8.0
|
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pytest-httpx>=0.34.0; extra == "dev"
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
27
29
|
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# everestapi
|
|
@@ -12,5 +12,9 @@ src/everestapi.egg-info/dependency_links.txt
|
|
|
12
12
|
src/everestapi.egg-info/entry_points.txt
|
|
13
13
|
src/everestapi.egg-info/requires.txt
|
|
14
14
|
src/everestapi.egg-info/top_level.txt
|
|
15
|
+
src/everestapi/mcp/__init__.py
|
|
16
|
+
src/everestapi/mcp/__main__.py
|
|
17
|
+
src/everestapi/mcp/server.py
|
|
15
18
|
tests/test_cli.py
|
|
16
|
-
tests/test_client.py
|
|
19
|
+
tests/test_client.py
|
|
20
|
+
tests/test_mcp_and_models.py
|
|
@@ -88,3 +88,49 @@ def test_safe_output_path_rejects_traversal(api, tmp_path):
|
|
|
88
88
|
with pytest.raises(ValueError):
|
|
89
89
|
_safe_output_path("..", server_supplied=True)
|
|
90
90
|
assert _safe_output_path(str(tmp_path / "ok.bin")) == str(tmp_path / "ok.bin")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- Cloudflare Access service token ---------------------------------------
|
|
94
|
+
# Staging sits behind Cloudflare Access; a service token clears the gate
|
|
95
|
+
# programmatically. Headers ride alongside X-API-Key and stay absent by default.
|
|
96
|
+
|
|
97
|
+
_K = "eiq_test_key"
|
|
98
|
+
_URL = "http://test"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_cf_service_token_absent_by_default(monkeypatch):
|
|
102
|
+
monkeypatch.delenv("CF_ACCESS_CLIENT_ID", raising=False)
|
|
103
|
+
monkeypatch.delenv("CF_ACCESS_CLIENT_SECRET", raising=False)
|
|
104
|
+
client = EverestAPI(api_key=_K, base_url=_URL)
|
|
105
|
+
h = client._client.headers
|
|
106
|
+
assert "cf-access-client-id" not in h
|
|
107
|
+
assert "cf-access-client-secret" not in h
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_cf_service_token_from_env(monkeypatch):
|
|
111
|
+
cfid, cftok = "abc.access", "shh"
|
|
112
|
+
monkeypatch.setenv("CF_ACCESS_CLIENT_ID", cfid)
|
|
113
|
+
monkeypatch.setenv("CF_ACCESS_CLIENT_SECRET", cftok)
|
|
114
|
+
client = EverestAPI(api_key=_K, base_url=_URL)
|
|
115
|
+
h = client._client.headers
|
|
116
|
+
assert h["cf-access-client-id"] == cfid
|
|
117
|
+
assert h["cf-access-client-secret"] == cftok
|
|
118
|
+
assert h["x-api-key"] == _K
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_cf_service_token_kwargs_override_env(monkeypatch):
|
|
122
|
+
monkeypatch.setenv("CF_ACCESS_CLIENT_ID", "env.access")
|
|
123
|
+
monkeypatch.setenv("CF_ACCESS_CLIENT_SECRET", "envv")
|
|
124
|
+
kid, ksec = "kw.access", "kwv"
|
|
125
|
+
client = EverestAPI(api_key=_K, base_url=_URL,
|
|
126
|
+
cf_access_client_id=kid, cf_access_client_secret=ksec)
|
|
127
|
+
h = client._client.headers
|
|
128
|
+
assert h["cf-access-client-id"] == kid
|
|
129
|
+
assert h["cf-access-client-secret"] == ksec
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_cf_service_token_needs_both_halves(monkeypatch):
|
|
133
|
+
monkeypatch.setenv("CF_ACCESS_CLIENT_ID", "abc.access")
|
|
134
|
+
monkeypatch.delenv("CF_ACCESS_CLIENT_SECRET", raising=False)
|
|
135
|
+
client = EverestAPI(api_key=_K, base_url=_URL)
|
|
136
|
+
assert "cf-access-client-id" not in client._client.headers
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Tests for the MCP server module + the model-management / versioned-download
|
|
2
|
+
client methods added alongside it (everestapi 0.2.0)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from everestapi import EverestAPI, EverestError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def api(monkeypatch):
|
|
15
|
+
for var in (
|
|
16
|
+
"EVEREST_API_URL",
|
|
17
|
+
"EVEREST_API_KEY",
|
|
18
|
+
"CF_ACCESS_CLIENT_ID",
|
|
19
|
+
"CF_ACCESS_CLIENT_SECRET",
|
|
20
|
+
"EIQ_BASIC_AUTH_USER",
|
|
21
|
+
"EIQ_BASIC_AUTH_PASS",
|
|
22
|
+
):
|
|
23
|
+
monkeypatch.delenv(var, raising=False)
|
|
24
|
+
return EverestAPI(api_key="eiq_test_key", base_url="http://test")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# -- client: model management --------------------------------------------
|
|
28
|
+
|
|
29
|
+
def test_create_model_posts_body(httpx_mock, api):
|
|
30
|
+
httpx_mock.add_response(
|
|
31
|
+
url="http://test/api/v1/agents/me/models", method="POST",
|
|
32
|
+
json={"id": "m1", "name": "my-model"},
|
|
33
|
+
)
|
|
34
|
+
out = api.create_model("my-model", description="d")
|
|
35
|
+
assert out["name"] == "my-model"
|
|
36
|
+
assert json.loads(httpx_mock.get_request().content) == {
|
|
37
|
+
"name": "my-model", "description": "d",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_create_model_idempotent_on_409(httpx_mock, api):
|
|
42
|
+
httpx_mock.add_response(
|
|
43
|
+
url="http://test/api/v1/agents/me/models", method="POST",
|
|
44
|
+
status_code=409, json={"detail": "exists"},
|
|
45
|
+
)
|
|
46
|
+
assert api.create_model("dup") == {"name": "dup", "status": "already_exists"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_create_model_reraises_non_409(httpx_mock, api):
|
|
50
|
+
httpx_mock.add_response(
|
|
51
|
+
url="http://test/api/v1/agents/me/models", method="POST",
|
|
52
|
+
status_code=400, json={"detail": "bad"},
|
|
53
|
+
)
|
|
54
|
+
with pytest.raises(EverestError):
|
|
55
|
+
api.create_model("x")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_get_models(httpx_mock, api):
|
|
59
|
+
httpx_mock.add_response(
|
|
60
|
+
url="http://test/api/v1/agents/me/models", method="GET",
|
|
61
|
+
json={"agent_id": "a", "models": []},
|
|
62
|
+
)
|
|
63
|
+
assert api.get_models()["models"] == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# -- client: versioned dataset download (the #558 fix) -------------------
|
|
67
|
+
|
|
68
|
+
def test_download_dataset_uses_versioned_route(httpx_mock, api, tmp_path):
|
|
69
|
+
httpx_mock.add_response(
|
|
70
|
+
url="http://test/api/v1/data/download/bregen/futures/train",
|
|
71
|
+
content=b"PAR1data",
|
|
72
|
+
)
|
|
73
|
+
out = api.download_dataset(
|
|
74
|
+
universe="futures", split="train", version="bregen",
|
|
75
|
+
output_path=str(tmp_path / "t.parquet"),
|
|
76
|
+
)
|
|
77
|
+
assert httpx_mock.get_request().url.path == "/api/v1/data/download/bregen/futures/train"
|
|
78
|
+
with open(out, "rb") as f:
|
|
79
|
+
assert f.read() == b"PAR1data"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_download_dataset_latest_resolves_version(httpx_mock, api, tmp_path):
|
|
83
|
+
httpx_mock.add_response(
|
|
84
|
+
url="http://test/api/v1/data/versions",
|
|
85
|
+
json={"versions": [{"version": "bregen", "universes": ["futures"]}]},
|
|
86
|
+
)
|
|
87
|
+
httpx_mock.add_response(
|
|
88
|
+
url="http://test/api/v1/data/download/bregen/futures/live",
|
|
89
|
+
content=b"PAR1live",
|
|
90
|
+
)
|
|
91
|
+
out = api.download_dataset(
|
|
92
|
+
universe="futures", split="live", version="latest",
|
|
93
|
+
output_path=str(tmp_path / "l.parquet"),
|
|
94
|
+
)
|
|
95
|
+
with open(out, "rb") as f:
|
|
96
|
+
assert f.read() == b"PAR1live"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# -- MCP server -----------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def test_mcp_tools_include_loop_and_model_tools():
|
|
102
|
+
from everestapi.mcp import server
|
|
103
|
+
names = {t["name"] for t in server.TOOLS}
|
|
104
|
+
for needed in (
|
|
105
|
+
"download_dataset", "create_model", "get_models",
|
|
106
|
+
"submit_futures_predictions", "get_current_round", "get_scores",
|
|
107
|
+
):
|
|
108
|
+
assert needed in names, needed
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_mcp_dispatch_create_model_uses_client(monkeypatch):
|
|
112
|
+
from everestapi.mcp import server
|
|
113
|
+
|
|
114
|
+
class FakeClient:
|
|
115
|
+
def create_model(self, name, description=None):
|
|
116
|
+
return {"name": name, "description": description}
|
|
117
|
+
|
|
118
|
+
monkeypatch.setattr(server, "_client", FakeClient())
|
|
119
|
+
out = json.loads(server._dispatch("create_model", {"name": "z", "description": "d"}))
|
|
120
|
+
assert out == {"name": "z", "description": "d"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|