futu-stock-mcp-server 0.1.4__py3-none-any.whl → 0.1.6__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.

Potentially problematic release.


This version of futu-stock-mcp-server might be problematic. Click here for more details.

@@ -51,23 +51,29 @@ class StdoutProtector:
51
51
  def flush(self):
52
52
  self.original.flush()
53
53
 
54
+ def readable(self):
55
+ """Delegate readable() method to original stdout"""
56
+ return getattr(self.original, 'readable', lambda: False)()
57
+
58
+ def writable(self):
59
+ """Delegate writable() method to original stdout"""
60
+ return getattr(self.original, 'writable', lambda: True)()
61
+
62
+ def seekable(self):
63
+ """Delegate seekable() method to original stdout"""
64
+ return getattr(self.original, 'seekable', lambda: False)()
65
+
54
66
  def __getattr__(self, name):
55
67
  return getattr(self.original, name)
56
68
 
57
69
  # Apply stdout protection in MCP mode VERY EARLY, before any imports
58
70
  if os.getenv('MCP_MODE') == '1':
59
- sys.stdout = StdoutProtector(sys.stdout)
71
+ # sys.stdout = StdoutProtector(sys.stdout)
60
72
 
61
- # 5. Redirect stderr to null in MCP mode to prevent any accidental output
73
+ # 5. Don't redirect stderr in MCP mode - let it work normally
74
+ # MCP servers can use stderr for logging, only stdout needs protection
62
75
  _stderr_redirected = False
63
76
  _stderr_backup = None
64
- if os.getenv('MCP_MODE') == '1':
65
- # Save original stderr for emergency use
66
- _stderr_backup = sys.stderr
67
- # Redirect stderr to devnull
68
- devnull = open(os.devnull, 'w')
69
- sys.stderr = devnull
70
- _stderr_redirected = True
71
77
 
72
78
  # Now we can safely import other modules
73
79
  from contextlib import asynccontextmanager
@@ -263,6 +269,8 @@ trade_ctx = None
263
269
  lock_fd = None
264
270
  _is_shutting_down = False
265
271
  _is_trade_initialized = False
272
+ _futu_host = '127.0.0.1'
273
+ _futu_port = 11111
266
274
 
267
275
  def is_process_running(pid):
268
276
  """Check if a process with given PID is running"""
@@ -436,14 +444,14 @@ def acquire_lock():
436
444
 
437
445
  def init_quote_connection():
438
446
  """Initialize quote connection only"""
439
- global quote_ctx
447
+ global quote_ctx, _futu_host, _futu_port
440
448
 
441
449
  try:
442
450
  # Check if OpenD is running by attempting to get global state
443
451
  try:
444
452
  temp_ctx = OpenQuoteContext(
445
- host=os.getenv('FUTU_HOST', '127.0.0.1'),
446
- port=int(os.getenv('FUTU_PORT', '11111'))
453
+ host=_futu_host,
454
+ port=_futu_port
447
455
  )
448
456
  ret, _ = temp_ctx.get_global_state()
449
457
  temp_ctx.close()
@@ -456,8 +464,8 @@ def init_quote_connection():
456
464
 
457
465
  # Initialize Futu connection
458
466
  quote_ctx = OpenQuoteContext(
459
- host=os.getenv('FUTU_HOST', '127.0.0.1'),
460
- port=int(os.getenv('FUTU_PORT', '11111'))
467
+ host=_futu_host,
468
+ port=_futu_port
461
469
  )
462
470
  logger.info("Successfully connected to Futu Quote API")
463
471
  return True
@@ -469,7 +477,7 @@ def init_quote_connection():
469
477
 
470
478
  def init_trade_connection():
471
479
  """Initialize trade connection only"""
472
- global trade_ctx, _is_trade_initialized
480
+ global trade_ctx, _is_trade_initialized, _futu_host, _futu_port
473
481
 
474
482
  if _is_trade_initialized and trade_ctx:
475
483
  return True
@@ -489,8 +497,8 @@ def init_trade_connection():
489
497
  # 创建交易上下文
490
498
  trade_ctx = OpenSecTradeContext(
491
499
  filter_trdmarket=trd_market,
492
- host=os.getenv('FUTU_HOST', '127.0.0.1'),
493
- port=int(os.getenv('FUTU_PORT', '11111')),
500
+ host=_futu_host,
501
+ port=_futu_port,
494
502
  security_firm=security_firm
495
503
  )
496
504
 
@@ -554,110 +562,76 @@ def init_trade_connection():
554
562
  _is_trade_initialized = False
555
563
  return False
556
564
 
557
- def init_futu_connection() -> bool:
565
+ def init_futu_connection(host: str = '127.0.0.1', port: int = 11111) -> bool:
558
566
  """
559
567
  Initialize connection to Futu OpenD.
568
+
569
+ Args:
570
+ host: Futu OpenD host address
571
+ port: Futu OpenD port number
572
+
560
573
  Returns True if successful, False otherwise.
561
574
  """
562
- global quote_ctx, trade_ctx, _is_trade_initialized
575
+ global quote_ctx, trade_ctx, _is_trade_initialized, _futu_host, _futu_port
563
576
 
564
577
  try:
565
- # Get connection parameters from environment
566
- host = os.getenv('FUTU_HOST', '127.0.0.1')
567
- port = int(os.getenv('FUTU_PORT', '11111'))
578
+ # Set global connection parameters
579
+ _futu_host = host
580
+ _futu_port = port
568
581
 
569
582
  # Log to file only
570
583
  logger.info(f"Initializing Futu connection to {host}:{port}")
571
584
 
572
- # Temporarily redirect stderr to capture any futu library output
573
- original_stderr = None
574
- futu_log_file = None
575
- futu_log_fd = None
576
-
577
- if os.getenv('MCP_MODE') == '1' and _stderr_redirected:
578
- try:
579
- # Create a special log file for futu connection logs
580
- home_dir = os.path.expanduser("~")
581
- log_dir = os.path.join(home_dir, "logs")
582
- os.makedirs(log_dir, exist_ok=True)
583
- futu_log_file = os.path.join(log_dir, "futu_connection.log")
584
-
585
- # Save current stderr (should be devnull)
586
- original_stderr = sys.stderr
587
-
588
- # Redirect stderr to futu log file temporarily
589
- futu_log_fd = open(futu_log_file, 'a')
590
- sys.stderr = futu_log_fd
591
- except Exception as e:
592
- logger.debug(f"Could not redirect stderr for futu connection: {e}")
585
+ # Initialize quote context
586
+ quote_ctx = OpenQuoteContext(host=host, port=port)
587
+
588
+ # Initialize trade context if needed
589
+ if os.getenv('FUTU_ENABLE_TRADING', '0') == '1':
590
+ # Get trading parameters
591
+ trade_env = os.getenv('FUTU_TRADE_ENV', 'SIMULATE')
592
+ security_firm = os.getenv('FUTU_SECURITY_FIRM', 'FUTUSECURITIES')
593
+ trd_market = os.getenv('FUTU_TRD_MARKET', 'HK')
594
+
595
+ # Map environment strings to Futu enums
596
+ trade_env_enum = {
597
+ 'REAL': TrdMarket.REAL,
598
+ 'SIMULATE': TrdMarket.SIMULATE
599
+ }.get(trade_env, TrdMarket.SIMULATE)
600
+
601
+ security_firm_enum = {
602
+ 'FUTUSECURITIES': SecurityFirm.FUTUSECURITIES,
603
+ 'FUTUINC': SecurityFirm.FUTUINC
604
+ }.get(security_firm, SecurityFirm.FUTUSECURITIES)
605
+
606
+ trd_market_enum = {
607
+ 'HK': TrdMarket.HK,
608
+ 'US': TrdMarket.US,
609
+ 'CN': TrdMarket.CN,
610
+ 'HKCC': TrdMarket.HKCC,
611
+ 'AU': TrdMarket.AU
612
+ }.get(trd_market, TrdMarket.HK)
613
+
614
+ # Initialize trade context
615
+ trade_ctx = OpenSecTradeContext(
616
+ host=host,
617
+ port=port,
618
+ security_firm=security_firm_enum
619
+ )
620
+ _is_trade_initialized = True
621
+ logger.info("Trade context initialized successfully")
593
622
 
594
- try:
595
- # Initialize quote context
596
- quote_ctx = OpenQuoteContext(host=host, port=port)
597
-
598
- # Initialize trade context if needed
599
- if os.getenv('FUTU_ENABLE_TRADING', '0') == '1':
600
- # Get trading parameters
601
- trade_env = os.getenv('FUTU_TRADE_ENV', 'SIMULATE')
602
- security_firm = os.getenv('FUTU_SECURITY_FIRM', 'FUTUSECURITIES')
603
- trd_market = os.getenv('FUTU_TRD_MARKET', 'HK')
604
-
605
- # Map environment strings to Futu enums
606
- trade_env_enum = {
607
- 'REAL': TrdMarket.REAL,
608
- 'SIMULATE': TrdMarket.SIMULATE
609
- }.get(trade_env, TrdMarket.SIMULATE)
610
-
611
- security_firm_enum = {
612
- 'FUTUSECURITIES': SecurityFirm.FUTUSECURITIES,
613
- 'FUTUINC': SecurityFirm.FUTUINC
614
- }.get(security_firm, SecurityFirm.FUTUSECURITIES)
615
-
616
- trd_market_enum = {
617
- 'HK': TrdMarket.HK,
618
- 'US': TrdMarket.US,
619
- 'CN': TrdMarket.CN,
620
- 'HKCC': TrdMarket.HKCC,
621
- 'AU': TrdMarket.AU
622
- }.get(trd_market, TrdMarket.HK)
623
-
624
- # Initialize trade context
625
- trade_ctx = OpenSecTradeContext(
626
- host=host,
627
- port=port,
628
- security_firm=security_firm_enum
629
- )
630
- _is_trade_initialized = True
631
- logger.info("Trade context initialized successfully")
632
-
633
- logger.info("Futu connection initialized successfully")
634
- return True
635
-
636
- finally:
637
- # Restore stderr redirection
638
- if futu_log_fd:
639
- try:
640
- futu_log_fd.close()
641
- except:
642
- pass
643
- # Restore original stderr (should be devnull)
644
- if original_stderr:
645
- sys.stderr = original_stderr
623
+ logger.info("Futu connection initialized successfully")
624
+ return True
646
625
 
647
626
  except Exception as e:
648
627
  error_msg = f"Failed to initialize Futu connection: {str(e)}"
649
628
  logger.error(error_msg)
650
- # Make sure we restore stderr even in case of errors
651
- if _stderr_redirected and _stderr_backup:
652
- sys.stderr = _stderr_backup
653
629
  return False
654
630
 
655
631
  @asynccontextmanager
656
632
  async def lifespan(server: Server):
657
- # Startup - only initialize quote connection
658
- if not init_quote_connection():
659
- logger.error("Failed to initialize quote connection")
660
- raise Exception("Quote connection failed")
633
+ # Startup - connections are already initialized in main()
634
+ # No need to initialize here as it's done before mcp.run()
661
635
  try:
662
636
  yield
663
637
  finally:
@@ -1779,12 +1753,15 @@ def main():
1779
1753
  formatter_class=argparse.RawDescriptionHelpFormatter,
1780
1754
  epilog="""
1781
1755
  Examples:
1782
- futu-mcp-server # Start the MCP server
1783
- futu-mcp-server --help # Show this help message
1756
+ futu-mcp-server # Start the MCP server with default settings
1757
+ futu-mcp-server --host 192.168.1.100 --port 11111 # Connect to remote OpenD
1758
+ futu-mcp-server --help # Show this help message
1759
+
1760
+ Arguments:
1761
+ --host # Futu OpenD host (default: 127.0.0.1)
1762
+ --port # Futu OpenD port (default: 11111)
1784
1763
 
1785
1764
  Environment Variables:
1786
- FUTU_HOST # Futu OpenD host (default: 127.0.0.1)
1787
- FUTU_PORT # Futu OpenD port (default: 11111)
1788
1765
  FUTU_ENABLE_TRADING # Enable trading features (default: 0)
1789
1766
  FUTU_TRADE_ENV # Trading environment: SIMULATE or REAL (default: SIMULATE)
1790
1767
  FUTU_SECURITY_FIRM # Security firm: FUTUSECURITIES or FUTUINC (default: FUTUSECURITIES)
@@ -1793,6 +1770,19 @@ Environment Variables:
1793
1770
  """
1794
1771
  )
1795
1772
 
1773
+ parser.add_argument(
1774
+ '--host',
1775
+ default='127.0.0.1',
1776
+ help='Futu OpenD host address (default: 127.0.0.1)'
1777
+ )
1778
+
1779
+ parser.add_argument(
1780
+ '--port',
1781
+ type=int,
1782
+ default=11111,
1783
+ help='Futu OpenD port number (default: 11111)'
1784
+ )
1785
+
1796
1786
  parser.add_argument(
1797
1787
  '--version',
1798
1788
  action='version',
@@ -1831,13 +1821,15 @@ Environment Variables:
1831
1821
 
1832
1822
  # Initialize Futu connection with file logging only
1833
1823
  logger.info("Initializing Futu connection for MCP server...")
1834
- if init_futu_connection():
1824
+ if init_futu_connection(args.host, args.port):
1835
1825
  logger.info("Successfully initialized Futu connection")
1836
1826
  logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
1837
1827
 
1838
1828
  try:
1839
1829
  # Run MCP server - stdout will be used for JSON communication only
1840
- mcp.run(transport='stdio')
1830
+ logger.info("About to call mcp.run() without transport parameter")
1831
+ mcp.run()
1832
+ logger.info("mcp.run() completed successfully")
1841
1833
  except KeyboardInterrupt:
1842
1834
  logger.info("Received keyboard interrupt, shutting down gracefully...")
1843
1835
  cleanup_all()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futu-stock-mcp-server
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A Model Context Protocol (MCP) server for accessing Futu OpenAPI functionality
5
5
  Project-URL: Homepage, https://github.com/shuizhengqi1/futu-stock-mcp-server
6
6
  Project-URL: Documentation, https://github.com/shuizhengqi1/futu-stock-mcp-server#readme
@@ -0,0 +1,7 @@
1
+ futu_stock_mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ futu_stock_mcp_server/server.py,sha256=p3wC7FE_QC2jEYgmYOSU6X4MA3Vg08rzoWAablVIdac,65854
3
+ futu_stock_mcp_server-0.1.6.dist-info/METADATA,sha256=zwCGxXZRQscuo1xa5kDMUr23gHc50oQW0CLOS3O6kQE,21041
4
+ futu_stock_mcp_server-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ futu_stock_mcp_server-0.1.6.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
6
+ futu_stock_mcp_server-0.1.6.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
7
+ futu_stock_mcp_server-0.1.6.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- futu_stock_mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- futu_stock_mcp_server/server.py,sha256=SokGZ3seYN1KwNozJqpbAmRsjcoYVSJ8v_wvMHu3pcs,66611
3
- futu_stock_mcp_server-0.1.4.dist-info/METADATA,sha256=V8KxUH0ehGGfS7FtOGMH6t-kk1NYkKHhFLi3Vg84FO4,21041
4
- futu_stock_mcp_server-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- futu_stock_mcp_server-0.1.4.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
6
- futu_stock_mcp_server-0.1.4.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
7
- futu_stock_mcp_server-0.1.4.dist-info/RECORD,,