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

@@ -1,12 +1,88 @@
1
+ import os
2
+ import sys
3
+ import warnings
4
+ import logging
5
+ import argparse
6
+
7
+ # CRITICAL: Check if this is a help command before setting MCP mode
8
+ _is_help_command = any(arg in ['--help', '-h', '--version', '-v'] for arg in sys.argv)
9
+
10
+ # CRITICAL: Set MCP mode BEFORE any logging to ensure clean stdout
11
+ # But not if this is a help command - in that case, we want normal stdout
12
+ if not _is_help_command:
13
+ os.environ['MCP_MODE'] = os.environ.get('MCP_MODE', '1')
14
+
15
+ # CRITICAL: Completely disable all potential stdout pollution sources
16
+ # This must be done BEFORE any other imports or operations
17
+
18
+ # 1. Disable all warnings that might go to stdout/stderr
19
+ warnings.filterwarnings("ignore")
20
+ warnings.simplefilter("ignore")
21
+
22
+ # 2. Completely disable the standard logging system
23
+ logging.disable(logging.CRITICAL)
24
+
25
+ # 3. Set environment variables to prevent ANSI escape sequences
26
+ os.environ['NO_COLOR'] = '1'
27
+ os.environ['TERM'] = 'dumb'
28
+ os.environ['FORCE_COLOR'] = '0'
29
+ os.environ['COLORTERM'] = ''
30
+ os.environ['ANSI_COLORS_DISABLED'] = '1'
31
+ os.environ['PYTHONUNBUFFERED'] = '1'
32
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
33
+
34
+ # 4. Create a custom stdout wrapper to catch any accidental writes
35
+ class StdoutProtector:
36
+ """Protects stdout from any non-MCP content"""
37
+ def __init__(self, original_stdout):
38
+ self.original = original_stdout
39
+ self.buffer = ""
40
+
41
+ def write(self, text):
42
+ # Only allow JSON-like content or empty strings
43
+ if not text or text.isspace():
44
+ self.original.write(text)
45
+ elif text.strip().startswith(('{', '[', '"')) or text.strip() == '':
46
+ self.original.write(text)
47
+ else:
48
+ # Silently drop non-JSON content
49
+ pass
50
+
51
+ def flush(self):
52
+ self.original.flush()
53
+
54
+ def __getattr__(self, name):
55
+ return getattr(self.original, name)
56
+
57
+ # Apply stdout protection in MCP mode VERY EARLY, before any imports
58
+ if os.getenv('MCP_MODE') == '1':
59
+ sys.stdout = StdoutProtector(sys.stdout)
60
+
61
+ # 5. Redirect stderr to null in MCP mode to prevent any accidental output
62
+ _stderr_redirected = False
63
+ _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
+
72
+ # Now we can safely import other modules
1
73
  from contextlib import asynccontextmanager
2
74
  from collections.abc import AsyncIterator
3
75
  from typing import Dict, Any, List, Optional
4
- from futu import OpenQuoteContext, OpenSecTradeContext, TrdMarket, SecurityFirm, RET_OK
76
+ try:
77
+ from futu import OpenQuoteContext, OpenSecTradeContext, TrdMarket, SecurityFirm, RET_OK
78
+ except ImportError as e:
79
+ # In MCP mode, we should avoid printing to stdout/stderr
80
+ # Log to file only
81
+ logger.error(f"Failed to import futu: {e}")
82
+ sys.exit(1)
5
83
  import json
6
84
  import asyncio
7
85
  from loguru import logger
8
- import os
9
- import sys
10
86
  from dotenv import load_dotenv
11
87
  from mcp.server.fastmcp import FastMCP, Context
12
88
  from mcp.types import TextContent, PromptMessage
@@ -18,16 +94,11 @@ import fcntl
18
94
  import psutil
19
95
  import time
20
96
  from datetime import datetime
21
- import logging
22
- import warnings
23
97
 
24
- # Get the project root directory and add it to Python path
25
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
26
- sys.path.insert(0, project_root)
27
-
28
- # Load environment variables from project root
29
- env_path = os.path.join(project_root, '.env')
30
- load_dotenv(env_path)
98
+ # Get the user home directory and create logs directory there
99
+ home_dir = os.path.expanduser("~")
100
+ log_dir = os.path.join(home_dir, "logs")
101
+ os.makedirs(log_dir, exist_ok=True)
31
102
 
32
103
  # CRITICAL: Configure logging to be MCP-compatible
33
104
  # According to MCP best practices, we should:
@@ -42,24 +113,36 @@ warnings.filterwarnings("ignore")
42
113
  # Configure loguru for file-only logging
43
114
  logger.remove() # Remove all default handlers
44
115
 
45
- # Get the project root directory
46
- log_dir = os.path.join(project_root, "logs")
47
- os.makedirs(log_dir, exist_ok=True)
48
-
49
- # Add file handler only - NO console output to avoid MCP communication interference
50
- logger.add(
51
- os.path.join(log_dir, "futu_server.log"),
52
- rotation="500 MB",
53
- retention="10 days",
54
- level="DEBUG",
55
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
56
- enqueue=True, # Thread-safe logging
57
- backtrace=True,
58
- diagnose=True
59
- )
116
+ # CRITICAL: In MCP mode, ensure NO stderr output at all
117
+ if os.getenv('MCP_MODE') == '1':
118
+ # Remove any remaining handlers that might output to stderr
119
+ logger.remove()
120
+ # Add only file handler - NO console output
121
+ logger.add(
122
+ os.path.join(log_dir, "futu_mcp_server.log"),
123
+ rotation="500 MB",
124
+ retention="10 days",
125
+ level="DEBUG",
126
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
127
+ enqueue=True, # Thread-safe logging
128
+ backtrace=True,
129
+ diagnose=True
130
+ )
131
+ else:
132
+ # Non-MCP mode: add file handler
133
+ logger.add(
134
+ os.path.join(log_dir, "futu_mcp_server.log"),
135
+ rotation="500 MB",
136
+ retention="10 days",
137
+ level="DEBUG",
138
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
139
+ enqueue=True, # Thread-safe logging
140
+ backtrace=True,
141
+ diagnose=True
142
+ )
60
143
 
61
144
  # Only add stderr logging if explicitly in debug mode and not in MCP mode
62
- if os.getenv('FUTU_DEBUG_MODE') == '1' and not os.getenv('MCP_MODE'):
145
+ if os.getenv('FUTU_DEBUG_MODE') == '1' and not os.getenv('MCP_MODE') == '1':
63
146
  logger.add(
64
147
  sys.stderr,
65
148
  level="INFO",
@@ -92,6 +175,45 @@ for logger_name in [
92
175
  lib_logger.setLevel(logging.CRITICAL + 1)
93
176
  lib_logger.propagate = False
94
177
 
178
+ # Even more aggressive suppression for futu library
179
+ # This is critical because futu library may output logs during connection
180
+ try:
181
+ # Suppress futu library logging completely
182
+ futu_logger = logging.getLogger('futu')
183
+ futu_logger.disabled = True
184
+ futu_logger.setLevel(logging.CRITICAL + 1)
185
+ futu_logger.propagate = False
186
+
187
+ # Suppress specific futu sub-modules that are known to output logs
188
+ for sub_logger_name in [
189
+ 'futu', 'futu.common', 'futu.quote', 'futu.trade',
190
+ 'futu.common.constant', 'futu.common.sys_utils',
191
+ 'futu.quote.open_quote_context', 'futu.quote.quote_response_handler',
192
+ 'futu.trade.open_trade_context', 'futu.trade.trade_response_handler',
193
+ 'futu.common.open_context_base', 'futu.common.network_manager'
194
+ ]:
195
+ sub_logger = logging.getLogger(sub_logger_name)
196
+ sub_logger.disabled = True
197
+ sub_logger.setLevel(logging.CRITICAL + 1)
198
+ sub_logger.propagate = False
199
+
200
+ # Also redirect any direct print statements from futu to a file
201
+ if os.getenv('MCP_MODE') == '1':
202
+ # Create a special log file for futu connection logs
203
+ home_dir = os.path.expanduser("~")
204
+ log_dir = os.path.join(home_dir, "logs")
205
+ os.makedirs(log_dir, exist_ok=True)
206
+
207
+ futu_conn_log_file = os.path.join(log_dir, "futu_connection.log")
208
+ futu_conn_log = open(futu_conn_log_file, 'a')
209
+
210
+ # This is a last resort to catch any print statements from futu
211
+ # We'll redirect stderr to this file temporarily during connection
212
+ # and then restore it to devnull
213
+ except Exception as e:
214
+ # If we can't set up additional logging, continue anyway
215
+ pass
216
+
95
217
  # MCP-compatible logging helper functions
96
218
  async def log_to_mcp(ctx: Context, level: str, message: str):
97
219
  """Send log message through MCP Context when available"""
@@ -116,7 +238,7 @@ def safe_log(level: str, message: str, ctx: Context = None):
116
238
  logger.log(level.upper(), message)
117
239
 
118
240
  # Also send to MCP if context is available
119
- if ctx:
241
+ if ctx and os.getenv('MCP_MODE') == '1' and not _stderr_redirected:
120
242
  try:
121
243
  import asyncio
122
244
  loop = asyncio.get_event_loop()
@@ -125,8 +247,11 @@ def safe_log(level: str, message: str, ctx: Context = None):
125
247
  except Exception:
126
248
  pass # Ignore MCP logging errors
127
249
 
128
- logger.info(f"Starting Futu MCP Server with log directory: {log_dir}")
129
- logger.info("Logging configured for MCP compatibility - stdout reserved for JSON communication")
250
+ # Only log to file, never to stdout/stderr in MCP mode
251
+ # These logs will only be written to file, not to stdout/stderr
252
+
253
+ # Get project root directory
254
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
130
255
 
131
256
  # PID file path
132
257
  PID_FILE = os.path.join(project_root, '.futu_mcp.pid')
@@ -429,9 +554,103 @@ def init_trade_connection():
429
554
  _is_trade_initialized = False
430
555
  return False
431
556
 
432
- def init_futu_connection():
433
- """Initialize both quote and trade connections"""
434
- return init_quote_connection()
557
+ def init_futu_connection() -> bool:
558
+ """
559
+ Initialize connection to Futu OpenD.
560
+ Returns True if successful, False otherwise.
561
+ """
562
+ global quote_ctx, trade_ctx, _is_trade_initialized
563
+
564
+ 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'))
568
+
569
+ # Log to file only
570
+ logger.info(f"Initializing Futu connection to {host}:{port}")
571
+
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}")
593
+
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
646
+
647
+ except Exception as e:
648
+ error_msg = f"Failed to initialize Futu connection: {str(e)}"
649
+ 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
+ return False
435
654
 
436
655
  @asynccontextmanager
437
656
  async def lifespan(server: Server):
@@ -1554,6 +1773,34 @@ async def get_current_time() -> Dict[str, Any]:
1554
1773
 
1555
1774
  def main():
1556
1775
  """Main entry point for the futu-mcp-server command."""
1776
+ # Parse command line arguments first
1777
+ parser = argparse.ArgumentParser(
1778
+ description="Futu Stock MCP Server - A Model Context Protocol server for accessing Futu OpenAPI functionality",
1779
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1780
+ epilog="""
1781
+ Examples:
1782
+ futu-mcp-server # Start the MCP server
1783
+ futu-mcp-server --help # Show this help message
1784
+
1785
+ Environment Variables:
1786
+ FUTU_HOST # Futu OpenD host (default: 127.0.0.1)
1787
+ FUTU_PORT # Futu OpenD port (default: 11111)
1788
+ FUTU_ENABLE_TRADING # Enable trading features (default: 0)
1789
+ FUTU_TRADE_ENV # Trading environment: SIMULATE or REAL (default: SIMULATE)
1790
+ FUTU_SECURITY_FIRM # Security firm: FUTUSECURITIES or FUTUINC (default: FUTUSECURITIES)
1791
+ FUTU_TRD_MARKET # Trading market: HK or US (default: HK)
1792
+ FUTU_DEBUG_MODE # Enable debug logging (default: 0)
1793
+ """
1794
+ )
1795
+
1796
+ parser.add_argument(
1797
+ '--version',
1798
+ action='version',
1799
+ version='futu-stock-mcp-server 0.1.3'
1800
+ )
1801
+
1802
+ args = parser.parse_args()
1803
+
1557
1804
  try:
1558
1805
  # CRITICAL: Set MCP mode BEFORE any logging to ensure clean stdout
1559
1806
  os.environ['MCP_MODE'] = '1'
@@ -1564,19 +1811,14 @@ def main():
1564
1811
  os.environ['FORCE_COLOR'] = '0'
1565
1812
  os.environ['COLORTERM'] = ''
1566
1813
  os.environ['ANSI_COLORS_DISABLED'] = '1'
1567
-
1568
- # Disable Python buffering to ensure clean MCP JSON communication
1569
1814
  os.environ['PYTHONUNBUFFERED'] = '1'
1570
1815
  os.environ['PYTHONIOENCODING'] = 'utf-8'
1571
1816
 
1572
- # According to MCP best practices:
1573
- # - stdout is RESERVED for MCP JSON communication only
1574
- # - All logging should go to files
1575
- # - No stderr output in production mode to avoid pollution
1817
+ # Disable Python buffering to ensure clean MCP JSON communication
1576
1818
 
1577
1819
  # Clean up stale processes and acquire lock
1578
1820
  cleanup_stale_processes()
1579
-
1821
+
1580
1822
  lock_fd = acquire_lock()
1581
1823
  if lock_fd is None:
1582
1824
  # Use file logging only - no stderr output in MCP mode
@@ -1609,6 +1851,8 @@ def main():
1609
1851
  os._exit(1)
1610
1852
 
1611
1853
  except Exception as e:
1854
+ # In MCP mode, we should avoid printing to stdout
1855
+ # Log to file only
1612
1856
  logger.error(f"Error starting MCP server: {str(e)}")
1613
1857
  sys.exit(1)
1614
1858
  finally:
@@ -1616,3 +1860,4 @@ def main():
1616
1860
 
1617
1861
  if __name__ == "__main__":
1618
1862
  main()
1863
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futu-stock-mcp-server
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
@@ -40,7 +40,6 @@ Requires-Dist: pytest-asyncio; extra == 'dev'
40
40
  Requires-Dist: ruff; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
- [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/shuizhengqi1-futu-stocp-mcp-server-badge.png)](https://mseep.ai/app/shuizhengqi1-futu-stocp-mcp-server)
44
43
 
45
44
  # Futu Stock MCP Server
46
45
 
@@ -0,0 +1,7 @@
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,,
@@ -1,7 +0,0 @@
1
- futu_stock_mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- futu_stock_mcp_server/server.py,sha256=d03R4mc1eKNNX1JhYDnbj59NRN12O8XG_oc_T8mNVJU,57336
3
- futu_stock_mcp_server-0.1.3.dist-info/METADATA,sha256=PeSU1LZfoEXaThL6W-ZUhQMr7VR9JcWNQp6M6i56Or8,21205
4
- futu_stock_mcp_server-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- futu_stock_mcp_server-0.1.3.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
6
- futu_stock_mcp_server-0.1.3.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
7
- futu_stock_mcp_server-0.1.3.dist-info/RECORD,,