futu-stock-mcp-server 0.1.3__py3-none-any.whl → 0.1.5__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
-
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
97
 
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,66 @@ 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
+ # Initialize quote context
573
+ quote_ctx = OpenQuoteContext(host=host, port=port)
574
+
575
+ # Initialize trade context if needed
576
+ if os.getenv('FUTU_ENABLE_TRADING', '0') == '1':
577
+ # Get trading parameters
578
+ trade_env = os.getenv('FUTU_TRADE_ENV', 'SIMULATE')
579
+ security_firm = os.getenv('FUTU_SECURITY_FIRM', 'FUTUSECURITIES')
580
+ trd_market = os.getenv('FUTU_TRD_MARKET', 'HK')
581
+
582
+ # Map environment strings to Futu enums
583
+ trade_env_enum = {
584
+ 'REAL': TrdMarket.REAL,
585
+ 'SIMULATE': TrdMarket.SIMULATE
586
+ }.get(trade_env, TrdMarket.SIMULATE)
587
+
588
+ security_firm_enum = {
589
+ 'FUTUSECURITIES': SecurityFirm.FUTUSECURITIES,
590
+ 'FUTUINC': SecurityFirm.FUTUINC
591
+ }.get(security_firm, SecurityFirm.FUTUSECURITIES)
592
+
593
+ trd_market_enum = {
594
+ 'HK': TrdMarket.HK,
595
+ 'US': TrdMarket.US,
596
+ 'CN': TrdMarket.CN,
597
+ 'HKCC': TrdMarket.HKCC,
598
+ 'AU': TrdMarket.AU
599
+ }.get(trd_market, TrdMarket.HK)
600
+
601
+ # Initialize trade context
602
+ trade_ctx = OpenSecTradeContext(
603
+ host=host,
604
+ port=port,
605
+ security_firm=security_firm_enum
606
+ )
607
+ _is_trade_initialized = True
608
+ logger.info("Trade context initialized successfully")
609
+
610
+ logger.info("Futu connection initialized successfully")
611
+ return True
612
+
613
+ except Exception as e:
614
+ error_msg = f"Failed to initialize Futu connection: {str(e)}"
615
+ logger.error(error_msg)
616
+ return False
435
617
 
436
618
  @asynccontextmanager
437
619
  async def lifespan(server: Server):
@@ -1554,6 +1736,34 @@ async def get_current_time() -> Dict[str, Any]:
1554
1736
 
1555
1737
  def main():
1556
1738
  """Main entry point for the futu-mcp-server command."""
1739
+ # Parse command line arguments first
1740
+ parser = argparse.ArgumentParser(
1741
+ description="Futu Stock MCP Server - A Model Context Protocol server for accessing Futu OpenAPI functionality",
1742
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1743
+ epilog="""
1744
+ Examples:
1745
+ futu-mcp-server # Start the MCP server
1746
+ futu-mcp-server --help # Show this help message
1747
+
1748
+ Environment Variables:
1749
+ FUTU_HOST # Futu OpenD host (default: 127.0.0.1)
1750
+ FUTU_PORT # Futu OpenD port (default: 11111)
1751
+ FUTU_ENABLE_TRADING # Enable trading features (default: 0)
1752
+ FUTU_TRADE_ENV # Trading environment: SIMULATE or REAL (default: SIMULATE)
1753
+ FUTU_SECURITY_FIRM # Security firm: FUTUSECURITIES or FUTUINC (default: FUTUSECURITIES)
1754
+ FUTU_TRD_MARKET # Trading market: HK or US (default: HK)
1755
+ FUTU_DEBUG_MODE # Enable debug logging (default: 0)
1756
+ """
1757
+ )
1758
+
1759
+ parser.add_argument(
1760
+ '--version',
1761
+ action='version',
1762
+ version='futu-stock-mcp-server 0.1.3'
1763
+ )
1764
+
1765
+ args = parser.parse_args()
1766
+
1557
1767
  try:
1558
1768
  # CRITICAL: Set MCP mode BEFORE any logging to ensure clean stdout
1559
1769
  os.environ['MCP_MODE'] = '1'
@@ -1564,19 +1774,14 @@ def main():
1564
1774
  os.environ['FORCE_COLOR'] = '0'
1565
1775
  os.environ['COLORTERM'] = ''
1566
1776
  os.environ['ANSI_COLORS_DISABLED'] = '1'
1567
-
1568
- # Disable Python buffering to ensure clean MCP JSON communication
1569
1777
  os.environ['PYTHONUNBUFFERED'] = '1'
1570
1778
  os.environ['PYTHONIOENCODING'] = 'utf-8'
1571
1779
 
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
1780
+ # Disable Python buffering to ensure clean MCP JSON communication
1576
1781
 
1577
1782
  # Clean up stale processes and acquire lock
1578
1783
  cleanup_stale_processes()
1579
-
1784
+
1580
1785
  lock_fd = acquire_lock()
1581
1786
  if lock_fd is None:
1582
1787
  # Use file logging only - no stderr output in MCP mode
@@ -1609,6 +1814,8 @@ def main():
1609
1814
  os._exit(1)
1610
1815
 
1611
1816
  except Exception as e:
1817
+ # In MCP mode, we should avoid printing to stdout
1818
+ # Log to file only
1612
1819
  logger.error(f"Error starting MCP server: {str(e)}")
1613
1820
  sys.exit(1)
1614
1821
  finally:
@@ -1616,3 +1823,4 @@ def main():
1616
1823
 
1617
1824
  if __name__ == "__main__":
1618
1825
  main()
1826
+
@@ -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.5
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=oMD3yHyBmECzqxQa7LU-QuPSDLkLo0RsluXACNeLKM0,65035
3
+ futu_stock_mcp_server-0.1.5.dist-info/METADATA,sha256=04hyGftEjBkbcpdA-zSJ3YoZmzUKGf6HtTSQJ76IMww,21041
4
+ futu_stock_mcp_server-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ futu_stock_mcp_server-0.1.5.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
6
+ futu_stock_mcp_server-0.1.5.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
7
+ futu_stock_mcp_server-0.1.5.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,,