ctxprotocol 0.5.5__tar.gz → 0.5.6__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.
- ctxprotocol-0.5.6/.gitignore +50 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/PKG-INFO +15 -6
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/README.md +14 -5
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/auth/__init__.py +34 -7
- ctxprotocol-0.5.6/examples/server/hummingbot-contributor/README.md +207 -0
- ctxprotocol-0.5.6/examples/server/hummingbot-contributor/env.example +15 -0
- ctxprotocol-0.5.6/examples/server/hummingbot-contributor/requirements.txt +20 -0
- ctxprotocol-0.5.6/examples/server/hummingbot-contributor/server.py +784 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/pyproject.toml +1 -1
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/__init__.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/__init__.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/client.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/__init__.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/discovery.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/tools.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/types.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/__init__.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/hyperliquid.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/polymarket.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/wallet.py +0 -0
- {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/py.typed +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.env
|
|
25
|
+
.venv
|
|
26
|
+
env/
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# IDE
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.tox/
|
|
38
|
+
.coverage
|
|
39
|
+
.coverage.*
|
|
40
|
+
htmlcov/
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
|
|
43
|
+
# md files
|
|
44
|
+
*.md
|
|
45
|
+
!README.md
|
|
46
|
+
!docs/mcp-builder-template.md
|
|
47
|
+
|
|
48
|
+
# Shell scripts (deployment)
|
|
49
|
+
*.sh
|
|
50
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxprotocol
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically
|
|
5
5
|
Project-URL: Homepage, https://ctxprotocol.com
|
|
6
6
|
Project-URL: Documentation, https://docs.ctxprotocol.com
|
|
@@ -263,15 +263,24 @@ if is_protected_mcp_method(body["method"]):
|
|
|
263
263
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
-
### Security Model
|
|
266
|
+
### MCP Security Model
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
|
271
|
-
|
|
268
|
+
The SDK implements a **selective authentication** model — discovery is open, execution is protected:
|
|
269
|
+
|
|
270
|
+
| MCP Method | Auth Required | Why |
|
|
271
|
+
|------------|---------------|-----|
|
|
272
272
|
| `initialize` | ❌ No | Session setup |
|
|
273
|
+
| `tools/list` | ❌ No | Discovery - agents need to see your schemas |
|
|
273
274
|
| `resources/list` | ❌ No | Discovery |
|
|
274
275
|
| `prompts/list` | ❌ No | Discovery |
|
|
276
|
+
| `tools/call` | ✅ **Yes** | **Execution - costs money, runs your code** |
|
|
277
|
+
|
|
278
|
+
**What this means in practice:**
|
|
279
|
+
- ✅ `https://your-mcp.com/mcp` + `initialize` → Works without auth
|
|
280
|
+
- ✅ `https://your-mcp.com/mcp` + `tools/list` → Works without auth
|
|
281
|
+
- ❌ `https://your-mcp.com/mcp` + `tools/call` → **Requires Context Protocol JWT**
|
|
282
|
+
|
|
283
|
+
This matches standard API patterns (OpenAPI schemas are public, GraphQL introspection is open).
|
|
275
284
|
|
|
276
285
|
## Context Injection (Personalized Tools)
|
|
277
286
|
|
|
@@ -225,15 +225,24 @@ if is_protected_mcp_method(body["method"]):
|
|
|
225
225
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
### Security Model
|
|
228
|
+
### MCP Security Model
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
|
233
|
-
|
|
230
|
+
The SDK implements a **selective authentication** model — discovery is open, execution is protected:
|
|
231
|
+
|
|
232
|
+
| MCP Method | Auth Required | Why |
|
|
233
|
+
|------------|---------------|-----|
|
|
234
234
|
| `initialize` | ❌ No | Session setup |
|
|
235
|
+
| `tools/list` | ❌ No | Discovery - agents need to see your schemas |
|
|
235
236
|
| `resources/list` | ❌ No | Discovery |
|
|
236
237
|
| `prompts/list` | ❌ No | Discovery |
|
|
238
|
+
| `tools/call` | ✅ **Yes** | **Execution - costs money, runs your code** |
|
|
239
|
+
|
|
240
|
+
**What this means in practice:**
|
|
241
|
+
- ✅ `https://your-mcp.com/mcp` + `initialize` → Works without auth
|
|
242
|
+
- ✅ `https://your-mcp.com/mcp` + `tools/list` → Works without auth
|
|
243
|
+
- ❌ `https://your-mcp.com/mcp` + `tools/call` → **Requires Context Protocol JWT**
|
|
244
|
+
|
|
245
|
+
This matches standard API patterns (OpenAPI schemas are public, GraphQL introspection is open).
|
|
237
246
|
|
|
238
247
|
## Context Injection (Personalized Tools)
|
|
239
248
|
|
|
@@ -160,17 +160,32 @@ async def verify_context_request(
|
|
|
160
160
|
CONTEXT_PLATFORM_PUBLIC_KEY_PEM.encode()
|
|
161
161
|
)
|
|
162
162
|
|
|
163
|
+
# Build decode options - match TypeScript SDK behavior
|
|
164
|
+
decode_options: dict[str, Any] = {
|
|
165
|
+
"verify_signature": True,
|
|
166
|
+
"verify_exp": True,
|
|
167
|
+
"verify_iat": True,
|
|
168
|
+
"require": ["exp", "iat"],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Only verify issuer if we expect it (TypeScript SDK does this)
|
|
172
|
+
# But don't require it in case the platform doesn't always include it
|
|
173
|
+
decode_options["verify_iss"] = True
|
|
174
|
+
|
|
175
|
+
# Only verify audience if explicitly provided
|
|
176
|
+
if audience:
|
|
177
|
+
decode_options["verify_aud"] = True
|
|
178
|
+
else:
|
|
179
|
+
decode_options["verify_aud"] = False
|
|
180
|
+
|
|
163
181
|
# Verify the JWT
|
|
164
182
|
payload = jwt.decode(
|
|
165
183
|
token,
|
|
166
184
|
public_key,
|
|
167
185
|
algorithms=["RS256"],
|
|
168
186
|
issuer="https://ctxprotocol.com",
|
|
169
|
-
audience=audience,
|
|
170
|
-
options=
|
|
171
|
-
"require": ["iss", "sub", "exp", "iat"],
|
|
172
|
-
"verify_aud": audience is not None,
|
|
173
|
-
},
|
|
187
|
+
audience=audience if audience else None,
|
|
188
|
+
options=decode_options,
|
|
174
189
|
)
|
|
175
190
|
|
|
176
191
|
return payload
|
|
@@ -193,9 +208,21 @@ async def verify_context_request(
|
|
|
193
208
|
code="unauthorized",
|
|
194
209
|
status_code=401,
|
|
195
210
|
)
|
|
196
|
-
except jwt.
|
|
211
|
+
except jwt.DecodeError as e:
|
|
212
|
+
raise ContextError(
|
|
213
|
+
message=f"JWT decode error: {e}",
|
|
214
|
+
code="unauthorized",
|
|
215
|
+
status_code=401,
|
|
216
|
+
)
|
|
217
|
+
except jwt.InvalidSignatureError:
|
|
218
|
+
raise ContextError(
|
|
219
|
+
message="Invalid JWT signature",
|
|
220
|
+
code="unauthorized",
|
|
221
|
+
status_code=401,
|
|
222
|
+
)
|
|
223
|
+
except jwt.PyJWTError as e:
|
|
197
224
|
raise ContextError(
|
|
198
|
-
message="
|
|
225
|
+
message=f"JWT verification failed: {e}",
|
|
199
226
|
code="unauthorized",
|
|
200
227
|
status_code=401,
|
|
201
228
|
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Hummingbot Market Intelligence MCP Server (Python)
|
|
2
|
+
|
|
3
|
+
A **public market data** MCP server powered by the Hummingbot API. This is a Python implementation using FastAPI and the `ctxprotocol` SDK for payment verification.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
✅ **Public Market Data Only**
|
|
8
|
+
- Price data, order books, candles
|
|
9
|
+
- Liquidity analysis, trade impact estimation
|
|
10
|
+
- Funding rates for perpetuals
|
|
11
|
+
|
|
12
|
+
❌ **Excluded (User-Specific Data)**
|
|
13
|
+
- Portfolio balances
|
|
14
|
+
- Trading positions/orders
|
|
15
|
+
- Account management
|
|
16
|
+
- Bot orchestration
|
|
17
|
+
|
|
18
|
+
## Tools Overview
|
|
19
|
+
|
|
20
|
+
| Tool | Description |
|
|
21
|
+
|------|-------------|
|
|
22
|
+
| `get_prices` | Batch price lookup for multiple pairs |
|
|
23
|
+
| `get_order_book` | Order book snapshot with spread |
|
|
24
|
+
| `get_candles` | OHLCV candlestick data |
|
|
25
|
+
| `get_funding_rates` | Perpetual funding rate data |
|
|
26
|
+
| `analyze_trade_impact` | VWAP and price impact calculation |
|
|
27
|
+
| `get_connectors` | List all supported exchanges |
|
|
28
|
+
|
|
29
|
+
## Supported Exchanges
|
|
30
|
+
|
|
31
|
+
**CEX (Spot):** Binance, Bybit, OKX, KuCoin, Gate.io, Coinbase, Kraken, and more
|
|
32
|
+
|
|
33
|
+
**CEX (Perpetuals):** Binance Perpetual, Bybit Perpetual, Hyperliquid, OKX Perpetual, dYdX v4
|
|
34
|
+
|
|
35
|
+
**DEX:** Jupiter (Solana), Uniswap, PancakeSwap, Raydium, Meteora
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
### 1. Install Dependencies
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd examples/server/hummingbot-contributor
|
|
43
|
+
pip install -r requirements.txt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Environment Variables
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cp env.example .env
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Edit `.env`:
|
|
53
|
+
```bash
|
|
54
|
+
# Hummingbot API connection
|
|
55
|
+
HUMMINGBOT_API_URL=http://localhost:8000
|
|
56
|
+
HB_USERNAME=admin
|
|
57
|
+
HB_PASSWORD=admin
|
|
58
|
+
|
|
59
|
+
# Server port
|
|
60
|
+
PORT=4010
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Run the Server
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python server.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or with uvicorn directly:
|
|
70
|
+
```bash
|
|
71
|
+
uvicorn server:app --host 0.0.0.0 --port 4010 --reload
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Endpoints
|
|
75
|
+
|
|
76
|
+
| Endpoint | Description |
|
|
77
|
+
|----------|-------------|
|
|
78
|
+
| `GET /health` | Health check with tool list |
|
|
79
|
+
| `POST /mcp` | MCP JSON-RPC endpoint |
|
|
80
|
+
|
|
81
|
+
## Example Usage
|
|
82
|
+
|
|
83
|
+
### Get Prices
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -X POST http://localhost:4010/mcp \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{
|
|
89
|
+
"jsonrpc": "2.0",
|
|
90
|
+
"method": "tools/call",
|
|
91
|
+
"params": {
|
|
92
|
+
"name": "get_prices",
|
|
93
|
+
"arguments": {
|
|
94
|
+
"connector_name": "binance",
|
|
95
|
+
"trading_pairs": ["BTC-USDT", "ETH-USDT"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"id": 1
|
|
99
|
+
}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Analyze Trade Impact
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
curl -X POST http://localhost:4010/mcp \
|
|
106
|
+
-H "Content-Type: application/json" \
|
|
107
|
+
-d '{
|
|
108
|
+
"jsonrpc": "2.0",
|
|
109
|
+
"method": "tools/call",
|
|
110
|
+
"params": {
|
|
111
|
+
"name": "analyze_trade_impact",
|
|
112
|
+
"arguments": {
|
|
113
|
+
"connector_name": "binance",
|
|
114
|
+
"trading_pair": "BTC-USDT",
|
|
115
|
+
"side": "BUY",
|
|
116
|
+
"amount": 1.0
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"id": 1
|
|
120
|
+
}'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Get Funding Rates
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
curl -X POST http://localhost:4010/mcp \
|
|
127
|
+
-H "Content-Type: application/json" \
|
|
128
|
+
-d '{
|
|
129
|
+
"jsonrpc": "2.0",
|
|
130
|
+
"method": "tools/call",
|
|
131
|
+
"params": {
|
|
132
|
+
"name": "get_funding_rates",
|
|
133
|
+
"arguments": {
|
|
134
|
+
"connector_name": "binance_perpetual",
|
|
135
|
+
"trading_pair": "BTC-USDT"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"id": 1
|
|
139
|
+
}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Context Protocol Integration
|
|
143
|
+
|
|
144
|
+
This server uses `ctxprotocol` for payment verification:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from ctxprotocol import verify_context_request, is_protected_mcp_method
|
|
148
|
+
|
|
149
|
+
# In your endpoint handler:
|
|
150
|
+
if is_protected_mcp_method(method):
|
|
151
|
+
payload = await verify_context_request(
|
|
152
|
+
authorization_header=request.headers.get("authorization"),
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Security Model
|
|
157
|
+
|
|
158
|
+
| MCP Method | Auth Required | Reason |
|
|
159
|
+
|------------|---------------|--------|
|
|
160
|
+
| `tools/list` | ❌ No | Discovery - returns tool schemas |
|
|
161
|
+
| `tools/call` | ✅ Yes | Execution - runs code, costs money |
|
|
162
|
+
| `initialize` | ❌ No | Session setup |
|
|
163
|
+
|
|
164
|
+
## Deployment
|
|
165
|
+
|
|
166
|
+
### Deploy to Server
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
./deploy-hummingbot.sh
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### On the Server
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
cd ~/hummingbot-mcp-python
|
|
176
|
+
./setup-server.sh # Start with systemd
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Architecture
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
183
|
+
│ Server │
|
|
184
|
+
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
|
185
|
+
│ │ Hummingbot API │ │ Market Intel MCP (Python) │ │
|
|
186
|
+
│ │ (localhost:8000) │◄───│ (localhost:4010) │ │
|
|
187
|
+
│ │ │ │ │ │
|
|
188
|
+
│ │ • Market Data │ │ • FastAPI │ │
|
|
189
|
+
│ │ • Order Books │ │ • ctxprotocol auth │ │
|
|
190
|
+
│ │ • Gateway (DEX) │ │ • MCP Protocol │ │
|
|
191
|
+
│ └─────────────────────┘ └─────────────────────────────┘ │
|
|
192
|
+
└─────────────────────────────────────────────────────────────┘
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Comparison: TypeScript vs Python Implementation
|
|
196
|
+
|
|
197
|
+
| Aspect | TypeScript | Python |
|
|
198
|
+
|--------|------------|--------|
|
|
199
|
+
| Framework | Express + MCP SDK | FastAPI |
|
|
200
|
+
| Auth SDK | `@ctxprotocol/sdk` | `ctxprotocol` |
|
|
201
|
+
| Port | 4009 | 4010 |
|
|
202
|
+
| Same functionality | ✅ | ✅ |
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
207
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Hummingbot API MCP Server Configuration
|
|
2
|
+
|
|
3
|
+
# Port for the MCP server (default: 4010)
|
|
4
|
+
PORT=4010
|
|
5
|
+
|
|
6
|
+
# Hummingbot API Configuration
|
|
7
|
+
# NOTE: This server runs on the SAME machine as Hummingbot API
|
|
8
|
+
# so we use localhost by default
|
|
9
|
+
HUMMINGBOT_API_URL=http://localhost:8000
|
|
10
|
+
|
|
11
|
+
# Hummingbot API Basic Auth Credentials
|
|
12
|
+
# These should match your Hummingbot API configuration
|
|
13
|
+
HB_USERNAME=admin
|
|
14
|
+
HB_PASSWORD=admin
|
|
15
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Hummingbot Market Intelligence MCP Server - Python
|
|
2
|
+
#
|
|
3
|
+
# Core dependencies for the MCP server
|
|
4
|
+
|
|
5
|
+
# Web framework
|
|
6
|
+
fastapi>=0.111.0
|
|
7
|
+
uvicorn[standard]>=0.30.0
|
|
8
|
+
|
|
9
|
+
# HTTP client for Hummingbot API
|
|
10
|
+
httpx>=0.27.0
|
|
11
|
+
|
|
12
|
+
# Context Protocol SDK for payment verification
|
|
13
|
+
ctxprotocol>=0.5.5
|
|
14
|
+
|
|
15
|
+
# Environment management
|
|
16
|
+
python-dotenv>=1.0.0
|
|
17
|
+
|
|
18
|
+
# Type validation
|
|
19
|
+
pydantic>=2.0.0
|
|
20
|
+
|
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hummingbot Market Intelligence MCP Server (Python)
|
|
3
|
+
|
|
4
|
+
A PUBLIC MARKET DATA MCP server powered by Hummingbot API.
|
|
5
|
+
Provides access to real-time market data, liquidity analysis, and DEX quotes.
|
|
6
|
+
|
|
7
|
+
SCOPE: Public market data only - NO user account data, NO trading operations
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Multi-exchange price data (40+ CEX/DEX connectors)
|
|
11
|
+
- Order book analysis with VWAP and slippage estimation
|
|
12
|
+
- Funding rate analysis for perpetuals
|
|
13
|
+
- DEX swap quotes (Jupiter, 0x, etc.)
|
|
14
|
+
|
|
15
|
+
Architecture:
|
|
16
|
+
- Runs on the SAME server as Hummingbot API (localhost:8000)
|
|
17
|
+
- Uses Basic Auth with HB_USERNAME and HB_PASSWORD env vars
|
|
18
|
+
- Integrates ctxprotocol SDK for payment verification
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import base64
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
from dotenv import load_dotenv
|
|
28
|
+
from fastapi import FastAPI, Request, HTTPException, Depends
|
|
29
|
+
from fastapi.responses import JSONResponse
|
|
30
|
+
from pydantic import BaseModel, Field
|
|
31
|
+
|
|
32
|
+
from ctxprotocol import (
|
|
33
|
+
verify_context_request,
|
|
34
|
+
is_protected_mcp_method,
|
|
35
|
+
ContextError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Load environment variables
|
|
39
|
+
load_dotenv()
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# CONFIGURATION
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
HUMMINGBOT_API_URL = os.getenv("HUMMINGBOT_API_URL", "http://localhost:8000")
|
|
46
|
+
HB_USERNAME = os.getenv("HB_USERNAME", "admin")
|
|
47
|
+
HB_PASSWORD = os.getenv("HB_PASSWORD", "admin")
|
|
48
|
+
PORT = int(os.getenv("PORT", "4010"))
|
|
49
|
+
|
|
50
|
+
# Exchange lists for enums
|
|
51
|
+
TOP_SPOT_EXCHANGES = ["binance", "bybit", "okx", "kucoin", "gate_io"]
|
|
52
|
+
TOP_PERP_EXCHANGES = [
|
|
53
|
+
"binance_perpetual",
|
|
54
|
+
"bybit_perpetual",
|
|
55
|
+
"hyperliquid_perpetual",
|
|
56
|
+
"okx_perpetual",
|
|
57
|
+
"gate_io_perpetual",
|
|
58
|
+
]
|
|
59
|
+
ALL_EXCHANGES = TOP_SPOT_EXCHANGES + TOP_PERP_EXCHANGES
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# HUMMINGBOT API CLIENT
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_basic_auth_header() -> str:
|
|
67
|
+
"""Generate Basic Auth header for Hummingbot API."""
|
|
68
|
+
credentials = base64.b64encode(f"{HB_USERNAME}:{HB_PASSWORD}".encode()).decode()
|
|
69
|
+
return f"Basic {credentials}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def hb_fetch(
|
|
73
|
+
endpoint: str,
|
|
74
|
+
method: str = "GET",
|
|
75
|
+
body: dict[str, Any] | None = None,
|
|
76
|
+
params: dict[str, str] | None = None,
|
|
77
|
+
timeout: float = 30.0,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Make authenticated request to Hummingbot API."""
|
|
80
|
+
url = f"{HUMMINGBOT_API_URL}{endpoint}"
|
|
81
|
+
|
|
82
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
83
|
+
response = await client.request(
|
|
84
|
+
method=method,
|
|
85
|
+
url=url,
|
|
86
|
+
headers={
|
|
87
|
+
"Authorization": get_basic_auth_header(),
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
json=body,
|
|
91
|
+
params=params,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not response.is_success:
|
|
95
|
+
error_text = response.text[:500]
|
|
96
|
+
raise HTTPException(
|
|
97
|
+
status_code=response.status_code,
|
|
98
|
+
detail=f"Hummingbot API error: {error_text}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return response.json()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# MCP TOOL DEFINITIONS
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
TOOLS = [
|
|
109
|
+
# =========================================================================
|
|
110
|
+
# RAW DATA TOOLS - Direct market data access
|
|
111
|
+
# =========================================================================
|
|
112
|
+
{
|
|
113
|
+
"name": "get_prices",
|
|
114
|
+
"description": """📊 Get real-time prices for trading pairs across exchanges.
|
|
115
|
+
|
|
116
|
+
Fetches current mid prices for one or more trading pairs from any supported exchange.
|
|
117
|
+
|
|
118
|
+
Example: Get BTC and ETH prices from Binance
|
|
119
|
+
- connector_name: "binance"
|
|
120
|
+
- trading_pairs: ["BTC-USDT", "ETH-USDT"]
|
|
121
|
+
|
|
122
|
+
Supported exchanges: binance, bybit, okx, kucoin, gate_io, hyperliquid_perpetual, and 40+ more.""",
|
|
123
|
+
"inputSchema": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"connector_name": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"enum": ALL_EXCHANGES,
|
|
129
|
+
"description": "Exchange connector name",
|
|
130
|
+
},
|
|
131
|
+
"trading_pairs": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": {"type": "string"},
|
|
134
|
+
"description": "Trading pairs (e.g., ['BTC-USDT', 'ETH-USDT'])",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"required": ["connector_name", "trading_pairs"],
|
|
138
|
+
},
|
|
139
|
+
"outputSchema": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"connector": {"type": "string"},
|
|
143
|
+
"prices": {
|
|
144
|
+
"type": "array",
|
|
145
|
+
"items": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"trading_pair": {"type": "string"},
|
|
149
|
+
"price": {"type": "number"},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
"timestamp": {"type": "string"},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "get_order_book",
|
|
159
|
+
"description": """📊 Get order book snapshot for a trading pair.
|
|
160
|
+
|
|
161
|
+
Returns top bids and asks from the order book with price and quantity.
|
|
162
|
+
|
|
163
|
+
Example: Get BTC-USDT order book from Binance
|
|
164
|
+
- connector_name: "binance"
|
|
165
|
+
- trading_pair: "BTC-USDT"
|
|
166
|
+
- depth: 10
|
|
167
|
+
|
|
168
|
+
Supported exchanges: All CEX connectors.""",
|
|
169
|
+
"inputSchema": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"connector_name": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"enum": ALL_EXCHANGES,
|
|
175
|
+
"description": "Exchange connector name",
|
|
176
|
+
},
|
|
177
|
+
"trading_pair": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"description": "Trading pair (e.g., 'BTC-USDT')",
|
|
180
|
+
},
|
|
181
|
+
"depth": {
|
|
182
|
+
"type": "integer",
|
|
183
|
+
"description": "Number of levels to fetch (default: 10)",
|
|
184
|
+
"default": 10,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
"required": ["connector_name", "trading_pair"],
|
|
188
|
+
},
|
|
189
|
+
"outputSchema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"connector": {"type": "string"},
|
|
193
|
+
"trading_pair": {"type": "string"},
|
|
194
|
+
"bids": {"type": "array", "items": {"type": "object"}},
|
|
195
|
+
"asks": {"type": "array", "items": {"type": "object"}},
|
|
196
|
+
"spread": {"type": "object"},
|
|
197
|
+
"timestamp": {"type": "string"},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "get_candles",
|
|
203
|
+
"description": """📊 Get OHLCV candlestick data for technical analysis.
|
|
204
|
+
|
|
205
|
+
Returns historical candlestick data with open, high, low, close, volume.
|
|
206
|
+
|
|
207
|
+
Example: Get 1-hour BTC candles from Binance
|
|
208
|
+
- connector_name: "binance"
|
|
209
|
+
- trading_pair: "BTC-USDT"
|
|
210
|
+
- interval: "1h"
|
|
211
|
+
- limit: 100
|
|
212
|
+
|
|
213
|
+
Intervals: 1m, 5m, 15m, 1h, 4h, 1d""",
|
|
214
|
+
"inputSchema": {
|
|
215
|
+
"type": "object",
|
|
216
|
+
"properties": {
|
|
217
|
+
"connector_name": {
|
|
218
|
+
"type": "string",
|
|
219
|
+
"enum": ALL_EXCHANGES,
|
|
220
|
+
"description": "Exchange connector name",
|
|
221
|
+
},
|
|
222
|
+
"trading_pair": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"description": "Trading pair (e.g., 'BTC-USDT')",
|
|
225
|
+
},
|
|
226
|
+
"interval": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"enum": ["1m", "5m", "15m", "1h", "4h", "1d"],
|
|
229
|
+
"description": "Candle interval",
|
|
230
|
+
},
|
|
231
|
+
"limit": {
|
|
232
|
+
"type": "integer",
|
|
233
|
+
"description": "Number of candles (default: 100, max: 500)",
|
|
234
|
+
"default": 100,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
"required": ["connector_name", "trading_pair", "interval"],
|
|
238
|
+
},
|
|
239
|
+
"outputSchema": {
|
|
240
|
+
"type": "object",
|
|
241
|
+
"properties": {
|
|
242
|
+
"connector": {"type": "string"},
|
|
243
|
+
"trading_pair": {"type": "string"},
|
|
244
|
+
"interval": {"type": "string"},
|
|
245
|
+
"candles": {
|
|
246
|
+
"type": "array",
|
|
247
|
+
"items": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"properties": {
|
|
250
|
+
"timestamp": {"type": "string"},
|
|
251
|
+
"open": {"type": "number"},
|
|
252
|
+
"high": {"type": "number"},
|
|
253
|
+
"low": {"type": "number"},
|
|
254
|
+
"close": {"type": "number"},
|
|
255
|
+
"volume": {"type": "number"},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
"count": {"type": "integer"},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "get_funding_rates",
|
|
265
|
+
"description": """📊 Get funding rate for perpetual futures.
|
|
266
|
+
|
|
267
|
+
Returns current funding rate, next funding time, and mark/index prices.
|
|
268
|
+
|
|
269
|
+
Example: Get BTC funding rate from Binance Perpetual
|
|
270
|
+
- connector_name: "binance_perpetual"
|
|
271
|
+
- trading_pair: "BTC-USDT"
|
|
272
|
+
|
|
273
|
+
Supported: binance_perpetual, bybit_perpetual, hyperliquid_perpetual, okx_perpetual""",
|
|
274
|
+
"inputSchema": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"properties": {
|
|
277
|
+
"connector_name": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"enum": TOP_PERP_EXCHANGES,
|
|
280
|
+
"description": "Perpetual exchange connector",
|
|
281
|
+
},
|
|
282
|
+
"trading_pair": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"description": "Trading pair (e.g., 'BTC-USDT')",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
"required": ["connector_name", "trading_pair"],
|
|
288
|
+
},
|
|
289
|
+
"outputSchema": {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"properties": {
|
|
292
|
+
"connector": {"type": "string"},
|
|
293
|
+
"trading_pair": {"type": "string"},
|
|
294
|
+
"funding_rate": {"type": "number"},
|
|
295
|
+
"funding_rate_pct": {"type": "string"},
|
|
296
|
+
"annualized_rate_pct": {"type": "string"},
|
|
297
|
+
"mark_price": {"type": "number"},
|
|
298
|
+
"index_price": {"type": "number"},
|
|
299
|
+
"next_funding_time": {"type": "string"},
|
|
300
|
+
"timestamp": {"type": "string"},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
# =========================================================================
|
|
305
|
+
# INTELLIGENCE TOOLS - Analysis and computation
|
|
306
|
+
# =========================================================================
|
|
307
|
+
{
|
|
308
|
+
"name": "analyze_trade_impact",
|
|
309
|
+
"description": """🧠 Calculate exact price impact and VWAP for a trade.
|
|
310
|
+
|
|
311
|
+
Uses real order book data to compute:
|
|
312
|
+
- Exact execution price for your trade size
|
|
313
|
+
- VWAP (Volume Weighted Average Price)
|
|
314
|
+
- Price impact / slippage percentage
|
|
315
|
+
- Whether sufficient liquidity exists
|
|
316
|
+
|
|
317
|
+
Perfect for: Pre-trade analysis, optimal execution planning, large order sizing.""",
|
|
318
|
+
"inputSchema": {
|
|
319
|
+
"type": "object",
|
|
320
|
+
"properties": {
|
|
321
|
+
"connector_name": {
|
|
322
|
+
"type": "string",
|
|
323
|
+
"enum": ALL_EXCHANGES,
|
|
324
|
+
"description": "Exchange connector",
|
|
325
|
+
},
|
|
326
|
+
"trading_pair": {
|
|
327
|
+
"type": "string",
|
|
328
|
+
"description": "Trading pair (e.g., 'BTC-USDT')",
|
|
329
|
+
},
|
|
330
|
+
"side": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"enum": ["BUY", "SELL"],
|
|
333
|
+
"description": "Trade side - BUY walks the asks, SELL walks the bids",
|
|
334
|
+
},
|
|
335
|
+
"amount": {
|
|
336
|
+
"type": "number",
|
|
337
|
+
"description": "Trade amount in BASE token (e.g., 1.5 for 1.5 BTC)",
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
"required": ["connector_name", "trading_pair", "side", "amount"],
|
|
341
|
+
},
|
|
342
|
+
"outputSchema": {
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"trading_pair": {"type": "string"},
|
|
346
|
+
"side": {"type": "string"},
|
|
347
|
+
"requested_amount": {"type": "number"},
|
|
348
|
+
"vwap": {"type": "number"},
|
|
349
|
+
"price_impact_pct": {"type": "number"},
|
|
350
|
+
"total_quote_volume": {"type": "number"},
|
|
351
|
+
"mid_price": {"type": "number"},
|
|
352
|
+
"spread": {"type": "object"},
|
|
353
|
+
"sufficient_liquidity": {"type": "boolean"},
|
|
354
|
+
"timestamp": {"type": "string"},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"name": "get_connectors",
|
|
360
|
+
"description": """📋 List all supported exchange connectors.
|
|
361
|
+
|
|
362
|
+
Returns the full list of 40+ CEX and DEX connectors available in Hummingbot.
|
|
363
|
+
|
|
364
|
+
No arguments required.""",
|
|
365
|
+
"inputSchema": {
|
|
366
|
+
"type": "object",
|
|
367
|
+
"properties": {},
|
|
368
|
+
},
|
|
369
|
+
"outputSchema": {
|
|
370
|
+
"type": "object",
|
|
371
|
+
"properties": {
|
|
372
|
+
"spot_exchanges": {"type": "array", "items": {"type": "string"}},
|
|
373
|
+
"perpetual_exchanges": {"type": "array", "items": {"type": "string"}},
|
|
374
|
+
"dex_connectors": {"type": "array", "items": {"type": "string"}},
|
|
375
|
+
"total_count": {"type": "integer"},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ============================================================================
|
|
383
|
+
# TOOL HANDLERS
|
|
384
|
+
# ============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def handle_get_prices(args: dict[str, Any]) -> dict[str, Any]:
|
|
388
|
+
"""Handle get_prices tool call."""
|
|
389
|
+
connector_name = args["connector_name"]
|
|
390
|
+
trading_pairs = args["trading_pairs"]
|
|
391
|
+
|
|
392
|
+
# Call Hummingbot API
|
|
393
|
+
result = await hb_fetch(
|
|
394
|
+
"/api/v1/get-ticker",
|
|
395
|
+
method="POST",
|
|
396
|
+
body={
|
|
397
|
+
"connector": connector_name,
|
|
398
|
+
"trading_pairs": trading_pairs,
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
prices = []
|
|
403
|
+
for pair, data in result.items():
|
|
404
|
+
if isinstance(data, dict) and "mid_price" in data:
|
|
405
|
+
prices.append({
|
|
406
|
+
"trading_pair": pair,
|
|
407
|
+
"price": float(data["mid_price"]),
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
"connector": connector_name,
|
|
412
|
+
"prices": prices,
|
|
413
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def handle_get_order_book(args: dict[str, Any]) -> dict[str, Any]:
|
|
418
|
+
"""Handle get_order_book tool call."""
|
|
419
|
+
connector_name = args["connector_name"]
|
|
420
|
+
trading_pair = args["trading_pair"]
|
|
421
|
+
depth = args.get("depth", 10)
|
|
422
|
+
|
|
423
|
+
result = await hb_fetch(
|
|
424
|
+
"/api/v1/get-order-book-snapshot",
|
|
425
|
+
method="POST",
|
|
426
|
+
body={
|
|
427
|
+
"connector": connector_name,
|
|
428
|
+
"trading_pair": trading_pair,
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
bids = result.get("bids", [])[:depth]
|
|
433
|
+
asks = result.get("asks", [])[:depth]
|
|
434
|
+
|
|
435
|
+
# Calculate spread
|
|
436
|
+
best_bid = float(bids[0][0]) if bids else 0
|
|
437
|
+
best_ask = float(asks[0][0]) if asks else 0
|
|
438
|
+
spread_abs = best_ask - best_bid if best_bid and best_ask else 0
|
|
439
|
+
spread_pct = (spread_abs / best_bid * 100) if best_bid else 0
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
"connector": connector_name,
|
|
443
|
+
"trading_pair": trading_pair,
|
|
444
|
+
"bids": [{"price": float(b[0]), "quantity": float(b[1])} for b in bids],
|
|
445
|
+
"asks": [{"price": float(a[0]), "quantity": float(a[1])} for a in asks],
|
|
446
|
+
"spread": {
|
|
447
|
+
"absolute": spread_abs,
|
|
448
|
+
"percentage": round(spread_pct, 4),
|
|
449
|
+
},
|
|
450
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
async def handle_get_candles(args: dict[str, Any]) -> dict[str, Any]:
|
|
455
|
+
"""Handle get_candles tool call."""
|
|
456
|
+
connector_name = args["connector_name"]
|
|
457
|
+
trading_pair = args["trading_pair"]
|
|
458
|
+
interval = args["interval"]
|
|
459
|
+
limit = min(args.get("limit", 100), 500)
|
|
460
|
+
|
|
461
|
+
result = await hb_fetch(
|
|
462
|
+
"/api/v1/get-candles",
|
|
463
|
+
method="POST",
|
|
464
|
+
body={
|
|
465
|
+
"connector": connector_name,
|
|
466
|
+
"trading_pair": trading_pair,
|
|
467
|
+
"interval": interval,
|
|
468
|
+
"limit": limit,
|
|
469
|
+
},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
candles = []
|
|
473
|
+
for candle in result.get("candles", []):
|
|
474
|
+
candles.append({
|
|
475
|
+
"timestamp": candle.get("timestamp"),
|
|
476
|
+
"open": float(candle.get("open", 0)),
|
|
477
|
+
"high": float(candle.get("high", 0)),
|
|
478
|
+
"low": float(candle.get("low", 0)),
|
|
479
|
+
"close": float(candle.get("close", 0)),
|
|
480
|
+
"volume": float(candle.get("volume", 0)),
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
"connector": connector_name,
|
|
485
|
+
"trading_pair": trading_pair,
|
|
486
|
+
"interval": interval,
|
|
487
|
+
"candles": candles,
|
|
488
|
+
"count": len(candles),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
async def handle_get_funding_rates(args: dict[str, Any]) -> dict[str, Any]:
|
|
493
|
+
"""Handle get_funding_rates tool call."""
|
|
494
|
+
connector_name = args["connector_name"]
|
|
495
|
+
trading_pair = args["trading_pair"]
|
|
496
|
+
|
|
497
|
+
result = await hb_fetch(
|
|
498
|
+
"/api/v1/get-funding-info",
|
|
499
|
+
method="POST",
|
|
500
|
+
body={
|
|
501
|
+
"connector": connector_name,
|
|
502
|
+
"trading_pair": trading_pair,
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
funding_rate = float(result.get("funding_rate", 0))
|
|
507
|
+
annualized = funding_rate * 3 * 365 * 100 # 8h funding, 3x per day, annualized
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
"connector": connector_name,
|
|
511
|
+
"trading_pair": trading_pair,
|
|
512
|
+
"funding_rate": funding_rate,
|
|
513
|
+
"funding_rate_pct": f"{funding_rate * 100:.4f}%",
|
|
514
|
+
"annualized_rate_pct": f"{annualized:.2f}%",
|
|
515
|
+
"mark_price": float(result.get("mark_price", 0)),
|
|
516
|
+
"index_price": float(result.get("index_price", 0)),
|
|
517
|
+
"next_funding_time": result.get("next_funding_time", ""),
|
|
518
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
async def handle_analyze_trade_impact(args: dict[str, Any]) -> dict[str, Any]:
|
|
523
|
+
"""Handle analyze_trade_impact tool call."""
|
|
524
|
+
connector_name = args["connector_name"]
|
|
525
|
+
trading_pair = args["trading_pair"]
|
|
526
|
+
side = args["side"]
|
|
527
|
+
amount = float(args["amount"])
|
|
528
|
+
|
|
529
|
+
# Get order book
|
|
530
|
+
ob_result = await hb_fetch(
|
|
531
|
+
"/api/v1/get-order-book-snapshot",
|
|
532
|
+
method="POST",
|
|
533
|
+
body={
|
|
534
|
+
"connector": connector_name,
|
|
535
|
+
"trading_pair": trading_pair,
|
|
536
|
+
},
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
bids = ob_result.get("bids", [])
|
|
540
|
+
asks = ob_result.get("asks", [])
|
|
541
|
+
|
|
542
|
+
# Calculate mid price
|
|
543
|
+
best_bid = float(bids[0][0]) if bids else 0
|
|
544
|
+
best_ask = float(asks[0][0]) if asks else 0
|
|
545
|
+
mid_price = (best_bid + best_ask) / 2 if best_bid and best_ask else 0
|
|
546
|
+
|
|
547
|
+
# Walk the book to calculate VWAP
|
|
548
|
+
book = asks if side == "BUY" else bids
|
|
549
|
+
remaining = amount
|
|
550
|
+
total_quote = 0.0
|
|
551
|
+
total_base = 0.0
|
|
552
|
+
|
|
553
|
+
for level in book:
|
|
554
|
+
price = float(level[0])
|
|
555
|
+
qty = float(level[1])
|
|
556
|
+
fill_qty = min(remaining, qty)
|
|
557
|
+
total_quote += fill_qty * price
|
|
558
|
+
total_base += fill_qty
|
|
559
|
+
remaining -= fill_qty
|
|
560
|
+
if remaining <= 0:
|
|
561
|
+
break
|
|
562
|
+
|
|
563
|
+
sufficient_liquidity = remaining <= 0
|
|
564
|
+
vwap = total_quote / total_base if total_base > 0 else 0
|
|
565
|
+
price_impact = ((vwap - mid_price) / mid_price * 100) if mid_price else 0
|
|
566
|
+
if side == "SELL":
|
|
567
|
+
price_impact = -price_impact
|
|
568
|
+
|
|
569
|
+
spread_abs = best_ask - best_bid if best_bid and best_ask else 0
|
|
570
|
+
spread_pct = (spread_abs / best_bid * 100) if best_bid else 0
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
"trading_pair": trading_pair,
|
|
574
|
+
"side": side,
|
|
575
|
+
"requested_amount": amount,
|
|
576
|
+
"vwap": round(vwap, 8),
|
|
577
|
+
"price_impact_pct": round(abs(price_impact), 4),
|
|
578
|
+
"total_quote_volume": round(total_quote, 2),
|
|
579
|
+
"mid_price": round(mid_price, 8),
|
|
580
|
+
"spread": {
|
|
581
|
+
"absolute": round(spread_abs, 8),
|
|
582
|
+
"percentage": round(spread_pct, 4),
|
|
583
|
+
},
|
|
584
|
+
"sufficient_liquidity": sufficient_liquidity,
|
|
585
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
async def handle_get_connectors(args: dict[str, Any]) -> dict[str, Any]:
|
|
590
|
+
"""Handle get_connectors tool call."""
|
|
591
|
+
return {
|
|
592
|
+
"spot_exchanges": [
|
|
593
|
+
"binance", "bybit", "okx", "kucoin", "gate_io", "coinbase_advanced_trade",
|
|
594
|
+
"kraken", "bitfinex", "mexc", "bitget", "htx", "crypto_com",
|
|
595
|
+
],
|
|
596
|
+
"perpetual_exchanges": [
|
|
597
|
+
"binance_perpetual", "bybit_perpetual", "okx_perpetual", "gate_io_perpetual",
|
|
598
|
+
"kucoin_perpetual", "hyperliquid_perpetual", "dydx_v4_perpetual",
|
|
599
|
+
],
|
|
600
|
+
"dex_connectors": [
|
|
601
|
+
"jupiter", "uniswap", "pancakeswap", "raydium", "meteora", "vertex",
|
|
602
|
+
],
|
|
603
|
+
"total_count": 25,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# Tool handler dispatch
|
|
608
|
+
TOOL_HANDLERS = {
|
|
609
|
+
"get_prices": handle_get_prices,
|
|
610
|
+
"get_order_book": handle_get_order_book,
|
|
611
|
+
"get_candles": handle_get_candles,
|
|
612
|
+
"get_funding_rates": handle_get_funding_rates,
|
|
613
|
+
"analyze_trade_impact": handle_analyze_trade_impact,
|
|
614
|
+
"get_connectors": handle_get_connectors,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# ============================================================================
|
|
619
|
+
# FASTAPI APP
|
|
620
|
+
# ============================================================================
|
|
621
|
+
|
|
622
|
+
app = FastAPI(
|
|
623
|
+
title="Hummingbot Market Intelligence MCP Server",
|
|
624
|
+
description="Public market data MCP server powered by Hummingbot API",
|
|
625
|
+
version="1.0.0",
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ============================================================================
|
|
630
|
+
# CONTEXT PROTOCOL AUTHENTICATION
|
|
631
|
+
# ============================================================================
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
async def verify_context_auth(request: Request) -> dict[str, Any] | None:
|
|
635
|
+
"""Verify Context Protocol JWT for protected methods."""
|
|
636
|
+
try:
|
|
637
|
+
body = await request.json()
|
|
638
|
+
except Exception:
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
method = body.get("method", "")
|
|
642
|
+
|
|
643
|
+
# Allow discovery methods without authentication
|
|
644
|
+
if not method or not is_protected_mcp_method(method):
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
# Protected method - require authentication
|
|
648
|
+
authorization = request.headers.get("authorization")
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
payload = await verify_context_request(
|
|
652
|
+
authorization_header=authorization,
|
|
653
|
+
)
|
|
654
|
+
return payload
|
|
655
|
+
except ContextError as e:
|
|
656
|
+
raise HTTPException(status_code=401, detail=f"Unauthorized: {e.message}")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# ============================================================================
|
|
660
|
+
# MCP ENDPOINTS
|
|
661
|
+
# ============================================================================
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@app.get("/health")
|
|
665
|
+
async def health_check():
|
|
666
|
+
"""Health check endpoint."""
|
|
667
|
+
return {
|
|
668
|
+
"status": "healthy",
|
|
669
|
+
"service": "hummingbot-mcp-python",
|
|
670
|
+
"tools": [t["name"] for t in TOOLS],
|
|
671
|
+
"hummingbot_api": HUMMINGBOT_API_URL,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@app.post("/mcp")
|
|
676
|
+
async def mcp_endpoint(request: Request):
|
|
677
|
+
"""MCP JSON-RPC endpoint."""
|
|
678
|
+
try:
|
|
679
|
+
body = await request.json()
|
|
680
|
+
except Exception:
|
|
681
|
+
return JSONResponse(
|
|
682
|
+
status_code=400,
|
|
683
|
+
content={"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None},
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
method = body.get("method", "")
|
|
687
|
+
params = body.get("params", {})
|
|
688
|
+
request_id = body.get("id")
|
|
689
|
+
|
|
690
|
+
# Verify authentication for protected methods
|
|
691
|
+
if is_protected_mcp_method(method):
|
|
692
|
+
authorization = request.headers.get("authorization")
|
|
693
|
+
try:
|
|
694
|
+
await verify_context_request(authorization_header=authorization)
|
|
695
|
+
except ContextError as e:
|
|
696
|
+
return JSONResponse(
|
|
697
|
+
status_code=401,
|
|
698
|
+
content={
|
|
699
|
+
"jsonrpc": "2.0",
|
|
700
|
+
"error": {"code": -32001, "message": f"Unauthorized: {e.message}"},
|
|
701
|
+
"id": request_id,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Handle MCP methods
|
|
706
|
+
if method == "initialize":
|
|
707
|
+
return {
|
|
708
|
+
"jsonrpc": "2.0",
|
|
709
|
+
"result": {
|
|
710
|
+
"protocolVersion": "2024-11-05",
|
|
711
|
+
"serverInfo": {
|
|
712
|
+
"name": "hummingbot-market-intel",
|
|
713
|
+
"version": "1.0.0",
|
|
714
|
+
},
|
|
715
|
+
"capabilities": {
|
|
716
|
+
"tools": {"listChanged": False},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
"id": request_id,
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
elif method == "tools/list":
|
|
723
|
+
return {
|
|
724
|
+
"jsonrpc": "2.0",
|
|
725
|
+
"result": {"tools": TOOLS},
|
|
726
|
+
"id": request_id,
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
elif method == "tools/call":
|
|
730
|
+
tool_name = params.get("name", "")
|
|
731
|
+
arguments = params.get("arguments", {})
|
|
732
|
+
|
|
733
|
+
handler = TOOL_HANDLERS.get(tool_name)
|
|
734
|
+
if not handler:
|
|
735
|
+
return {
|
|
736
|
+
"jsonrpc": "2.0",
|
|
737
|
+
"error": {"code": -32601, "message": f"Unknown tool: {tool_name}"},
|
|
738
|
+
"id": request_id,
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
result = await handler(arguments)
|
|
743
|
+
return {
|
|
744
|
+
"jsonrpc": "2.0",
|
|
745
|
+
"result": {
|
|
746
|
+
"content": [{"type": "text", "text": str(result)}],
|
|
747
|
+
"structuredContent": result,
|
|
748
|
+
},
|
|
749
|
+
"id": request_id,
|
|
750
|
+
}
|
|
751
|
+
except HTTPException as e:
|
|
752
|
+
return {
|
|
753
|
+
"jsonrpc": "2.0",
|
|
754
|
+
"error": {"code": -32000, "message": e.detail},
|
|
755
|
+
"id": request_id,
|
|
756
|
+
}
|
|
757
|
+
except Exception as e:
|
|
758
|
+
return {
|
|
759
|
+
"jsonrpc": "2.0",
|
|
760
|
+
"error": {"code": -32000, "message": str(e)},
|
|
761
|
+
"id": request_id,
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
else:
|
|
765
|
+
return {
|
|
766
|
+
"jsonrpc": "2.0",
|
|
767
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
768
|
+
"id": request_id,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# ============================================================================
|
|
773
|
+
# MAIN
|
|
774
|
+
# ============================================================================
|
|
775
|
+
|
|
776
|
+
if __name__ == "__main__":
|
|
777
|
+
import uvicorn
|
|
778
|
+
|
|
779
|
+
print(f"🚀 Starting Hummingbot MCP Server on port {PORT}")
|
|
780
|
+
print(f"📡 Hummingbot API: {HUMMINGBOT_API_URL}")
|
|
781
|
+
print(f"🔧 Tools: {[t['name'] for t in TOOLS]}")
|
|
782
|
+
|
|
783
|
+
uvicorn.run(app, host="0.0.0.0", port=PORT)
|
|
784
|
+
|
|
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
|
|
File without changes
|
|
File without changes
|