futu-stock-mcp-server 0.1.5__tar.gz → 0.1.6__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futu-stock-mcp-server
3
- Version: 0.1.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "futu-stock-mcp-server"
3
- version = "0.1.5"
3
+ version = "0.1.6"
4
4
  description = "A Model Context Protocol (MCP) server for accessing Futu OpenAPI functionality"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -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,17 +562,22 @@ 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}")
@@ -617,10 +630,8 @@ def init_futu_connection() -> bool:
617
630
 
618
631
  @asynccontextmanager
619
632
  async def lifespan(server: Server):
620
- # Startup - only initialize quote connection
621
- if not init_quote_connection():
622
- logger.error("Failed to initialize quote connection")
623
- 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()
624
635
  try:
625
636
  yield
626
637
  finally:
@@ -1742,12 +1753,15 @@ def main():
1742
1753
  formatter_class=argparse.RawDescriptionHelpFormatter,
1743
1754
  epilog="""
1744
1755
  Examples:
1745
- futu-mcp-server # Start the MCP server
1746
- 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)
1747
1763
 
1748
1764
  Environment Variables:
1749
- FUTU_HOST # Futu OpenD host (default: 127.0.0.1)
1750
- FUTU_PORT # Futu OpenD port (default: 11111)
1751
1765
  FUTU_ENABLE_TRADING # Enable trading features (default: 0)
1752
1766
  FUTU_TRADE_ENV # Trading environment: SIMULATE or REAL (default: SIMULATE)
1753
1767
  FUTU_SECURITY_FIRM # Security firm: FUTUSECURITIES or FUTUINC (default: FUTUSECURITIES)
@@ -1756,6 +1770,19 @@ Environment Variables:
1756
1770
  """
1757
1771
  )
1758
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
+
1759
1786
  parser.add_argument(
1760
1787
  '--version',
1761
1788
  action='version',
@@ -1794,13 +1821,15 @@ Environment Variables:
1794
1821
 
1795
1822
  # Initialize Futu connection with file logging only
1796
1823
  logger.info("Initializing Futu connection for MCP server...")
1797
- if init_futu_connection():
1824
+ if init_futu_connection(args.host, args.port):
1798
1825
  logger.info("Successfully initialized Futu connection")
1799
1826
  logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
1800
1827
 
1801
1828
  try:
1802
1829
  # Run MCP server - stdout will be used for JSON communication only
1803
- 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")
1804
1833
  except KeyboardInterrupt:
1805
1834
  logger.info("Received keyboard interrupt, shutting down gracefully...")
1806
1835
  cleanup_all()