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.
Files changed (21) hide show
  1. ctxprotocol-0.5.6/.gitignore +50 -0
  2. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/PKG-INFO +15 -6
  3. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/README.md +14 -5
  4. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/auth/__init__.py +34 -7
  5. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/README.md +207 -0
  6. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/env.example +15 -0
  7. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/requirements.txt +20 -0
  8. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/server.py +784 -0
  9. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/pyproject.toml +1 -1
  10. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/__init__.py +0 -0
  11. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/__init__.py +0 -0
  12. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/client.py +0 -0
  13. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/__init__.py +0 -0
  14. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/discovery.py +0 -0
  15. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/resources/tools.py +0 -0
  16. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/client/types.py +0 -0
  17. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/__init__.py +0 -0
  18. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/hyperliquid.py +0 -0
  19. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/polymarket.py +0 -0
  20. {ctxprotocol-0.5.5 → ctxprotocol-0.5.6}/ctxprotocol/context/wallet.py +0 -0
  21. {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.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
- | MCP Method | Auth Required | Reason |
269
- |------------|---------------|--------|
270
- | `tools/list` | No | Discovery - just returns tool schemas |
271
- | `tools/call` | ✅ Yes | Execution - runs code, may cost money |
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
- | MCP Method | Auth Required | Reason |
231
- |------------|---------------|--------|
232
- | `tools/list` | No | Discovery - just returns tool schemas |
233
- | `tools/call` | ✅ Yes | Execution - runs code, may cost money |
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.PyJWTError:
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="Invalid Context Protocol signature",
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
+
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxprotocol"
7
- version = "0.5.5"
7
+ version = "0.5.6"
8
8
  description = "Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically"
9
9
  readme = "README.md"
10
10
  license = "MIT"