futu-stock-mcp-server 0.1.1__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.1
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
@@ -40,6 +40,8 @@ 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
  # Futu Stock MCP Server
44
46
 
45
47
  [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org)
@@ -101,16 +103,25 @@ Description-Content-Type: text/markdown
101
103
 
102
104
  ## 🚀 快速开始
103
105
 
104
- ### 方式一:通过 PyPI 安装(推荐)
106
+ ### 方式一:通过 pipx 安装(推荐)
105
107
 
106
108
  ```bash
109
+ # 安装 pipx(如果还没有安装)
110
+ brew install pipx # macOS
111
+ # 或者 pip install --user pipx # 其他系统
112
+
107
113
  # 安装包
108
- pip install futu-stock-mcp-server
114
+ pipx install futu-stock-mcp-server
109
115
 
110
116
  # 运行服务器
111
117
  futu-mcp-server
112
118
  ```
113
119
 
120
+ > **为什么使用 pipx?**
121
+ > - pipx 专门用于安装 Python 应用程序到全局环境
122
+ > - 自动管理独立的虚拟环境,避免依赖冲突
123
+ > - 命令直接可用,无需激活虚拟环境
124
+
114
125
  ### 方式二:通过 Docker 运行
115
126
 
116
127
  ```bash
@@ -239,12 +250,13 @@ ruff format .
239
250
  }
240
251
  ```
241
252
 
242
- 3. **如果使用虚拟环境或从源码运行**:
253
+ 3. **故障排除配置**:
254
+ 如果上述配置不工作,可以尝试使用完整路径:
243
255
  ```json
244
256
  {
245
257
  "mcpServers": {
246
258
  "futu-stock": {
247
- "command": "/path/to/your/venv/bin/futu-mcp-server",
259
+ "command": "/Users/your-username/.local/bin/futu-mcp-server",
248
260
  "env": {
249
261
  "FUTU_HOST": "127.0.0.1",
250
262
  "FUTU_PORT": "11111"
@@ -254,22 +266,7 @@ ruff format .
254
266
  }
255
267
  ```
256
268
 
257
- 或者使用 Python 模块方式:
258
- ```json
259
- {
260
- "mcpServers": {
261
- "futu-stock": {
262
- "command": "python",
263
- "args": ["-m", "futu_stock_mcp_server.server"],
264
- "cwd": "/path/to/futu-stock-mcp-server",
265
- "env": {
266
- "FUTU_HOST": "127.0.0.1",
267
- "FUTU_PORT": "11111"
268
- }
269
- }
270
- }
271
- }
272
- ```
269
+ > **提示**:使用 `which futu-mcp-server` 命令查看完整路径
273
270
 
274
271
  ### 在其他 MCP 客户端中配置
275
272
 
@@ -363,10 +360,13 @@ LOG_LEVEL=INFO
363
360
  #### 1. 命令 `futu-mcp-server` 找不到
364
361
  ```bash
365
362
  # 确保已正确安装
366
- pip install futu-stock-mcp-server
363
+ pipx install futu-stock-mcp-server
367
364
 
368
- # 或者检查是否在虚拟环境中
365
+ # 检查命令是否可用
369
366
  which futu-mcp-server
367
+
368
+ # 如果还是找不到,检查 PATH
369
+ echo $PATH | grep -o '[^:]*\.local/bin[^:]*'
370
370
  ```
371
371
 
372
372
  #### 2. Ctrl+C 无法退出服务器
@@ -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
@@ -1,3 +1,5 @@
1
+ [![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)
2
+
1
3
  # Futu Stock MCP Server
2
4
 
3
5
  [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org)
@@ -59,16 +61,25 @@
59
61
 
60
62
  ## 🚀 快速开始
61
63
 
62
- ### 方式一:通过 PyPI 安装(推荐)
64
+ ### 方式一:通过 pipx 安装(推荐)
63
65
 
64
66
  ```bash
67
+ # 安装 pipx(如果还没有安装)
68
+ brew install pipx # macOS
69
+ # 或者 pip install --user pipx # 其他系统
70
+
65
71
  # 安装包
66
- pip install futu-stock-mcp-server
72
+ pipx install futu-stock-mcp-server
67
73
 
68
74
  # 运行服务器
69
75
  futu-mcp-server
70
76
  ```
71
77
 
78
+ > **为什么使用 pipx?**
79
+ > - pipx 专门用于安装 Python 应用程序到全局环境
80
+ > - 自动管理独立的虚拟环境,避免依赖冲突
81
+ > - 命令直接可用,无需激活虚拟环境
82
+
72
83
  ### 方式二:通过 Docker 运行
73
84
 
74
85
  ```bash
@@ -197,12 +208,13 @@ ruff format .
197
208
  }
198
209
  ```
199
210
 
200
- 3. **如果使用虚拟环境或从源码运行**:
211
+ 3. **故障排除配置**:
212
+ 如果上述配置不工作,可以尝试使用完整路径:
201
213
  ```json
202
214
  {
203
215
  "mcpServers": {
204
216
  "futu-stock": {
205
- "command": "/path/to/your/venv/bin/futu-mcp-server",
217
+ "command": "/Users/your-username/.local/bin/futu-mcp-server",
206
218
  "env": {
207
219
  "FUTU_HOST": "127.0.0.1",
208
220
  "FUTU_PORT": "11111"
@@ -212,22 +224,7 @@ ruff format .
212
224
  }
213
225
  ```
214
226
 
215
- 或者使用 Python 模块方式:
216
- ```json
217
- {
218
- "mcpServers": {
219
- "futu-stock": {
220
- "command": "python",
221
- "args": ["-m", "futu_stock_mcp_server.server"],
222
- "cwd": "/path/to/futu-stock-mcp-server",
223
- "env": {
224
- "FUTU_HOST": "127.0.0.1",
225
- "FUTU_PORT": "11111"
226
- }
227
- }
228
- }
229
- }
230
- ```
227
+ > **提示**:使用 `which futu-mcp-server` 命令查看完整路径
231
228
 
232
229
  ### 在其他 MCP 客户端中配置
233
230
 
@@ -321,10 +318,13 @@ LOG_LEVEL=INFO
321
318
  #### 1. 命令 `futu-mcp-server` 找不到
322
319
  ```bash
323
320
  # 确保已正确安装
324
- pip install futu-stock-mcp-server
321
+ pipx install futu-stock-mcp-server
325
322
 
326
- # 或者检查是否在虚拟环境中
323
+ # 检查命令是否可用
327
324
  which futu-mcp-server
325
+
326
+ # 如果还是找不到,检查 PATH
327
+ echo $PATH | grep -o '[^:]*\.local/bin[^:]*'
328
328
  ```
329
329
 
330
330
  #### 2. Ctrl+C 无法退出服务器
@@ -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.1"
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,30 +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
46
- logger.add(
47
- lambda msg: print(msg),
48
- level="INFO",
49
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
50
- )
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
+ )
70
+
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
51
78
 
52
- logger.info(f"Starting server with log directory: {log_dir}")
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)
112
+
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")
53
130
 
54
131
  # PID file path
55
132
  PID_FILE = os.path.join(project_root, '.futu_mcp.pid')
@@ -404,7 +481,7 @@ def handle_return_data(ret: int, data: Any) -> Dict[str, Any]:
404
481
 
405
482
  # Market Data Tools
406
483
  @mcp.tool()
407
- 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]:
408
485
  """Get stock quote data for given symbols
409
486
 
410
487
  Args:
@@ -448,21 +525,32 @@ async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
448
525
  - Consider actual needs when selecting stocks
449
526
  - Handle exceptions properly
450
527
  """
451
- ret, data = quote_ctx.get_stock_quote(symbols)
452
- if ret != RET_OK:
453
- return {'error': str(data)}
528
+ safe_log("info", f"Getting stock quotes for symbols: {symbols}", ctx)
454
529
 
455
- # Convert DataFrame to dict if necessary
456
- if hasattr(data, 'to_dict'):
457
- result = {
458
- 'quote_list': data.to_dict('records')
459
- }
460
- else:
461
- result = {
462
- 'quote_list': data
463
- }
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}
464
536
 
465
- 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}
466
554
 
467
555
  @mcp.tool()
468
556
  async def get_market_snapshot(symbols: List[str]) -> Dict[str, Any]:
@@ -1119,12 +1207,29 @@ async def get_option_butterfly(symbol: str, expiry: str, strike_price: float) ->
1119
1207
 
1120
1208
  # Account Query Tools
1121
1209
  @mcp.tool()
1122
- async def get_account_list() -> Dict[str, Any]:
1210
+ async def get_account_list(ctx: Context[ServerSession, None] = None) -> Dict[str, Any]:
1123
1211
  """Get account list"""
1212
+ safe_log("info", "Attempting to get account list", ctx)
1213
+
1124
1214
  if not init_trade_connection():
1125
- return {'error': 'Failed to initialize trade connection'}
1126
- ret, data = trade_ctx.get_acc_list()
1127
- 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}
1128
1233
 
1129
1234
  @mcp.tool()
1130
1235
  async def get_funds() -> Dict[str, Any]:
@@ -1450,39 +1555,61 @@ async def get_current_time() -> Dict[str, Any]:
1450
1555
  def main():
1451
1556
  """Main entry point for the futu-mcp-server command."""
1452
1557
  try:
1453
- # 清理旧的进程和文件
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
1562
+ os.environ['NO_COLOR'] = '1'
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'
1571
+
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
1454
1578
  cleanup_stale_processes()
1455
1579
 
1456
- # 获取锁
1457
1580
  lock_fd = acquire_lock()
1458
1581
  if lock_fd is None:
1582
+ # Use file logging only - no stderr output in MCP mode
1459
1583
  logger.error("Failed to acquire lock. Another instance may be running.")
1460
1584
  sys.exit(1)
1461
1585
 
1462
- # 设置信号处理
1586
+ # Set up signal handlers
1463
1587
  signal.signal(signal.SIGINT, signal_handler)
1464
1588
  signal.signal(signal.SIGTERM, signal_handler)
1465
1589
 
1466
- logger.info("Initializing Futu connection...")
1590
+ # Initialize Futu connection with file logging only
1591
+ logger.info("Initializing Futu connection for MCP server...")
1467
1592
  if init_futu_connection():
1468
1593
  logger.info("Successfully initialized Futu connection")
1469
- logger.info("Starting MCP server in stdio mode...")
1470
- logger.info("Press Ctrl+C to stop the server")
1594
+ logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
1595
+
1471
1596
  try:
1597
+ # Run MCP server - stdout will be used for JSON communication only
1472
1598
  mcp.run(transport='stdio')
1473
1599
  except KeyboardInterrupt:
1474
- logger.info("Received keyboard interrupt, shutting down...")
1600
+ logger.info("Received keyboard interrupt, shutting down gracefully...")
1475
1601
  cleanup_all()
1476
1602
  os._exit(0)
1477
1603
  except Exception as e:
1478
- logger.error(f"Error running server: {str(e)}")
1604
+ logger.error(f"Error running MCP server: {str(e)}")
1479
1605
  cleanup_all()
1480
1606
  os._exit(1)
1481
1607
  else:
1482
- logger.error("Failed to initialize Futu connection. Server will not start.")
1608
+ logger.error("Failed to initialize Futu connection. MCP server will not start.")
1483
1609
  os._exit(1)
1610
+
1484
1611
  except Exception as e:
1485
- logger.error(f"Error starting server: {str(e)}")
1612
+ logger.error(f"Error starting MCP server: {str(e)}")
1486
1613
  sys.exit(1)
1487
1614
  finally:
1488
1615
  cleanup_all()