futu-stock-mcp-server 0.1.2__tar.gz → 0.1.3__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.2
3
+ Version: 0.1.3
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
@@ -399,13 +399,29 @@ python -m futu_stock_mcp_server.server
399
399
  ```
400
400
 
401
401
  ### 日志调试
402
- 启用详细日志:
402
+
403
+ 本项目已根据 [MCP 官方文档](https://github.com/modelcontextprotocol/python-sdk) 的最佳实践配置了日志系统:
404
+
405
+ #### MCP 兼容的日志配置
406
+ - **文件日志**: 所有日志写入 `logs/futu_server.log`,自动轮转和清理
407
+ - **MCP Context 日志**: 工具执行期间通过 MCP Context 发送日志给客户端
408
+ - **stdout 保护**: 确保 stdout 仅用于 MCP JSON 通信,避免污染
409
+
410
+ #### 调试模式(仅开发时使用)
403
411
  ```bash
404
- export LOG_LEVEL=DEBUG
412
+ # 启用调试模式(会向 stderr 输出日志)
413
+ export FUTU_DEBUG_MODE=1
405
414
  futu-mcp-server
406
415
  ```
407
416
 
408
- 日志文件位置:`./logs/futu_server.log`
417
+ **注意**: 在 MCP 客户端中不要启用调试模式,因为它会向 stderr 输出日志。
418
+
419
+ #### 日志文件位置
420
+ - 主日志文件:`./logs/futu_server.log`
421
+ - 自动轮转:500 MB 后轮转
422
+ - 自动清理:保留 10 天
423
+
424
+ 详细的日志配置说明请参考 [docs/LOGGING.md](docs/LOGGING.md)。
409
425
  tools = await session.list_tools()
410
426
 
411
427
  # Call a tool
@@ -357,13 +357,29 @@ python -m futu_stock_mcp_server.server
357
357
  ```
358
358
 
359
359
  ### 日志调试
360
- 启用详细日志:
360
+
361
+ 本项目已根据 [MCP 官方文档](https://github.com/modelcontextprotocol/python-sdk) 的最佳实践配置了日志系统:
362
+
363
+ #### MCP 兼容的日志配置
364
+ - **文件日志**: 所有日志写入 `logs/futu_server.log`,自动轮转和清理
365
+ - **MCP Context 日志**: 工具执行期间通过 MCP Context 发送日志给客户端
366
+ - **stdout 保护**: 确保 stdout 仅用于 MCP JSON 通信,避免污染
367
+
368
+ #### 调试模式(仅开发时使用)
361
369
  ```bash
362
- export LOG_LEVEL=DEBUG
370
+ # 启用调试模式(会向 stderr 输出日志)
371
+ export FUTU_DEBUG_MODE=1
363
372
  futu-mcp-server
364
373
  ```
365
374
 
366
- 日志文件位置:`./logs/futu_server.log`
375
+ **注意**: 在 MCP 客户端中不要启用调试模式,因为它会向 stderr 输出日志。
376
+
377
+ #### 日志文件位置
378
+ - 主日志文件:`./logs/futu_server.log`
379
+ - 自动轮转:500 MB 后轮转
380
+ - 自动清理:保留 10 天
381
+
382
+ 详细的日志配置说明请参考 [docs/LOGGING.md](docs/LOGGING.md)。
367
383
  tools = await session.list_tools()
368
384
 
369
385
  # Call a tool
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "futu-stock-mcp-server"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A Model Context Protocol (MCP) server for accessing Futu OpenAPI functionality"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -8,15 +8,18 @@ from loguru import logger
8
8
  import os
9
9
  import sys
10
10
  from dotenv import load_dotenv
11
- from mcp.server.fastmcp import FastMCP
11
+ from mcp.server.fastmcp import FastMCP, Context
12
12
  from mcp.types import TextContent, PromptMessage
13
13
  from mcp.server import Server
14
+ from mcp.server.session import ServerSession
14
15
  import atexit
15
16
  import signal
16
17
  import fcntl
17
18
  import psutil
18
19
  import time
19
20
  from datetime import datetime
21
+ import logging
22
+ import warnings
20
23
 
21
24
  # Get the project root directory and add it to Python path
22
25
  project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -26,37 +29,104 @@ sys.path.insert(0, project_root)
26
29
  env_path = os.path.join(project_root, '.env')
27
30
  load_dotenv(env_path)
28
31
 
29
- # Configure logging
30
- logger.remove() # Remove default handler
32
+ # CRITICAL: Configure logging to be MCP-compatible
33
+ # According to MCP best practices, we should:
34
+ # 1. Never write to stdout (reserved for MCP JSON communication)
35
+ # 2. Use file logging for debugging
36
+ # 3. Use MCP Context for operational logging when available
37
+ # 4. Suppress third-party library logs that might pollute output
38
+
39
+ # Completely silence warnings and third-party logs
40
+ warnings.filterwarnings("ignore")
41
+
42
+ # Configure loguru for file-only logging
43
+ logger.remove() # Remove all default handlers
31
44
 
32
45
  # Get the project root directory
33
46
  log_dir = os.path.join(project_root, "logs")
34
47
  os.makedirs(log_dir, exist_ok=True)
35
48
 
36
- # Add file handler
49
+ # Add file handler only - NO console output to avoid MCP communication interference
37
50
  logger.add(
38
51
  os.path.join(log_dir, "futu_server.log"),
39
52
  rotation="500 MB",
40
53
  retention="10 days",
41
54
  level="DEBUG",
42
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
55
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
56
+ enqueue=True, # Thread-safe logging
57
+ backtrace=True,
58
+ diagnose=True
43
59
  )
44
60
 
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
- )
61
+ # 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'):
63
+ logger.add(
64
+ sys.stderr,
65
+ level="INFO",
66
+ format="{time:HH:mm:ss} | {level} | {message}",
67
+ colorize=False,
68
+ filter=lambda record: record["level"].name in ["INFO", "WARNING", "ERROR", "CRITICAL"]
69
+ )
52
70
 
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
71
+ # Suppress all third-party library logging to prevent stdout pollution
72
+ logging.disable(logging.CRITICAL)
73
+
74
+ # Set up null handlers for problematic loggers
75
+ class NullHandler(logging.Handler):
76
+ def emit(self, record):
77
+ pass
78
+
79
+ null_handler = NullHandler()
80
+ root_logger = logging.getLogger()
81
+ root_logger.addHandler(null_handler)
82
+ root_logger.setLevel(logging.CRITICAL + 1)
83
+
84
+ # Specifically silence known problematic loggers
85
+ for logger_name in [
86
+ 'mcp', 'fastmcp', 'futu', 'uvicorn', 'asyncio',
87
+ 'websockets', 'aiohttp', 'urllib3', 'requests'
88
+ ]:
89
+ lib_logger = logging.getLogger(logger_name)
90
+ lib_logger.disabled = True
91
+ lib_logger.addHandler(null_handler)
92
+ lib_logger.setLevel(logging.CRITICAL + 1)
93
+ lib_logger.propagate = False
94
+
95
+ # MCP-compatible logging helper functions
96
+ async def log_to_mcp(ctx: Context, level: str, message: str):
97
+ """Send log message through MCP Context when available"""
98
+ try:
99
+ if level.upper() == "DEBUG":
100
+ await ctx.debug(message)
101
+ elif level.upper() == "INFO":
102
+ await ctx.info(message)
103
+ elif level.upper() == "WARNING":
104
+ await ctx.warning(message)
105
+ elif level.upper() == "ERROR":
106
+ await ctx.error(message)
107
+ else:
108
+ await ctx.info(f"[{level}] {message}")
109
+ except Exception:
110
+ # Fallback to file logging if MCP context fails
111
+ logger.log(level.upper(), message)
58
112
 
59
- logger.info(f"Starting server with log directory: {log_dir}")
113
+ def safe_log(level: str, message: str, ctx: Context = None):
114
+ """Safe logging that uses MCP context when available, file logging otherwise"""
115
+ # Always log to file
116
+ logger.log(level.upper(), message)
117
+
118
+ # Also send to MCP if context is available
119
+ if ctx:
120
+ try:
121
+ import asyncio
122
+ loop = asyncio.get_event_loop()
123
+ if loop.is_running():
124
+ asyncio.create_task(log_to_mcp(ctx, level, message))
125
+ except Exception:
126
+ pass # Ignore MCP logging errors
127
+
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")
60
130
 
61
131
  # PID file path
62
132
  PID_FILE = os.path.join(project_root, '.futu_mcp.pid')
@@ -411,7 +481,7 @@ def handle_return_data(ret: int, data: Any) -> Dict[str, Any]:
411
481
 
412
482
  # Market Data Tools
413
483
  @mcp.tool()
414
- async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
484
+ async def get_stock_quote(symbols: List[str], ctx: Context[ServerSession, None] = None) -> Dict[str, Any]:
415
485
  """Get stock quote data for given symbols
416
486
 
417
487
  Args:
@@ -455,21 +525,32 @@ async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
455
525
  - Consider actual needs when selecting stocks
456
526
  - Handle exceptions properly
457
527
  """
458
- ret, data = quote_ctx.get_stock_quote(symbols)
459
- if ret != RET_OK:
460
- return {'error': str(data)}
528
+ safe_log("info", f"Getting stock quotes for symbols: {symbols}", ctx)
461
529
 
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
- }
530
+ try:
531
+ ret, data = quote_ctx.get_stock_quote(symbols)
532
+ if ret != RET_OK:
533
+ error_msg = f"Failed to get stock quote: {str(data)}"
534
+ safe_log("error", error_msg, ctx)
535
+ return {'error': error_msg}
471
536
 
472
- return result
537
+ # Convert DataFrame to dict if necessary
538
+ if hasattr(data, 'to_dict'):
539
+ result = {
540
+ 'quote_list': data.to_dict('records')
541
+ }
542
+ else:
543
+ result = {
544
+ 'quote_list': data
545
+ }
546
+
547
+ safe_log("info", f"Successfully retrieved quotes for {len(symbols)} symbols", ctx)
548
+ return result
549
+
550
+ except Exception as e:
551
+ error_msg = f"Exception in get_stock_quote: {str(e)}"
552
+ safe_log("error", error_msg, ctx)
553
+ return {'error': error_msg}
473
554
 
474
555
  @mcp.tool()
475
556
  async def get_market_snapshot(symbols: List[str]) -> Dict[str, Any]:
@@ -1126,12 +1207,29 @@ async def get_option_butterfly(symbol: str, expiry: str, strike_price: float) ->
1126
1207
 
1127
1208
  # Account Query Tools
1128
1209
  @mcp.tool()
1129
- async def get_account_list() -> Dict[str, Any]:
1210
+ async def get_account_list(ctx: Context[ServerSession, None] = None) -> Dict[str, Any]:
1130
1211
  """Get account list"""
1212
+ safe_log("info", "Attempting to get account list", ctx)
1213
+
1131
1214
  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)
1215
+ error_msg = 'Failed to initialize trade connection'
1216
+ safe_log("error", error_msg, ctx)
1217
+ return {'error': error_msg}
1218
+
1219
+ try:
1220
+ ret, data = trade_ctx.get_acc_list()
1221
+ result = handle_return_data(ret, data)
1222
+
1223
+ if 'error' not in result:
1224
+ safe_log("info", "Successfully retrieved account list", ctx)
1225
+ else:
1226
+ safe_log("error", f"Failed to get account list: {result['error']}", ctx)
1227
+
1228
+ return result
1229
+ except Exception as e:
1230
+ error_msg = f"Exception in get_account_list: {str(e)}"
1231
+ safe_log("error", error_msg, ctx)
1232
+ return {'error': error_msg}
1135
1233
 
1136
1234
  @mcp.tool()
1137
1235
  async def get_funds() -> Dict[str, Any]:
@@ -1457,43 +1555,61 @@ async def get_current_time() -> Dict[str, Any]:
1457
1555
  def main():
1458
1556
  """Main entry point for the futu-mcp-server command."""
1459
1557
  try:
1460
- # Ensure no color output in MCP mode
1558
+ # CRITICAL: Set MCP mode BEFORE any logging to ensure clean stdout
1559
+ os.environ['MCP_MODE'] = '1'
1560
+
1561
+ # Ensure no color output or ANSI escape sequences in MCP mode
1461
1562
  os.environ['NO_COLOR'] = '1'
1462
1563
  os.environ['TERM'] = 'dumb'
1564
+ os.environ['FORCE_COLOR'] = '0'
1565
+ os.environ['COLORTERM'] = ''
1566
+ os.environ['ANSI_COLORS_DISABLED'] = '1'
1567
+
1568
+ # Disable Python buffering to ensure clean MCP JSON communication
1569
+ os.environ['PYTHONUNBUFFERED'] = '1'
1570
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
1463
1571
 
1464
- # 清理旧的进程和文件
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
1576
+
1577
+ # Clean up stale processes and acquire lock
1465
1578
  cleanup_stale_processes()
1466
1579
 
1467
- # 获取锁
1468
1580
  lock_fd = acquire_lock()
1469
1581
  if lock_fd is None:
1582
+ # Use file logging only - no stderr output in MCP mode
1470
1583
  logger.error("Failed to acquire lock. Another instance may be running.")
1471
1584
  sys.exit(1)
1472
1585
 
1473
- # 设置信号处理
1586
+ # Set up signal handlers
1474
1587
  signal.signal(signal.SIGINT, signal_handler)
1475
1588
  signal.signal(signal.SIGTERM, signal_handler)
1476
1589
 
1477
- logger.info("Initializing Futu connection...")
1590
+ # Initialize Futu connection with file logging only
1591
+ logger.info("Initializing Futu connection for MCP server...")
1478
1592
  if init_futu_connection():
1479
1593
  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")
1594
+ logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
1595
+
1482
1596
  try:
1597
+ # Run MCP server - stdout will be used for JSON communication only
1483
1598
  mcp.run(transport='stdio')
1484
1599
  except KeyboardInterrupt:
1485
- logger.info("Received keyboard interrupt, shutting down...")
1600
+ logger.info("Received keyboard interrupt, shutting down gracefully...")
1486
1601
  cleanup_all()
1487
1602
  os._exit(0)
1488
1603
  except Exception as e:
1489
- logger.error(f"Error running server: {str(e)}")
1604
+ logger.error(f"Error running MCP server: {str(e)}")
1490
1605
  cleanup_all()
1491
1606
  os._exit(1)
1492
1607
  else:
1493
- logger.error("Failed to initialize Futu connection. Server will not start.")
1608
+ logger.error("Failed to initialize Futu connection. MCP server will not start.")
1494
1609
  os._exit(1)
1610
+
1495
1611
  except Exception as e:
1496
- logger.error(f"Error starting server: {str(e)}")
1612
+ logger.error(f"Error starting MCP server: {str(e)}")
1497
1613
  sys.exit(1)
1498
1614
  finally:
1499
1615
  cleanup_all()