futu-stock-mcp-server 0.1.2__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,16 +1,93 @@
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
- from mcp.server.fastmcp import FastMCP
87
+ from mcp.server.fastmcp import FastMCP, Context
12
88
  from mcp.types import TextContent, PromptMessage
13
89
  from mcp.server import Server
90
+ from mcp.server.session import ServerSession
14
91
  import atexit
15
92
  import signal
16
93
  import fcntl
@@ -18,45 +95,163 @@ import psutil
18
95
  import time
19
96
  from datetime import datetime
20
97
 
21
- # Get the project root directory and add it to Python path
22
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23
- sys.path.insert(0, project_root)
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)
24
102
 
25
- # Load environment variables from project root
26
- env_path = os.path.join(project_root, '.env')
27
- load_dotenv(env_path)
103
+ # CRITICAL: Configure logging to be MCP-compatible
104
+ # According to MCP best practices, we should:
105
+ # 1. Never write to stdout (reserved for MCP JSON communication)
106
+ # 2. Use file logging for debugging
107
+ # 3. Use MCP Context for operational logging when available
108
+ # 4. Suppress third-party library logs that might pollute output
28
109
 
29
- # Configure logging
30
- logger.remove() # Remove default handler
110
+ # Completely silence warnings and third-party logs
111
+ warnings.filterwarnings("ignore")
31
112
 
32
- # Get the project root directory
33
- log_dir = os.path.join(project_root, "logs")
34
- os.makedirs(log_dir, exist_ok=True)
113
+ # Configure loguru for file-only logging
114
+ logger.remove() # Remove all default handlers
35
115
 
36
- # Add file handler
37
- logger.add(
38
- os.path.join(log_dir, "futu_server.log"),
39
- rotation="500 MB",
40
- retention="10 days",
41
- level="DEBUG",
42
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
43
- )
44
-
45
- # Add console handler - output to stderr to avoid polluting MCP JSON communication
46
- logger.add(
47
- sys.stderr,
48
- level="INFO",
49
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
50
- colorize=False # Disable colors to avoid ANSI escape sequences
51
- )
52
-
53
- # Suppress other library logs that might interfere with MCP communication
54
- import logging
55
- logging.getLogger().setLevel(logging.WARNING) # Suppress INFO logs from other libraries
56
- logging.getLogger("mcp").setLevel(logging.WARNING) # Suppress MCP internal logs
57
- logging.getLogger("futu").setLevel(logging.WARNING) # Suppress Futu API logs
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
+ )
143
+
144
+ # Only add stderr logging if explicitly in debug mode and not in MCP mode
145
+ if os.getenv('FUTU_DEBUG_MODE') == '1' and not os.getenv('MCP_MODE') == '1':
146
+ logger.add(
147
+ sys.stderr,
148
+ level="INFO",
149
+ format="{time:HH:mm:ss} | {level} | {message}",
150
+ colorize=False,
151
+ filter=lambda record: record["level"].name in ["INFO", "WARNING", "ERROR", "CRITICAL"]
152
+ )
153
+
154
+ # Suppress all third-party library logging to prevent stdout pollution
155
+ logging.disable(logging.CRITICAL)
58
156
 
59
- logger.info(f"Starting server with log directory: {log_dir}")
157
+ # Set up null handlers for problematic loggers
158
+ class NullHandler(logging.Handler):
159
+ def emit(self, record):
160
+ pass
161
+
162
+ null_handler = NullHandler()
163
+ root_logger = logging.getLogger()
164
+ root_logger.addHandler(null_handler)
165
+ root_logger.setLevel(logging.CRITICAL + 1)
166
+
167
+ # Specifically silence known problematic loggers
168
+ for logger_name in [
169
+ 'mcp', 'fastmcp', 'futu', 'uvicorn', 'asyncio',
170
+ 'websockets', 'aiohttp', 'urllib3', 'requests'
171
+ ]:
172
+ lib_logger = logging.getLogger(logger_name)
173
+ lib_logger.disabled = True
174
+ lib_logger.addHandler(null_handler)
175
+ lib_logger.setLevel(logging.CRITICAL + 1)
176
+ lib_logger.propagate = False
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
+
217
+ # MCP-compatible logging helper functions
218
+ async def log_to_mcp(ctx: Context, level: str, message: str):
219
+ """Send log message through MCP Context when available"""
220
+ try:
221
+ if level.upper() == "DEBUG":
222
+ await ctx.debug(message)
223
+ elif level.upper() == "INFO":
224
+ await ctx.info(message)
225
+ elif level.upper() == "WARNING":
226
+ await ctx.warning(message)
227
+ elif level.upper() == "ERROR":
228
+ await ctx.error(message)
229
+ else:
230
+ await ctx.info(f"[{level}] {message}")
231
+ except Exception:
232
+ # Fallback to file logging if MCP context fails
233
+ logger.log(level.upper(), message)
234
+
235
+ def safe_log(level: str, message: str, ctx: Context = None):
236
+ """Safe logging that uses MCP context when available, file logging otherwise"""
237
+ # Always log to file
238
+ logger.log(level.upper(), message)
239
+
240
+ # Also send to MCP if context is available
241
+ if ctx and os.getenv('MCP_MODE') == '1' and not _stderr_redirected:
242
+ try:
243
+ import asyncio
244
+ loop = asyncio.get_event_loop()
245
+ if loop.is_running():
246
+ asyncio.create_task(log_to_mcp(ctx, level, message))
247
+ except Exception:
248
+ pass # Ignore MCP logging errors
249
+
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__))))
60
255
 
61
256
  # PID file path
62
257
  PID_FILE = os.path.join(project_root, '.futu_mcp.pid')
@@ -359,9 +554,103 @@ def init_trade_connection():
359
554
  _is_trade_initialized = False
360
555
  return False
361
556
 
362
- def init_futu_connection():
363
- """Initialize both quote and trade connections"""
364
- 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
365
654
 
366
655
  @asynccontextmanager
367
656
  async def lifespan(server: Server):
@@ -411,7 +700,7 @@ def handle_return_data(ret: int, data: Any) -> Dict[str, Any]:
411
700
 
412
701
  # Market Data Tools
413
702
  @mcp.tool()
414
- async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
703
+ async def get_stock_quote(symbols: List[str], ctx: Context[ServerSession, None] = None) -> Dict[str, Any]:
415
704
  """Get stock quote data for given symbols
416
705
 
417
706
  Args:
@@ -455,21 +744,32 @@ async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
455
744
  - Consider actual needs when selecting stocks
456
745
  - Handle exceptions properly
457
746
  """
458
- ret, data = quote_ctx.get_stock_quote(symbols)
459
- if ret != RET_OK:
460
- return {'error': str(data)}
747
+ safe_log("info", f"Getting stock quotes for symbols: {symbols}", ctx)
461
748
 
462
- # Convert DataFrame to dict if necessary
463
- if hasattr(data, 'to_dict'):
464
- result = {
465
- 'quote_list': data.to_dict('records')
466
- }
467
- else:
468
- result = {
469
- 'quote_list': data
470
- }
749
+ try:
750
+ ret, data = quote_ctx.get_stock_quote(symbols)
751
+ if ret != RET_OK:
752
+ error_msg = f"Failed to get stock quote: {str(data)}"
753
+ safe_log("error", error_msg, ctx)
754
+ return {'error': error_msg}
471
755
 
472
- return result
756
+ # Convert DataFrame to dict if necessary
757
+ if hasattr(data, 'to_dict'):
758
+ result = {
759
+ 'quote_list': data.to_dict('records')
760
+ }
761
+ else:
762
+ result = {
763
+ 'quote_list': data
764
+ }
765
+
766
+ safe_log("info", f"Successfully retrieved quotes for {len(symbols)} symbols", ctx)
767
+ return result
768
+
769
+ except Exception as e:
770
+ error_msg = f"Exception in get_stock_quote: {str(e)}"
771
+ safe_log("error", error_msg, ctx)
772
+ return {'error': error_msg}
473
773
 
474
774
  @mcp.tool()
475
775
  async def get_market_snapshot(symbols: List[str]) -> Dict[str, Any]:
@@ -1126,12 +1426,29 @@ async def get_option_butterfly(symbol: str, expiry: str, strike_price: float) ->
1126
1426
 
1127
1427
  # Account Query Tools
1128
1428
  @mcp.tool()
1129
- async def get_account_list() -> Dict[str, Any]:
1429
+ async def get_account_list(ctx: Context[ServerSession, None] = None) -> Dict[str, Any]:
1130
1430
  """Get account list"""
1431
+ safe_log("info", "Attempting to get account list", ctx)
1432
+
1131
1433
  if not init_trade_connection():
1132
- return {'error': 'Failed to initialize trade connection'}
1133
- ret, data = trade_ctx.get_acc_list()
1134
- return handle_return_data(ret, data)
1434
+ error_msg = 'Failed to initialize trade connection'
1435
+ safe_log("error", error_msg, ctx)
1436
+ return {'error': error_msg}
1437
+
1438
+ try:
1439
+ ret, data = trade_ctx.get_acc_list()
1440
+ result = handle_return_data(ret, data)
1441
+
1442
+ if 'error' not in result:
1443
+ safe_log("info", "Successfully retrieved account list", ctx)
1444
+ else:
1445
+ safe_log("error", f"Failed to get account list: {result['error']}", ctx)
1446
+
1447
+ return result
1448
+ except Exception as e:
1449
+ error_msg = f"Exception in get_account_list: {str(e)}"
1450
+ safe_log("error", error_msg, ctx)
1451
+ return {'error': error_msg}
1135
1452
 
1136
1453
  @mcp.tool()
1137
1454
  async def get_funds() -> Dict[str, Any]:
@@ -1456,47 +1773,91 @@ async def get_current_time() -> Dict[str, Any]:
1456
1773
 
1457
1774
  def main():
1458
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
+
1459
1804
  try:
1460
- # Ensure no color output in MCP mode
1805
+ # CRITICAL: Set MCP mode BEFORE any logging to ensure clean stdout
1806
+ os.environ['MCP_MODE'] = '1'
1807
+
1808
+ # Ensure no color output or ANSI escape sequences in MCP mode
1461
1809
  os.environ['NO_COLOR'] = '1'
1462
1810
  os.environ['TERM'] = 'dumb'
1811
+ os.environ['FORCE_COLOR'] = '0'
1812
+ os.environ['COLORTERM'] = ''
1813
+ os.environ['ANSI_COLORS_DISABLED'] = '1'
1814
+ os.environ['PYTHONUNBUFFERED'] = '1'
1815
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
1463
1816
 
1464
- # 清理旧的进程和文件
1817
+ # Disable Python buffering to ensure clean MCP JSON communication
1818
+
1819
+ # Clean up stale processes and acquire lock
1465
1820
  cleanup_stale_processes()
1466
-
1467
- # 获取锁
1821
+
1468
1822
  lock_fd = acquire_lock()
1469
1823
  if lock_fd is None:
1824
+ # Use file logging only - no stderr output in MCP mode
1470
1825
  logger.error("Failed to acquire lock. Another instance may be running.")
1471
1826
  sys.exit(1)
1472
1827
 
1473
- # 设置信号处理
1828
+ # Set up signal handlers
1474
1829
  signal.signal(signal.SIGINT, signal_handler)
1475
1830
  signal.signal(signal.SIGTERM, signal_handler)
1476
1831
 
1477
- logger.info("Initializing Futu connection...")
1832
+ # Initialize Futu connection with file logging only
1833
+ logger.info("Initializing Futu connection for MCP server...")
1478
1834
  if init_futu_connection():
1479
1835
  logger.info("Successfully initialized Futu connection")
1480
- logger.info("Starting MCP server in stdio mode...")
1481
- logger.info("Press Ctrl+C to stop the server")
1836
+ logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
1837
+
1482
1838
  try:
1839
+ # Run MCP server - stdout will be used for JSON communication only
1483
1840
  mcp.run(transport='stdio')
1484
1841
  except KeyboardInterrupt:
1485
- logger.info("Received keyboard interrupt, shutting down...")
1842
+ logger.info("Received keyboard interrupt, shutting down gracefully...")
1486
1843
  cleanup_all()
1487
1844
  os._exit(0)
1488
1845
  except Exception as e:
1489
- logger.error(f"Error running server: {str(e)}")
1846
+ logger.error(f"Error running MCP server: {str(e)}")
1490
1847
  cleanup_all()
1491
1848
  os._exit(1)
1492
1849
  else:
1493
- logger.error("Failed to initialize Futu connection. Server will not start.")
1850
+ logger.error("Failed to initialize Futu connection. MCP server will not start.")
1494
1851
  os._exit(1)
1852
+
1495
1853
  except Exception as e:
1496
- logger.error(f"Error starting server: {str(e)}")
1854
+ # In MCP mode, we should avoid printing to stdout
1855
+ # Log to file only
1856
+ logger.error(f"Error starting MCP server: {str(e)}")
1497
1857
  sys.exit(1)
1498
1858
  finally:
1499
1859
  cleanup_all()
1500
1860
 
1501
1861
  if __name__ == "__main__":
1502
1862
  main()
1863
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: futu-stock-mcp-server
3
- Version: 0.1.2
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
 
@@ -399,13 +398,29 @@ python -m futu_stock_mcp_server.server
399
398
  ```
400
399
 
401
400
  ### 日志调试
402
- 启用详细日志:
401
+
402
+ 本项目已根据 [MCP 官方文档](https://github.com/modelcontextprotocol/python-sdk) 的最佳实践配置了日志系统:
403
+
404
+ #### MCP 兼容的日志配置
405
+ - **文件日志**: 所有日志写入 `logs/futu_server.log`,自动轮转和清理
406
+ - **MCP Context 日志**: 工具执行期间通过 MCP Context 发送日志给客户端
407
+ - **stdout 保护**: 确保 stdout 仅用于 MCP JSON 通信,避免污染
408
+
409
+ #### 调试模式(仅开发时使用)
403
410
  ```bash
404
- export LOG_LEVEL=DEBUG
411
+ # 启用调试模式(会向 stderr 输出日志)
412
+ export FUTU_DEBUG_MODE=1
405
413
  futu-mcp-server
406
414
  ```
407
415
 
408
- 日志文件位置:`./logs/futu_server.log`
416
+ **注意**: 在 MCP 客户端中不要启用调试模式,因为它会向 stderr 输出日志。
417
+
418
+ #### 日志文件位置
419
+ - 主日志文件:`./logs/futu_server.log`
420
+ - 自动轮转:500 MB 后轮转
421
+ - 自动清理:保留 10 天
422
+
423
+ 详细的日志配置说明请参考 [docs/LOGGING.md](docs/LOGGING.md)。
409
424
  tools = await session.list_tools()
410
425
 
411
426
  # Call a tool
@@ -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=xFtYbURXhj0DOkbCFgwHDKHCAATWlSxdBZudrWsodVc,52810
3
- futu_stock_mcp_server-0.1.2.dist-info/METADATA,sha256=CmSaX6UUSExUdbci62zSZER2udDlV6Fqfqd3SSYHSlg,20453
4
- futu_stock_mcp_server-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- futu_stock_mcp_server-0.1.2.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
6
- futu_stock_mcp_server-0.1.2.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
7
- futu_stock_mcp_server-0.1.2.dist-info/RECORD,,