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.
@@ -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
+ )