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.
Files changed (31) hide show
  1. tradingview_mcp/data/__init__.py +9 -0
  2. tradingview_mcp/data/column_display_names.json +827 -0
  3. tradingview_mcp/data/extracted/__init__.py +1 -0
  4. tradingview_mcp/data/extracted/ai_quick_reference.json +212 -0
  5. tradingview_mcp/data/extracted/common_fields.json +3627 -0
  6. tradingview_mcp/data/extracted/fields_by_market.json +23299 -0
  7. tradingview_mcp/data/extracted/screener_code_examples.json +9 -0
  8. tradingview_mcp/data/markets.json +83 -0
  9. tradingview_mcp/data/metainfo/bond.json +17406 -0
  10. tradingview_mcp/data/metainfo/bonds.json +1303 -0
  11. tradingview_mcp/data/metainfo/cfd.json +21603 -0
  12. tradingview_mcp/data/metainfo/coin.json +21111 -0
  13. tradingview_mcp/data/metainfo/crypto.json +23078 -0
  14. tradingview_mcp/data/metainfo/economics2.json +1026 -0
  15. tradingview_mcp/data/metainfo/forex.json +21003 -0
  16. tradingview_mcp/data/metainfo/futures.json +2972 -0
  17. tradingview_mcp/data/metainfo/ireland.json +22517 -0
  18. tradingview_mcp/data/metainfo/options.json +499 -0
  19. tradingview_mcp/data/metainfo/stocks.json +29808 -0
  20. tradingview_mcp/data/screeners/main_screeners.json +540 -0
  21. tradingview_mcp/data/screeners/markets.json +70 -0
  22. tradingview_mcp/data/screeners/stocks.json +416 -0
  23. tradingview_mcp/data/screeners/stocks_failed.json +36081 -0
  24. tradingview_mcp/docs_data.py +297 -0
  25. tradingview_mcp/server.py +471 -1
  26. {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/METADATA +131 -5
  27. tradingview_mcp-1.1.0.dist-info/RECORD +37 -0
  28. tradingview_mcp-1.0.0.dist-info/RECORD +0 -13
  29. {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/WHEEL +0 -0
  30. {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.1.0.dist-info}/entry_points.txt +0 -0
  31. {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
  # =============================================================================