tradingview-mcp 1.0.0__py3-none-any.whl → 1.1.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.
- tradingview_mcp/data/__init__.py +9 -0
- tradingview_mcp/data/column_display_names.json +827 -0
- tradingview_mcp/data/extracted/__init__.py +1 -0
- tradingview_mcp/data/extracted/ai_quick_reference.json +212 -0
- tradingview_mcp/data/extracted/common_fields.json +3627 -0
- tradingview_mcp/data/extracted/fields_by_market.json +23299 -0
- tradingview_mcp/data/extracted/screener_code_examples.json +9 -0
- tradingview_mcp/data/markets.json +83 -0
- tradingview_mcp/data/metainfo/bond.json +17406 -0
- tradingview_mcp/data/metainfo/bonds.json +1303 -0
- tradingview_mcp/data/metainfo/cfd.json +21603 -0
- tradingview_mcp/data/metainfo/coin.json +21111 -0
- tradingview_mcp/data/metainfo/crypto.json +23078 -0
- tradingview_mcp/data/metainfo/economics2.json +1026 -0
- tradingview_mcp/data/metainfo/forex.json +21003 -0
- tradingview_mcp/data/metainfo/futures.json +2972 -0
- tradingview_mcp/data/metainfo/ireland.json +22517 -0
- tradingview_mcp/data/metainfo/options.json +499 -0
- tradingview_mcp/data/metainfo/stocks.json +29808 -0
- tradingview_mcp/data/screeners/main_screeners.json +540 -0
- tradingview_mcp/data/screeners/markets.json +70 -0
- tradingview_mcp/data/screeners/stocks.json +416 -0
- tradingview_mcp/data/screeners/stocks_failed.json +36081 -0
- tradingview_mcp/docs_data.py +297 -0
- tradingview_mcp/server.py +471 -1
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/METADATA +131 -5
- tradingview_mcp-1.1.0.dist-info/RECORD +37 -0
- tradingview_mcp-1.0.0.dist-info/RECORD +0 -13
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/licenses/LICENSE +0 -0
tradingview_mcp/server.py
CHANGED
|
@@ -7,8 +7,12 @@ Provides comprehensive MCP tools and resources for TradingView market screening.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import inspect
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
13
|
+
import traceback
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from functools import wraps
|
|
12
16
|
from typing import Any, Optional
|
|
13
17
|
|
|
14
18
|
from mcp.server.fastmcp import FastMCP
|
|
@@ -38,6 +42,23 @@ from tradingview_mcp.utils import (
|
|
|
38
42
|
sanitize_timeframe,
|
|
39
43
|
timeframe_to_resolution,
|
|
40
44
|
)
|
|
45
|
+
from tradingview_mcp.docs_data import (
|
|
46
|
+
get_ai_quick_reference,
|
|
47
|
+
get_column_display_names,
|
|
48
|
+
get_common_fields,
|
|
49
|
+
get_fields_by_market,
|
|
50
|
+
get_markets_data,
|
|
51
|
+
get_metainfo as load_metainfo,
|
|
52
|
+
get_screener_code_examples,
|
|
53
|
+
get_screener_markets,
|
|
54
|
+
get_screener_presets,
|
|
55
|
+
paginate_data,
|
|
56
|
+
search_fields,
|
|
57
|
+
get_field_summary,
|
|
58
|
+
estimate_tokens,
|
|
59
|
+
truncate_for_tokens,
|
|
60
|
+
MAX_ITEMS_DEFAULT,
|
|
61
|
+
)
|
|
41
62
|
|
|
42
63
|
# Initialize MCP Server
|
|
43
64
|
mcp = FastMCP(
|
|
@@ -45,10 +66,101 @@ mcp = FastMCP(
|
|
|
45
66
|
instructions=(
|
|
46
67
|
"A comprehensive MCP server for TradingView market screening. "
|
|
47
68
|
"Provides tools for stock and cryptocurrency screening, technical analysis, "
|
|
48
|
-
"and market data retrieval. Supports 76+ markets and 250+ technical indicators."
|
|
69
|
+
"and market data retrieval. Supports 76+ markets and 250+ technical indicators. "
|
|
70
|
+
"Docs resources: docs://params, docs://screeners, docs://fields, docs://markets. "
|
|
71
|
+
"Set TRADINGVIEW_MCP_DEBUG=1 for detailed debug responses on errors."
|
|
49
72
|
),
|
|
50
73
|
)
|
|
51
74
|
|
|
75
|
+
DEBUG_MODE = os.getenv("TRADINGVIEW_MCP_DEBUG", "0").lower() in {"1", "true", "yes"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _safe_json(value: Any) -> Any:
|
|
79
|
+
try:
|
|
80
|
+
return json.loads(json.dumps(value, default=str))
|
|
81
|
+
except Exception:
|
|
82
|
+
return str(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _debug_hint(exc: Exception) -> str | None:
|
|
86
|
+
message = str(exc).lower()
|
|
87
|
+
if "market" in message and "invalid" in message:
|
|
88
|
+
return "Check the market name using the markets://list or docs://screeners resources."
|
|
89
|
+
if "column" in message and "unknown" in message:
|
|
90
|
+
return "Check available columns via columns://list or docs://fields."
|
|
91
|
+
if "http" in message or "status" in message:
|
|
92
|
+
return "TradingView API may be rate-limiting or blocked. Retry with fewer requests."
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_error_response(tool_name: str, exc: Exception, context: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
payload: dict[str, Any] = {
|
|
98
|
+
"ok": False,
|
|
99
|
+
"tool": tool_name,
|
|
100
|
+
"error": {"type": exc.__class__.__name__, "message": str(exc)},
|
|
101
|
+
"context": _safe_json(context),
|
|
102
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
103
|
+
}
|
|
104
|
+
hint = _debug_hint(exc)
|
|
105
|
+
if hint:
|
|
106
|
+
payload["hint"] = hint
|
|
107
|
+
if DEBUG_MODE:
|
|
108
|
+
payload["trace"] = traceback.format_exc()
|
|
109
|
+
return payload
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _error_response(exc: Exception, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
113
|
+
frame = inspect.currentframe()
|
|
114
|
+
caller = frame.f_back.f_code.co_name if frame and frame.f_back else "unknown"
|
|
115
|
+
return _build_error_response(caller, exc, context or {})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def debug_tool(fn):
|
|
119
|
+
"""Decorator to return structured debug responses on failure."""
|
|
120
|
+
|
|
121
|
+
@wraps(fn)
|
|
122
|
+
def wrapper(*args, **kwargs):
|
|
123
|
+
try:
|
|
124
|
+
result = fn(*args, **kwargs)
|
|
125
|
+
if isinstance(result, dict) and "error" in result:
|
|
126
|
+
return _build_error_response(
|
|
127
|
+
fn.__name__,
|
|
128
|
+
Exception(str(result.get("error"))),
|
|
129
|
+
{"args": args, "kwargs": kwargs, "note": "error returned by tool"},
|
|
130
|
+
)
|
|
131
|
+
if (
|
|
132
|
+
isinstance(result, list)
|
|
133
|
+
and len(result) == 1
|
|
134
|
+
and isinstance(result[0], dict)
|
|
135
|
+
and "error" in result[0]
|
|
136
|
+
):
|
|
137
|
+
return _build_error_response(
|
|
138
|
+
fn.__name__,
|
|
139
|
+
Exception(str(result[0].get("error"))),
|
|
140
|
+
{"args": args, "kwargs": kwargs, "note": "error returned by tool"},
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
143
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
144
|
+
context = {"args": args, "kwargs": kwargs}
|
|
145
|
+
return _build_error_response(fn.__name__, exc, context)
|
|
146
|
+
|
|
147
|
+
return wrapper
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_original_mcp_tool = mcp.tool
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _debug_mcp_tool(*args, **kwargs):
|
|
154
|
+
"""Wrap FastMCP tool registration with debug handling."""
|
|
155
|
+
|
|
156
|
+
def decorator(fn):
|
|
157
|
+
return _original_mcp_tool(*args, **kwargs)(debug_tool(fn))
|
|
158
|
+
|
|
159
|
+
return decorator
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
mcp.tool = _debug_mcp_tool
|
|
163
|
+
|
|
52
164
|
|
|
53
165
|
# =============================================================================
|
|
54
166
|
# MCP Resources
|
|
@@ -242,6 +354,364 @@ def list_scanner_presets() -> str:
|
|
|
242
354
|
return result
|
|
243
355
|
|
|
244
356
|
|
|
357
|
+
@mcp.resource("docs://params")
|
|
358
|
+
def docs_params() -> str:
|
|
359
|
+
"""TradingView screener parameter format and schema."""
|
|
360
|
+
schema = {
|
|
361
|
+
"markets": ["america"],
|
|
362
|
+
"columns": ["close", "volume", "market_cap_basic"],
|
|
363
|
+
"filter": [
|
|
364
|
+
{"left": "change", "operation": "gt", "right": 2},
|
|
365
|
+
{"left": "volume", "operation": "greater", "right": 1000000},
|
|
366
|
+
],
|
|
367
|
+
"filter2": {
|
|
368
|
+
"operator": "and",
|
|
369
|
+
"operands": [
|
|
370
|
+
{
|
|
371
|
+
"expression": {"left": "type", "operation": "equal", "right": "stock"}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"expression": {"left": "is_primary", "operation": "equal", "right": True}
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
"symbols": {"query": {"types": []}, "tickers": []},
|
|
379
|
+
"sort": {"sortBy": "market_cap_basic", "sortOrder": "desc"},
|
|
380
|
+
"range": [0, 50],
|
|
381
|
+
"options": {"lang": "en"},
|
|
382
|
+
"ignore_unknown_fields": False,
|
|
383
|
+
"price_conversion": {"to_symbol": False},
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
format_notes = (
|
|
387
|
+
"# Screener Params Format\n\n"
|
|
388
|
+
"## Core fields\n"
|
|
389
|
+
"- markets: list of market identifiers (e.g., america, crypto, forex)\n"
|
|
390
|
+
"- columns: list of fields to return\n"
|
|
391
|
+
"- filter: flat list of expressions\n"
|
|
392
|
+
"- filter2: nested boolean logic tree (and/or)\n"
|
|
393
|
+
"- symbols: optional tickers/types filter\n"
|
|
394
|
+
"- sort: {sortBy, sortOrder}\n"
|
|
395
|
+
"- range: [offset, limit]\n"
|
|
396
|
+
"- options: {lang}\n"
|
|
397
|
+
"- ignore_unknown_fields: boolean\n"
|
|
398
|
+
"- price_conversion: {to_symbol}\n\n"
|
|
399
|
+
"## Expression format\n"
|
|
400
|
+
"{left: <field>, operation: <op>, right: <value>}\n\n"
|
|
401
|
+
"Common operations: equal, ne, gt, gte, lt, lte, in_range, has, has_none_of\n\n"
|
|
402
|
+
"## Example schema\n"
|
|
403
|
+
)
|
|
404
|
+
return format_notes + json.dumps(schema, ensure_ascii=False, indent=2)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@mcp.resource("docs://screeners")
|
|
408
|
+
def docs_screeners() -> str:
|
|
409
|
+
"""Predefined screener presets from docs data (compact overview)."""
|
|
410
|
+
presets = get_screener_presets()
|
|
411
|
+
overview = [
|
|
412
|
+
{"name": p.get("name"), "url": p.get("url"), "has_query": bool(p.get("query"))}
|
|
413
|
+
for p in presets
|
|
414
|
+
]
|
|
415
|
+
return json.dumps(overview, ensure_ascii=False, indent=2)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@mcp.resource("docs://fields")
|
|
419
|
+
def docs_fields() -> str:
|
|
420
|
+
"""Field display names (first 100). Use search_available_fields tool for more."""
|
|
421
|
+
mapping = get_column_display_names()
|
|
422
|
+
# Limit output to save tokens
|
|
423
|
+
limited = dict(list(mapping.items())[:100])
|
|
424
|
+
result = {
|
|
425
|
+
"fields": limited,
|
|
426
|
+
"total": len(mapping),
|
|
427
|
+
"note": "Showing first 100 fields. Use search_available_fields() tool to search for specific fields.",
|
|
428
|
+
}
|
|
429
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@mcp.resource("docs://markets")
|
|
433
|
+
def docs_markets() -> str:
|
|
434
|
+
"""Markets metadata (compact). Use ai_get_reference tool for categorized list."""
|
|
435
|
+
markets = get_markets_data()
|
|
436
|
+
screener_markets = get_screener_markets()
|
|
437
|
+
return json.dumps(
|
|
438
|
+
{
|
|
439
|
+
"markets_count": len(markets) if isinstance(markets, (list, dict)) else 0,
|
|
440
|
+
"screener_markets": screener_markets,
|
|
441
|
+
"hint": "Use ai_get_reference() tool for full categorized market list.",
|
|
442
|
+
},
|
|
443
|
+
ensure_ascii=False,
|
|
444
|
+
indent=2,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@mcp.tool()
|
|
449
|
+
def get_field_info(field: str) -> dict[str, Any]:
|
|
450
|
+
"""Get display name, type, and related info for a field.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
field: Field/column name to look up
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Field info including display name, type, and variants if available
|
|
457
|
+
"""
|
|
458
|
+
# Try get_field_summary first (uses common_fields)
|
|
459
|
+
summary = get_field_summary(field)
|
|
460
|
+
if summary and summary.get("display_name"):
|
|
461
|
+
return summary
|
|
462
|
+
|
|
463
|
+
# Fallback to display names mapping
|
|
464
|
+
mapping = get_column_display_names()
|
|
465
|
+
display = mapping.get(field)
|
|
466
|
+
if display:
|
|
467
|
+
return {"field": field, "display_name": display}
|
|
468
|
+
|
|
469
|
+
# Try case-insensitive or partial matches
|
|
470
|
+
normalized = field.lower()
|
|
471
|
+
suggestions = [
|
|
472
|
+
name for name in list(mapping.keys())[:500]
|
|
473
|
+
if normalized in name.lower() or normalized in str(mapping[name]).lower()
|
|
474
|
+
][:10]
|
|
475
|
+
return {
|
|
476
|
+
"field": field,
|
|
477
|
+
"display_name": None,
|
|
478
|
+
"suggestions": suggestions,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@mcp.tool()
|
|
483
|
+
def get_screener_preset(name: str) -> dict[str, Any]:
|
|
484
|
+
"""Return a full screener preset (query + code) by name."""
|
|
485
|
+
presets = get_screener_presets()
|
|
486
|
+
for preset in presets:
|
|
487
|
+
if preset.get("name", "").lower() == name.lower():
|
|
488
|
+
return preset
|
|
489
|
+
return {
|
|
490
|
+
"error": f"Preset '{name}' not found.",
|
|
491
|
+
"available": [p.get("name") for p in presets],
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@mcp.tool()
|
|
496
|
+
def get_market_metainfo(
|
|
497
|
+
market: str,
|
|
498
|
+
limit: int = 50,
|
|
499
|
+
offset: int = 0,
|
|
500
|
+
confirm_large: bool = False,
|
|
501
|
+
) -> dict[str, Any]:
|
|
502
|
+
"""Return metainfo (field definitions and allowed values) for a market.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
market: Market type (stocks, crypto, forex, bond, etc.)
|
|
506
|
+
limit: Maximum fields to return (default 50, max 500)
|
|
507
|
+
offset: Starting offset for pagination
|
|
508
|
+
confirm_large: Set True to allow returning more than 100 items
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Paginated metainfo with field definitions
|
|
512
|
+
"""
|
|
513
|
+
limit = min(limit, 500) # Hard cap
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
metainfo = load_metainfo(market)
|
|
517
|
+
|
|
518
|
+
# Paginate if it's a list or large dict
|
|
519
|
+
if isinstance(metainfo, list):
|
|
520
|
+
return paginate_data(metainfo, offset, limit, confirm_large)
|
|
521
|
+
elif isinstance(metainfo, dict):
|
|
522
|
+
# Return summary if too large
|
|
523
|
+
total_fields = len(metainfo.get("fields", []))
|
|
524
|
+
if total_fields > MAX_ITEMS_DEFAULT and not confirm_large:
|
|
525
|
+
return {
|
|
526
|
+
"market": market,
|
|
527
|
+
"total_fields": total_fields,
|
|
528
|
+
"warning": f"⚠️ Large data ({total_fields} fields). Use limit/offset to paginate or set confirm_large=True.",
|
|
529
|
+
"sample_fields": list(metainfo.get("fields", {}).keys())[:20] if isinstance(metainfo.get("fields"), dict) else None,
|
|
530
|
+
}
|
|
531
|
+
return {"market": market, "metainfo": metainfo}
|
|
532
|
+
return {"market": market, "metainfo": metainfo}
|
|
533
|
+
except FileNotFoundError:
|
|
534
|
+
return {
|
|
535
|
+
"error": f"Metainfo for '{market}' not found.",
|
|
536
|
+
"available_markets": ["stocks", "crypto", "forex", "bond", "bonds", "cfd", "futures", "options", "coin", "economics2"],
|
|
537
|
+
"hint": "Use one of the available markets listed above.",
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# =============================================================================
|
|
542
|
+
# AI-Friendly Tools - Quick Reference and Search
|
|
543
|
+
# =============================================================================
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@mcp.resource("docs://ai-reference")
|
|
547
|
+
def docs_ai_reference() -> str:
|
|
548
|
+
"""AI quick reference guide with common patterns, markets, and filters."""
|
|
549
|
+
ref = get_ai_quick_reference()
|
|
550
|
+
return json.dumps(ref, ensure_ascii=False, indent=2)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@mcp.resource("docs://code-examples")
|
|
554
|
+
def docs_code_examples() -> str:
|
|
555
|
+
"""Python code examples for common screener queries."""
|
|
556
|
+
examples = get_screener_code_examples()
|
|
557
|
+
return json.dumps(examples, ensure_ascii=False, indent=2)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@mcp.tool()
|
|
561
|
+
def ai_get_reference() -> dict[str, Any]:
|
|
562
|
+
"""Get AI-friendly quick reference for this MCP.
|
|
563
|
+
|
|
564
|
+
Returns a compact guide with:
|
|
565
|
+
- Available markets by category
|
|
566
|
+
- Common columns by type
|
|
567
|
+
- Filter operations
|
|
568
|
+
- Common filter patterns
|
|
569
|
+
- Timeframes
|
|
570
|
+
|
|
571
|
+
This is the recommended starting point for AI agents to understand how to use this MCP.
|
|
572
|
+
"""
|
|
573
|
+
return get_ai_quick_reference()
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@mcp.tool()
|
|
577
|
+
def search_available_fields(
|
|
578
|
+
query: str,
|
|
579
|
+
market: str = "stocks",
|
|
580
|
+
limit: int = 20,
|
|
581
|
+
) -> dict[str, Any]:
|
|
582
|
+
"""Search for fields/columns by name or description.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
query: Search term (e.g., "volume", "RSI", "market cap")
|
|
586
|
+
market: Market type to search in (stocks, crypto, forex, etc.)
|
|
587
|
+
limit: Maximum results (default 20, max 50)
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Matching fields with their display names and types
|
|
591
|
+
"""
|
|
592
|
+
limit = min(limit, 50)
|
|
593
|
+
results = search_fields(query, market, limit)
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
"query": query,
|
|
597
|
+
"market": market,
|
|
598
|
+
"count": len(results),
|
|
599
|
+
"fields": results,
|
|
600
|
+
"hint": "Use the 'name' field as the column name in queries." if results else "No matches found. Try a different search term.",
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@mcp.tool()
|
|
605
|
+
def get_code_example(screener_name: str) -> dict[str, Any]:
|
|
606
|
+
"""Get Python code example for a specific screener type.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
screener_name: Screener name (e.g., "Stocks (legacy)", "Crypto", "Forex", "ETFs", "Bonds")
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Python code example for building that screener query
|
|
613
|
+
"""
|
|
614
|
+
examples = get_screener_code_examples()
|
|
615
|
+
|
|
616
|
+
# Try exact match first
|
|
617
|
+
if screener_name in examples:
|
|
618
|
+
return {
|
|
619
|
+
"name": screener_name,
|
|
620
|
+
"code": examples[screener_name],
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# Try case-insensitive match
|
|
624
|
+
for name, code in examples.items():
|
|
625
|
+
if name.lower() == screener_name.lower():
|
|
626
|
+
return {"name": name, "code": code}
|
|
627
|
+
|
|
628
|
+
# Return available options
|
|
629
|
+
return {
|
|
630
|
+
"error": f"Example '{screener_name}' not found.",
|
|
631
|
+
"available": list(examples.keys()),
|
|
632
|
+
"hint": "Use one of the available screener names listed above.",
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@mcp.tool()
|
|
637
|
+
def list_fields_for_market(
|
|
638
|
+
market: str = "stocks",
|
|
639
|
+
category: str | None = None,
|
|
640
|
+
limit: int = 50,
|
|
641
|
+
offset: int = 0,
|
|
642
|
+
confirm_large: bool = False,
|
|
643
|
+
) -> dict[str, Any]:
|
|
644
|
+
"""List available fields for a specific market with pagination.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
market: Market type (stocks, crypto, forex, coin, bond, cfd, futures, options)
|
|
648
|
+
category: Optional filter by field type (price, volume, fundamental, technical)
|
|
649
|
+
limit: Maximum results (default 50, max 200)
|
|
650
|
+
offset: Starting offset for pagination
|
|
651
|
+
confirm_large: Set True to allow more than 100 results
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Paginated list of field definitions
|
|
655
|
+
"""
|
|
656
|
+
limit = min(limit, 200)
|
|
657
|
+
|
|
658
|
+
fields = get_fields_by_market(market)
|
|
659
|
+
|
|
660
|
+
if not fields:
|
|
661
|
+
return {
|
|
662
|
+
"error": f"No fields found for market '{market}'.",
|
|
663
|
+
"available_markets": ["stocks", "crypto", "forex", "coin", "bond", "bonds", "cfd", "futures", "options", "economics2", "ireland"],
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
# Filter by category if specified
|
|
667
|
+
if category and isinstance(fields, list):
|
|
668
|
+
category_lower = category.lower()
|
|
669
|
+
fields = [f for f in fields if category_lower in f.get("type", "").lower()]
|
|
670
|
+
|
|
671
|
+
return paginate_data(fields, offset, limit, confirm_large)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@mcp.tool()
|
|
675
|
+
def get_common_fields_summary(
|
|
676
|
+
category: str | None = None,
|
|
677
|
+
limit: int = 30,
|
|
678
|
+
) -> dict[str, Any]:
|
|
679
|
+
"""Get summary of commonly used fields across all markets.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
category: Optional filter (price, volume, fundamental, technical, rating)
|
|
683
|
+
limit: Maximum fields to return (default 30)
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Dict with field names, display names, and types
|
|
687
|
+
"""
|
|
688
|
+
ref = get_ai_quick_reference()
|
|
689
|
+
common_cols = ref.get("common_columns", {})
|
|
690
|
+
|
|
691
|
+
if category:
|
|
692
|
+
category_lower = category.lower()
|
|
693
|
+
if category_lower in common_cols:
|
|
694
|
+
return {
|
|
695
|
+
"category": category,
|
|
696
|
+
"fields": common_cols[category_lower],
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
"error": f"Category '{category}' not found.",
|
|
700
|
+
"available_categories": list(common_cols.keys()),
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
# Return all categories with limited fields
|
|
704
|
+
result = {}
|
|
705
|
+
for cat, fields in common_cols.items():
|
|
706
|
+
result[cat] = fields[:limit] if len(fields) > limit else fields
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
"categories": result,
|
|
710
|
+
"total_categories": len(common_cols),
|
|
711
|
+
"hint": "Use category parameter to filter by type.",
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
|
|
245
715
|
# =============================================================================
|
|
246
716
|
# MCP Tools - Basic Screening
|
|
247
717
|
# =============================================================================
|