defistream-mcp 0.2.0__py3-none-any.whl
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.
- defistream_mcp/__init__.py +3 -0
- defistream_mcp/api_client.py +102 -0
- defistream_mcp/config.py +99 -0
- defistream_mcp/formatters.py +247 -0
- defistream_mcp/resources.py +65 -0
- defistream_mcp/server.py +61 -0
- defistream_mcp/tools.py +727 -0
- defistream_mcp-0.2.0.dist-info/METADATA +224 -0
- defistream_mcp-0.2.0.dist-info/RECORD +11 -0
- defistream_mcp-0.2.0.dist-info/WHEEL +4 -0
- defistream_mcp-0.2.0.dist-info/entry_points.txt +2 -0
defistream_mcp/tools.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""MCP tool definitions for the DeFiStream API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import parse_qs, urlencode
|
|
9
|
+
|
|
10
|
+
from .api_client import create_client_with_key, get_client
|
|
11
|
+
from .formatters import (
|
|
12
|
+
format_aggregate_response,
|
|
13
|
+
format_events_response,
|
|
14
|
+
format_link_response,
|
|
15
|
+
format_local_execution_guide,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Internal helpers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_range_params(
|
|
27
|
+
block_start: int | None,
|
|
28
|
+
block_end: int | None,
|
|
29
|
+
since: str | None,
|
|
30
|
+
until: str | None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Build the block/time range query params, validating mutual exclusivity."""
|
|
33
|
+
params: dict[str, Any] = {}
|
|
34
|
+
has_blocks = block_start is not None or block_end is not None
|
|
35
|
+
has_time = since is not None or until is not None
|
|
36
|
+
|
|
37
|
+
if has_blocks and has_time:
|
|
38
|
+
raise ValueError("Specify either block_start/block_end or since/until, not both.")
|
|
39
|
+
|
|
40
|
+
if has_blocks:
|
|
41
|
+
if block_start is not None:
|
|
42
|
+
params["block_start"] = block_start
|
|
43
|
+
if block_end is not None:
|
|
44
|
+
params["block_end"] = block_end
|
|
45
|
+
elif has_time:
|
|
46
|
+
if since is not None:
|
|
47
|
+
params["since"] = since
|
|
48
|
+
if until is not None:
|
|
49
|
+
params["until"] = until
|
|
50
|
+
|
|
51
|
+
return params
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _build_query(
|
|
55
|
+
protocol: str,
|
|
56
|
+
event_type: str,
|
|
57
|
+
network: str,
|
|
58
|
+
extra: dict[str, Any],
|
|
59
|
+
*,
|
|
60
|
+
aggregate: bool = False,
|
|
61
|
+
group_by: str = "time",
|
|
62
|
+
period: str = "1h",
|
|
63
|
+
verbose: bool = False,
|
|
64
|
+
with_value: bool = False,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Build a query path (without base URL) from protocol, event type, and params."""
|
|
67
|
+
path = f"/{protocol}/events/{event_type}"
|
|
68
|
+
if aggregate:
|
|
69
|
+
path += "/aggregate"
|
|
70
|
+
|
|
71
|
+
query_params: dict[str, Any] = {"network": network}
|
|
72
|
+
|
|
73
|
+
if aggregate:
|
|
74
|
+
query_params["group_by"] = group_by
|
|
75
|
+
query_params["period"] = period
|
|
76
|
+
|
|
77
|
+
if verbose:
|
|
78
|
+
query_params["verbose"] = "true"
|
|
79
|
+
|
|
80
|
+
if with_value:
|
|
81
|
+
query_params["with_value"] = "true"
|
|
82
|
+
|
|
83
|
+
query_params.update({k: v for k, v in extra.items() if v is not None})
|
|
84
|
+
|
|
85
|
+
qs = urlencode(query_params)
|
|
86
|
+
return f"{path}?{qs}" if qs else path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_query(query: str) -> tuple[str, dict[str, Any]]:
|
|
90
|
+
"""Parse a query path like /erc20/events/transfer?network=eth&token=USDT."""
|
|
91
|
+
if "?" in query:
|
|
92
|
+
path, qs = query.split("?", 1)
|
|
93
|
+
parsed = parse_qs(qs, keep_blank_values=True)
|
|
94
|
+
params: dict[str, Any] = {
|
|
95
|
+
k: v[0] if len(v) == 1 else v for k, v in parsed.items()
|
|
96
|
+
}
|
|
97
|
+
else:
|
|
98
|
+
path = query
|
|
99
|
+
params = {}
|
|
100
|
+
return path, params
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Tool registration
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def register_tools(mcp, config): # noqa: ANN001
|
|
109
|
+
"""Register all DeFiStream tools on the FastMCP instance."""
|
|
110
|
+
|
|
111
|
+
# -----------------------------------------------------------------------
|
|
112
|
+
# Utility tools
|
|
113
|
+
# -----------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
async def supported_networks(protocol: str) -> str:
|
|
117
|
+
"""Get the list of supported blockchain networks for a protocol.
|
|
118
|
+
|
|
119
|
+
Use this to validate that a user's requested network is supported
|
|
120
|
+
before building a query. Reject requests for unsupported networks.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
protocol: Protocol name (erc20, native_token, aave_v3, uniswap_v3, lido, stader, threshold).
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
client = get_client()
|
|
127
|
+
data, _ = await client.get_json(f"/{protocol}/networks")
|
|
128
|
+
return f"Supported networks for {protocol}:\n" + ", ".join(data)
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
return f"Error fetching networks for '{protocol}': {exc}"
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
async def supported_events(protocol: str) -> str:
|
|
134
|
+
"""Get the list of supported event types for a protocol.
|
|
135
|
+
|
|
136
|
+
Use this to validate that a user's requested event type is supported
|
|
137
|
+
before building a query.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
protocol: Protocol name (erc20, native_token, aave_v3, uniswap_v3, lido, stader, threshold).
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
client = get_client()
|
|
144
|
+
data, _ = await client.get_json(f"/{protocol}/events/types")
|
|
145
|
+
return f"Supported event types for {protocol}:\n" + ", ".join(data)
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
return f"Error fetching event types for '{protocol}': {exc}"
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
async def base_url() -> str:
|
|
151
|
+
"""Get the DeFiStream API base URL.
|
|
152
|
+
|
|
153
|
+
Combine with a query path from any query builder to form the full
|
|
154
|
+
API URL. Example: base_url + query_path → full URL.
|
|
155
|
+
"""
|
|
156
|
+
return config.base_url
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
async def execute_query(
|
|
160
|
+
query: str,
|
|
161
|
+
api_key: str,
|
|
162
|
+
file_format: str = "csv",
|
|
163
|
+
limit: int = 200,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Execute a query and return results (for small to medium block ranges).
|
|
166
|
+
|
|
167
|
+
Requires your DeFiStream API key. Best for queries returning manageable
|
|
168
|
+
result sets. For very large ranges, use download_query_as_link instead.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
query: Query path from a query builder (e.g. /erc20/events/transfer?network=eth&token=USDT&...).
|
|
172
|
+
api_key: Your DeFiStream API key (dsk_xxx).
|
|
173
|
+
file_format: Output format — "json" or "csv" (default "csv").
|
|
174
|
+
limit: Max rows to return for JSON format (default 200, max 10000).
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if not api_key:
|
|
178
|
+
return "Error: api_key is required. Get your key at https://defistream.dev"
|
|
179
|
+
|
|
180
|
+
if file_format not in ("json", "csv"):
|
|
181
|
+
return "Error: file_format must be 'json' or 'csv'."
|
|
182
|
+
|
|
183
|
+
path, params = _parse_query(query)
|
|
184
|
+
params["format"] = file_format
|
|
185
|
+
|
|
186
|
+
# Create a client with the user's API key
|
|
187
|
+
client = create_client_with_key(api_key)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
if file_format == "csv":
|
|
191
|
+
# For CSV, we get raw text response
|
|
192
|
+
resp = await client._http.get(path, params=params)
|
|
193
|
+
resp.raise_for_status()
|
|
194
|
+
headers = {
|
|
195
|
+
k: v
|
|
196
|
+
for k, v in resp.headers.items()
|
|
197
|
+
if k.lower().startswith("x-ratelimit") or k.lower() == "x-request-cost"
|
|
198
|
+
}
|
|
199
|
+
csv_text = resp.text
|
|
200
|
+
|
|
201
|
+
# Count lines and potentially truncate
|
|
202
|
+
lines = csv_text.strip().split("\n")
|
|
203
|
+
effective_limit = min(limit, config.query_row_limit)
|
|
204
|
+
|
|
205
|
+
if len(lines) > effective_limit + 1: # +1 for header
|
|
206
|
+
truncated = lines[: effective_limit + 1]
|
|
207
|
+
result = f"Showing {effective_limit} of {len(lines) - 1} rows (truncated).\n\n"
|
|
208
|
+
result += "\n".join(truncated)
|
|
209
|
+
else:
|
|
210
|
+
result = f"{len(lines) - 1} row(s) returned.\n\n"
|
|
211
|
+
result += csv_text
|
|
212
|
+
|
|
213
|
+
# Add quota info
|
|
214
|
+
remaining = headers.get("x-ratelimit-remaining")
|
|
215
|
+
cost = headers.get("x-request-cost")
|
|
216
|
+
if remaining or cost:
|
|
217
|
+
parts = []
|
|
218
|
+
if cost:
|
|
219
|
+
parts.append(f"cost={cost} CU")
|
|
220
|
+
if remaining:
|
|
221
|
+
parts.append(f"remaining={remaining} CU")
|
|
222
|
+
result += "\n[Quota: " + ", ".join(parts) + "]"
|
|
223
|
+
|
|
224
|
+
return result
|
|
225
|
+
else:
|
|
226
|
+
# JSON format
|
|
227
|
+
body, headers = await client.get_json(path, params)
|
|
228
|
+
|
|
229
|
+
if isinstance(body, dict) and body.get("status") == "error":
|
|
230
|
+
return f"API error: {body.get('error', json.dumps(body))}"
|
|
231
|
+
|
|
232
|
+
effective_limit = min(limit, config.query_row_limit)
|
|
233
|
+
|
|
234
|
+
# Aggregate responses have "data" key, event responses have "events"
|
|
235
|
+
if "data" in body and "events" not in body:
|
|
236
|
+
return format_aggregate_response(body, headers)
|
|
237
|
+
|
|
238
|
+
return format_events_response(body, headers, effective_limit)
|
|
239
|
+
finally:
|
|
240
|
+
await client.close()
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
return f"Error executing query: {exc}"
|
|
243
|
+
|
|
244
|
+
@mcp.tool()
|
|
245
|
+
async def download_query_as_link(
|
|
246
|
+
query: str,
|
|
247
|
+
api_key: str,
|
|
248
|
+
file_format: str = "csv",
|
|
249
|
+
) -> str:
|
|
250
|
+
"""Get a shareable download link for query results.
|
|
251
|
+
|
|
252
|
+
Requires your DeFiStream API key. Supports unlimited block ranges.
|
|
253
|
+
Returns a link that expires in 1 hour - no API key needed to download.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
query: Query path from a query builder.
|
|
257
|
+
api_key: Your DeFiStream API key (dsk_xxx).
|
|
258
|
+
file_format: Output format — "csv" or "parquet" (default "csv").
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
if not api_key:
|
|
262
|
+
return "Error: api_key is required. Get your key at https://defistream.dev"
|
|
263
|
+
|
|
264
|
+
if file_format not in ("csv", "parquet"):
|
|
265
|
+
return "Error: file_format must be 'csv' or 'parquet'."
|
|
266
|
+
|
|
267
|
+
path, params = _parse_query(query)
|
|
268
|
+
params["format"] = file_format
|
|
269
|
+
params["link"] = "true"
|
|
270
|
+
|
|
271
|
+
# Create a client with the user's API key
|
|
272
|
+
client = create_client_with_key(api_key)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
body, headers = await client.get_json(path, params)
|
|
276
|
+
|
|
277
|
+
if isinstance(body, dict) and body.get("status") == "error":
|
|
278
|
+
return f"API error: {body.get('error', json.dumps(body))}"
|
|
279
|
+
|
|
280
|
+
return format_link_response(body, headers)
|
|
281
|
+
finally:
|
|
282
|
+
await client.close()
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
return f"Error getting download link: {exc}"
|
|
285
|
+
|
|
286
|
+
@mcp.tool()
|
|
287
|
+
def query_local_execution_guide(query: str) -> str:
|
|
288
|
+
"""Get instructions for executing a query locally on your system.
|
|
289
|
+
|
|
290
|
+
Use this when you want to run the query from your own environment
|
|
291
|
+
instead of through the MCP server. Returns examples using curl,
|
|
292
|
+
Python (requests/aiohttp), JavaScript (fetch), and the Python client library.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
query: Query path from a query builder.
|
|
296
|
+
"""
|
|
297
|
+
return format_local_execution_guide(query, config.base_url)
|
|
298
|
+
|
|
299
|
+
# -----------------------------------------------------------------------
|
|
300
|
+
# Protocol query builders
|
|
301
|
+
# -----------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
@mcp.tool()
|
|
304
|
+
def erc20_query_builder(
|
|
305
|
+
event_type: str,
|
|
306
|
+
network: str,
|
|
307
|
+
token: str,
|
|
308
|
+
block_start: int | None = None,
|
|
309
|
+
block_end: int | None = None,
|
|
310
|
+
since: str | None = None,
|
|
311
|
+
until: str | None = None,
|
|
312
|
+
verbose: bool = False,
|
|
313
|
+
with_value: bool = False,
|
|
314
|
+
aggregate: bool = False,
|
|
315
|
+
group_by: str = "time",
|
|
316
|
+
period: str = "1h",
|
|
317
|
+
decimals: int | None = None,
|
|
318
|
+
sender: str | None = None,
|
|
319
|
+
receiver: str | None = None,
|
|
320
|
+
involving: str | None = None,
|
|
321
|
+
involving_label: str | None = None,
|
|
322
|
+
involving_category: str | None = None,
|
|
323
|
+
sender_label: str | None = None,
|
|
324
|
+
sender_category: str | None = None,
|
|
325
|
+
receiver_label: str | None = None,
|
|
326
|
+
receiver_category: str | None = None,
|
|
327
|
+
min_amount: float | None = None,
|
|
328
|
+
max_amount: float | None = None,
|
|
329
|
+
) -> str:
|
|
330
|
+
"""Build a query for ERC-20 token events (USDT, USDC, WETH, …).
|
|
331
|
+
|
|
332
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
333
|
+
results or download_query() for CSV/Parquet files.
|
|
334
|
+
|
|
335
|
+
Event types: transfer
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
event_type: Event type (e.g. "transfer").
|
|
339
|
+
network: Network identifier (e.g. "ETH", "ARB", "BASE").
|
|
340
|
+
token: Token symbol or comma-separated symbols for multi-token queries (e.g. "USDT", "USDT,USDC,DAI"). Multi-token only supports known symbol names, not contract addresses. A single contract address (0x…) is also accepted.
|
|
341
|
+
block_start: Starting block number.
|
|
342
|
+
block_end: Ending block number.
|
|
343
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
344
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
345
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
346
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
347
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
348
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
349
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
350
|
+
decimals: Token decimals when using a custom token address instead of symbol.
|
|
351
|
+
sender: Filter by sender address (comma-separated for multiple).
|
|
352
|
+
receiver: Filter by receiver address (comma-separated for multiple).
|
|
353
|
+
involving: Filter by any involved address.
|
|
354
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
355
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
356
|
+
sender_label: Filter sender by label.
|
|
357
|
+
sender_category: Filter sender by category.
|
|
358
|
+
receiver_label: Filter receiver by label.
|
|
359
|
+
receiver_category: Filter receiver by category.
|
|
360
|
+
min_amount: Minimum transfer amount.
|
|
361
|
+
max_amount: Maximum transfer amount.
|
|
362
|
+
"""
|
|
363
|
+
extra: dict[str, Any] = {
|
|
364
|
+
"token": token,
|
|
365
|
+
"decimals": decimals,
|
|
366
|
+
"sender": sender,
|
|
367
|
+
"receiver": receiver,
|
|
368
|
+
"involving": involving,
|
|
369
|
+
"involving_label": involving_label,
|
|
370
|
+
"involving_category": involving_category,
|
|
371
|
+
"sender_label": sender_label,
|
|
372
|
+
"sender_category": sender_category,
|
|
373
|
+
"receiver_label": receiver_label,
|
|
374
|
+
"receiver_category": receiver_category,
|
|
375
|
+
"min_amount": min_amount,
|
|
376
|
+
"max_amount": max_amount,
|
|
377
|
+
}
|
|
378
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
379
|
+
return _build_query(
|
|
380
|
+
"erc20", event_type, network, extra,
|
|
381
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
382
|
+
with_value=with_value,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@mcp.tool()
|
|
386
|
+
def native_token_query_builder(
|
|
387
|
+
event_type: str,
|
|
388
|
+
network: str,
|
|
389
|
+
block_start: int | None = None,
|
|
390
|
+
block_end: int | None = None,
|
|
391
|
+
since: str | None = None,
|
|
392
|
+
until: str | None = None,
|
|
393
|
+
verbose: bool = False,
|
|
394
|
+
with_value: bool = False,
|
|
395
|
+
aggregate: bool = False,
|
|
396
|
+
group_by: str = "time",
|
|
397
|
+
period: str = "1h",
|
|
398
|
+
sender: str | None = None,
|
|
399
|
+
receiver: str | None = None,
|
|
400
|
+
involving: str | None = None,
|
|
401
|
+
involving_label: str | None = None,
|
|
402
|
+
involving_category: str | None = None,
|
|
403
|
+
sender_label: str | None = None,
|
|
404
|
+
sender_category: str | None = None,
|
|
405
|
+
receiver_label: str | None = None,
|
|
406
|
+
receiver_category: str | None = None,
|
|
407
|
+
min_amount: float | None = None,
|
|
408
|
+
max_amount: float | None = None,
|
|
409
|
+
) -> str:
|
|
410
|
+
"""Build a query for native blockchain token transfers (ETH, MATIC, BNB, …).
|
|
411
|
+
|
|
412
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
413
|
+
results or download_query() for CSV/Parquet files.
|
|
414
|
+
|
|
415
|
+
Event types: transfer
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
event_type: Event type (e.g. "transfer").
|
|
419
|
+
network: Network identifier (e.g. "ETH", "ARB", "BASE").
|
|
420
|
+
block_start: Starting block number.
|
|
421
|
+
block_end: Ending block number.
|
|
422
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
423
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
424
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
425
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
426
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
427
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
428
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
429
|
+
sender: Filter by sender address (comma-separated for multiple).
|
|
430
|
+
receiver: Filter by receiver address (comma-separated for multiple).
|
|
431
|
+
involving: Filter by any involved address.
|
|
432
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
433
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
434
|
+
sender_label: Filter sender by label.
|
|
435
|
+
sender_category: Filter sender by category.
|
|
436
|
+
receiver_label: Filter receiver by label.
|
|
437
|
+
receiver_category: Filter receiver by category.
|
|
438
|
+
min_amount: Minimum transfer amount.
|
|
439
|
+
max_amount: Maximum transfer amount.
|
|
440
|
+
"""
|
|
441
|
+
extra: dict[str, Any] = {
|
|
442
|
+
"sender": sender,
|
|
443
|
+
"receiver": receiver,
|
|
444
|
+
"involving": involving,
|
|
445
|
+
"involving_label": involving_label,
|
|
446
|
+
"involving_category": involving_category,
|
|
447
|
+
"sender_label": sender_label,
|
|
448
|
+
"sender_category": sender_category,
|
|
449
|
+
"receiver_label": receiver_label,
|
|
450
|
+
"receiver_category": receiver_category,
|
|
451
|
+
"min_amount": min_amount,
|
|
452
|
+
"max_amount": max_amount,
|
|
453
|
+
}
|
|
454
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
455
|
+
return _build_query(
|
|
456
|
+
"native_token", event_type, network, extra,
|
|
457
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
458
|
+
with_value=with_value,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
@mcp.tool()
|
|
462
|
+
def aave_v3_query_builder(
|
|
463
|
+
event_type: str,
|
|
464
|
+
network: str,
|
|
465
|
+
block_start: int | None = None,
|
|
466
|
+
block_end: int | None = None,
|
|
467
|
+
since: str | None = None,
|
|
468
|
+
until: str | None = None,
|
|
469
|
+
verbose: bool = False,
|
|
470
|
+
with_value: bool = False,
|
|
471
|
+
aggregate: bool = False,
|
|
472
|
+
group_by: str = "time",
|
|
473
|
+
period: str = "1h",
|
|
474
|
+
eth_market_type: str | None = None,
|
|
475
|
+
involving: str | None = None,
|
|
476
|
+
involving_label: str | None = None,
|
|
477
|
+
involving_category: str | None = None,
|
|
478
|
+
) -> str:
|
|
479
|
+
"""Build a query for AAVE V3 lending protocol events.
|
|
480
|
+
|
|
481
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
482
|
+
results or download_query() for CSV/Parquet files.
|
|
483
|
+
|
|
484
|
+
Event types: deposit, withdraw, borrow, repay, flashloan, liquidation
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
event_type: Event type (e.g. "deposit", "withdraw", "borrow", "repay", "flashloan", "liquidation").
|
|
488
|
+
network: Network identifier (e.g. "ETH", "ARB", "BASE").
|
|
489
|
+
block_start: Starting block number.
|
|
490
|
+
block_end: Ending block number.
|
|
491
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
492
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
493
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
494
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
495
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
496
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
497
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
498
|
+
eth_market_type: AAVE market type on ETH — "Core", "Prime", or "EtherFi".
|
|
499
|
+
involving: Filter by any involved address.
|
|
500
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
501
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
502
|
+
"""
|
|
503
|
+
extra: dict[str, Any] = {
|
|
504
|
+
"eth_market_type": eth_market_type,
|
|
505
|
+
"involving": involving,
|
|
506
|
+
"involving_label": involving_label,
|
|
507
|
+
"involving_category": involving_category,
|
|
508
|
+
}
|
|
509
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
510
|
+
return _build_query(
|
|
511
|
+
"aave_v3", event_type, network, extra,
|
|
512
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
513
|
+
with_value=with_value,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
@mcp.tool()
|
|
517
|
+
def uniswap_v3_query_builder(
|
|
518
|
+
event_type: str,
|
|
519
|
+
network: str,
|
|
520
|
+
symbol0: str,
|
|
521
|
+
symbol1: str,
|
|
522
|
+
fee: int,
|
|
523
|
+
block_start: int | None = None,
|
|
524
|
+
block_end: int | None = None,
|
|
525
|
+
since: str | None = None,
|
|
526
|
+
until: str | None = None,
|
|
527
|
+
verbose: bool = False,
|
|
528
|
+
with_value: bool = False,
|
|
529
|
+
aggregate: bool = False,
|
|
530
|
+
group_by: str = "time",
|
|
531
|
+
period: str = "1h",
|
|
532
|
+
involving: str | None = None,
|
|
533
|
+
involving_label: str | None = None,
|
|
534
|
+
involving_category: str | None = None,
|
|
535
|
+
) -> str:
|
|
536
|
+
"""Build a query for Uniswap V3 DEX events.
|
|
537
|
+
|
|
538
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
539
|
+
results or download_query() for CSV/Parquet files.
|
|
540
|
+
|
|
541
|
+
Event types: swap, deposit, withdraw, collect
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
event_type: Event type (e.g. "swap", "deposit", "withdraw", "collect").
|
|
545
|
+
network: Network identifier (e.g. "ETH", "ARB", "BASE").
|
|
546
|
+
symbol0: First token symbol in the pool (e.g. "WETH") — required.
|
|
547
|
+
symbol1: Second token symbol in the pool (e.g. "USDC") — required.
|
|
548
|
+
fee: Pool fee tier (100, 500, 3000, 10000) — required.
|
|
549
|
+
block_start: Starting block number.
|
|
550
|
+
block_end: Ending block number.
|
|
551
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
552
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
553
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
554
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
555
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
556
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
557
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
558
|
+
involving: Filter by any involved address.
|
|
559
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
560
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
561
|
+
"""
|
|
562
|
+
extra: dict[str, Any] = {
|
|
563
|
+
"symbol0": symbol0,
|
|
564
|
+
"symbol1": symbol1,
|
|
565
|
+
"fee": fee,
|
|
566
|
+
"involving": involving,
|
|
567
|
+
"involving_label": involving_label,
|
|
568
|
+
"involving_category": involving_category,
|
|
569
|
+
}
|
|
570
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
571
|
+
return _build_query(
|
|
572
|
+
"uniswap_v3", event_type, network, extra,
|
|
573
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
574
|
+
with_value=with_value,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
@mcp.tool()
|
|
578
|
+
def lido_query_builder(
|
|
579
|
+
event_type: str,
|
|
580
|
+
network: str,
|
|
581
|
+
block_start: int | None = None,
|
|
582
|
+
block_end: int | None = None,
|
|
583
|
+
since: str | None = None,
|
|
584
|
+
until: str | None = None,
|
|
585
|
+
verbose: bool = False,
|
|
586
|
+
with_value: bool = False,
|
|
587
|
+
aggregate: bool = False,
|
|
588
|
+
group_by: str = "time",
|
|
589
|
+
period: str = "1h",
|
|
590
|
+
involving: str | None = None,
|
|
591
|
+
involving_label: str | None = None,
|
|
592
|
+
involving_category: str | None = None,
|
|
593
|
+
) -> str:
|
|
594
|
+
"""Build a query for Lido liquid staking events.
|
|
595
|
+
|
|
596
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
597
|
+
results or download_query() for CSV/Parquet files.
|
|
598
|
+
|
|
599
|
+
Event types: deposit, withdrawal_request, withdrawal_claimed, l2_deposit, l2_withdrawal_request
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
event_type: Event type (e.g. "deposit", "withdrawal_request", "withdrawal_claimed").
|
|
603
|
+
network: Network identifier (e.g. "ETH", "ARB", "BASE", "OP").
|
|
604
|
+
block_start: Starting block number.
|
|
605
|
+
block_end: Ending block number.
|
|
606
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
607
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
608
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
609
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
610
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
611
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
612
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
613
|
+
involving: Filter by any involved address.
|
|
614
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
615
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
616
|
+
"""
|
|
617
|
+
extra: dict[str, Any] = {
|
|
618
|
+
"involving": involving,
|
|
619
|
+
"involving_label": involving_label,
|
|
620
|
+
"involving_category": involving_category,
|
|
621
|
+
}
|
|
622
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
623
|
+
return _build_query(
|
|
624
|
+
"lido", event_type, network, extra,
|
|
625
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
626
|
+
with_value=with_value,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
@mcp.tool()
|
|
630
|
+
def stader_query_builder(
|
|
631
|
+
event_type: str,
|
|
632
|
+
network: str,
|
|
633
|
+
block_start: int | None = None,
|
|
634
|
+
block_end: int | None = None,
|
|
635
|
+
since: str | None = None,
|
|
636
|
+
until: str | None = None,
|
|
637
|
+
verbose: bool = False,
|
|
638
|
+
with_value: bool = False,
|
|
639
|
+
aggregate: bool = False,
|
|
640
|
+
group_by: str = "time",
|
|
641
|
+
period: str = "1h",
|
|
642
|
+
involving: str | None = None,
|
|
643
|
+
involving_label: str | None = None,
|
|
644
|
+
involving_category: str | None = None,
|
|
645
|
+
) -> str:
|
|
646
|
+
"""Build a query for Stader ETHx staking events.
|
|
647
|
+
|
|
648
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
649
|
+
results or download_query() for CSV/Parquet files.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
event_type: Event type.
|
|
653
|
+
network: Network identifier (e.g. "ETH").
|
|
654
|
+
block_start: Starting block number.
|
|
655
|
+
block_end: Ending block number.
|
|
656
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
657
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
658
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
659
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
660
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
661
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
662
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
663
|
+
involving: Filter by any involved address.
|
|
664
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
665
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
666
|
+
"""
|
|
667
|
+
extra: dict[str, Any] = {
|
|
668
|
+
"involving": involving,
|
|
669
|
+
"involving_label": involving_label,
|
|
670
|
+
"involving_category": involving_category,
|
|
671
|
+
}
|
|
672
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
673
|
+
return _build_query(
|
|
674
|
+
"stader", event_type, network, extra,
|
|
675
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
676
|
+
with_value=with_value,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
@mcp.tool()
|
|
680
|
+
def threshold_query_builder(
|
|
681
|
+
event_type: str,
|
|
682
|
+
network: str,
|
|
683
|
+
block_start: int | None = None,
|
|
684
|
+
block_end: int | None = None,
|
|
685
|
+
since: str | None = None,
|
|
686
|
+
until: str | None = None,
|
|
687
|
+
verbose: bool = False,
|
|
688
|
+
with_value: bool = False,
|
|
689
|
+
aggregate: bool = False,
|
|
690
|
+
group_by: str = "time",
|
|
691
|
+
period: str = "1h",
|
|
692
|
+
involving: str | None = None,
|
|
693
|
+
involving_label: str | None = None,
|
|
694
|
+
involving_category: str | None = None,
|
|
695
|
+
) -> str:
|
|
696
|
+
"""Build a query for Threshold tBTC bridge events.
|
|
697
|
+
|
|
698
|
+
Returns a query path string. Pass it to execute_query() for JSON
|
|
699
|
+
results or download_query() for CSV/Parquet files.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
event_type: Event type.
|
|
703
|
+
network: Network identifier (e.g. "ETH").
|
|
704
|
+
block_start: Starting block number.
|
|
705
|
+
block_end: Ending block number.
|
|
706
|
+
since: Start time (ISO 8601 or Unix timestamp).
|
|
707
|
+
until: End time (ISO 8601 or Unix timestamp).
|
|
708
|
+
verbose: Include all metadata fields (tx_hash, log_index, etc.).
|
|
709
|
+
with_value: Enrich events with USD value data (adds value_usd column; agg_value_usd on aggregates).
|
|
710
|
+
aggregate: Set True to build an aggregate query instead of raw events.
|
|
711
|
+
group_by: Bucket grouping — "time" or "block_number" (aggregate only).
|
|
712
|
+
period: Bucket size e.g. "1h", "1d", "30m" for time; "1000" for blocks (aggregate only).
|
|
713
|
+
involving: Filter by any involved address.
|
|
714
|
+
involving_label: Filter by entity label substring (e.g. "Binance").
|
|
715
|
+
involving_category: Filter by address category (e.g. "exchange").
|
|
716
|
+
"""
|
|
717
|
+
extra: dict[str, Any] = {
|
|
718
|
+
"involving": involving,
|
|
719
|
+
"involving_label": involving_label,
|
|
720
|
+
"involving_category": involving_category,
|
|
721
|
+
}
|
|
722
|
+
extra.update(_build_range_params(block_start, block_end, since, until))
|
|
723
|
+
return _build_query(
|
|
724
|
+
"threshold", event_type, network, extra,
|
|
725
|
+
aggregate=aggregate, group_by=group_by, period=period, verbose=verbose,
|
|
726
|
+
with_value=with_value,
|
|
727
|
+
)
|