futu-stock-mcp-server 0.1.4__py3-none-any.whl → 0.1.6__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.
- futu_stock_mcp_server/server.py +100 -108
- {futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/METADATA +1 -1
- futu_stock_mcp_server-0.1.6.dist-info/RECORD +7 -0
- futu_stock_mcp_server-0.1.4.dist-info/RECORD +0 -7
- {futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/WHEEL +0 -0
- {futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/entry_points.txt +0 -0
- {futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/licenses/LICENSE +0 -0
futu_stock_mcp_server/server.py
CHANGED
|
@@ -51,23 +51,29 @@ class StdoutProtector:
|
|
|
51
51
|
def flush(self):
|
|
52
52
|
self.original.flush()
|
|
53
53
|
|
|
54
|
+
def readable(self):
|
|
55
|
+
"""Delegate readable() method to original stdout"""
|
|
56
|
+
return getattr(self.original, 'readable', lambda: False)()
|
|
57
|
+
|
|
58
|
+
def writable(self):
|
|
59
|
+
"""Delegate writable() method to original stdout"""
|
|
60
|
+
return getattr(self.original, 'writable', lambda: True)()
|
|
61
|
+
|
|
62
|
+
def seekable(self):
|
|
63
|
+
"""Delegate seekable() method to original stdout"""
|
|
64
|
+
return getattr(self.original, 'seekable', lambda: False)()
|
|
65
|
+
|
|
54
66
|
def __getattr__(self, name):
|
|
55
67
|
return getattr(self.original, name)
|
|
56
68
|
|
|
57
69
|
# Apply stdout protection in MCP mode VERY EARLY, before any imports
|
|
58
70
|
if os.getenv('MCP_MODE') == '1':
|
|
59
|
-
sys.stdout = StdoutProtector(sys.stdout)
|
|
71
|
+
# sys.stdout = StdoutProtector(sys.stdout)
|
|
60
72
|
|
|
61
|
-
# 5.
|
|
73
|
+
# 5. Don't redirect stderr in MCP mode - let it work normally
|
|
74
|
+
# MCP servers can use stderr for logging, only stdout needs protection
|
|
62
75
|
_stderr_redirected = False
|
|
63
76
|
_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
77
|
|
|
72
78
|
# Now we can safely import other modules
|
|
73
79
|
from contextlib import asynccontextmanager
|
|
@@ -263,6 +269,8 @@ trade_ctx = None
|
|
|
263
269
|
lock_fd = None
|
|
264
270
|
_is_shutting_down = False
|
|
265
271
|
_is_trade_initialized = False
|
|
272
|
+
_futu_host = '127.0.0.1'
|
|
273
|
+
_futu_port = 11111
|
|
266
274
|
|
|
267
275
|
def is_process_running(pid):
|
|
268
276
|
"""Check if a process with given PID is running"""
|
|
@@ -436,14 +444,14 @@ def acquire_lock():
|
|
|
436
444
|
|
|
437
445
|
def init_quote_connection():
|
|
438
446
|
"""Initialize quote connection only"""
|
|
439
|
-
global quote_ctx
|
|
447
|
+
global quote_ctx, _futu_host, _futu_port
|
|
440
448
|
|
|
441
449
|
try:
|
|
442
450
|
# Check if OpenD is running by attempting to get global state
|
|
443
451
|
try:
|
|
444
452
|
temp_ctx = OpenQuoteContext(
|
|
445
|
-
host=
|
|
446
|
-
port=
|
|
453
|
+
host=_futu_host,
|
|
454
|
+
port=_futu_port
|
|
447
455
|
)
|
|
448
456
|
ret, _ = temp_ctx.get_global_state()
|
|
449
457
|
temp_ctx.close()
|
|
@@ -456,8 +464,8 @@ def init_quote_connection():
|
|
|
456
464
|
|
|
457
465
|
# Initialize Futu connection
|
|
458
466
|
quote_ctx = OpenQuoteContext(
|
|
459
|
-
host=
|
|
460
|
-
port=
|
|
467
|
+
host=_futu_host,
|
|
468
|
+
port=_futu_port
|
|
461
469
|
)
|
|
462
470
|
logger.info("Successfully connected to Futu Quote API")
|
|
463
471
|
return True
|
|
@@ -469,7 +477,7 @@ def init_quote_connection():
|
|
|
469
477
|
|
|
470
478
|
def init_trade_connection():
|
|
471
479
|
"""Initialize trade connection only"""
|
|
472
|
-
global trade_ctx, _is_trade_initialized
|
|
480
|
+
global trade_ctx, _is_trade_initialized, _futu_host, _futu_port
|
|
473
481
|
|
|
474
482
|
if _is_trade_initialized and trade_ctx:
|
|
475
483
|
return True
|
|
@@ -489,8 +497,8 @@ def init_trade_connection():
|
|
|
489
497
|
# 创建交易上下文
|
|
490
498
|
trade_ctx = OpenSecTradeContext(
|
|
491
499
|
filter_trdmarket=trd_market,
|
|
492
|
-
host=
|
|
493
|
-
port=
|
|
500
|
+
host=_futu_host,
|
|
501
|
+
port=_futu_port,
|
|
494
502
|
security_firm=security_firm
|
|
495
503
|
)
|
|
496
504
|
|
|
@@ -554,110 +562,76 @@ def init_trade_connection():
|
|
|
554
562
|
_is_trade_initialized = False
|
|
555
563
|
return False
|
|
556
564
|
|
|
557
|
-
def init_futu_connection() -> bool:
|
|
565
|
+
def init_futu_connection(host: str = '127.0.0.1', port: int = 11111) -> bool:
|
|
558
566
|
"""
|
|
559
567
|
Initialize connection to Futu OpenD.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
host: Futu OpenD host address
|
|
571
|
+
port: Futu OpenD port number
|
|
572
|
+
|
|
560
573
|
Returns True if successful, False otherwise.
|
|
561
574
|
"""
|
|
562
|
-
global quote_ctx, trade_ctx, _is_trade_initialized
|
|
575
|
+
global quote_ctx, trade_ctx, _is_trade_initialized, _futu_host, _futu_port
|
|
563
576
|
|
|
564
577
|
try:
|
|
565
|
-
#
|
|
566
|
-
|
|
567
|
-
|
|
578
|
+
# Set global connection parameters
|
|
579
|
+
_futu_host = host
|
|
580
|
+
_futu_port = port
|
|
568
581
|
|
|
569
582
|
# Log to file only
|
|
570
583
|
logger.info(f"Initializing Futu connection to {host}:{port}")
|
|
571
584
|
|
|
572
|
-
#
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
585
|
+
# Initialize quote context
|
|
586
|
+
quote_ctx = OpenQuoteContext(host=host, port=port)
|
|
587
|
+
|
|
588
|
+
# Initialize trade context if needed
|
|
589
|
+
if os.getenv('FUTU_ENABLE_TRADING', '0') == '1':
|
|
590
|
+
# Get trading parameters
|
|
591
|
+
trade_env = os.getenv('FUTU_TRADE_ENV', 'SIMULATE')
|
|
592
|
+
security_firm = os.getenv('FUTU_SECURITY_FIRM', 'FUTUSECURITIES')
|
|
593
|
+
trd_market = os.getenv('FUTU_TRD_MARKET', 'HK')
|
|
594
|
+
|
|
595
|
+
# Map environment strings to Futu enums
|
|
596
|
+
trade_env_enum = {
|
|
597
|
+
'REAL': TrdMarket.REAL,
|
|
598
|
+
'SIMULATE': TrdMarket.SIMULATE
|
|
599
|
+
}.get(trade_env, TrdMarket.SIMULATE)
|
|
600
|
+
|
|
601
|
+
security_firm_enum = {
|
|
602
|
+
'FUTUSECURITIES': SecurityFirm.FUTUSECURITIES,
|
|
603
|
+
'FUTUINC': SecurityFirm.FUTUINC
|
|
604
|
+
}.get(security_firm, SecurityFirm.FUTUSECURITIES)
|
|
605
|
+
|
|
606
|
+
trd_market_enum = {
|
|
607
|
+
'HK': TrdMarket.HK,
|
|
608
|
+
'US': TrdMarket.US,
|
|
609
|
+
'CN': TrdMarket.CN,
|
|
610
|
+
'HKCC': TrdMarket.HKCC,
|
|
611
|
+
'AU': TrdMarket.AU
|
|
612
|
+
}.get(trd_market, TrdMarket.HK)
|
|
613
|
+
|
|
614
|
+
# Initialize trade context
|
|
615
|
+
trade_ctx = OpenSecTradeContext(
|
|
616
|
+
host=host,
|
|
617
|
+
port=port,
|
|
618
|
+
security_firm=security_firm_enum
|
|
619
|
+
)
|
|
620
|
+
_is_trade_initialized = True
|
|
621
|
+
logger.info("Trade context initialized successfully")
|
|
593
622
|
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
623
|
+
logger.info("Futu connection initialized successfully")
|
|
624
|
+
return True
|
|
646
625
|
|
|
647
626
|
except Exception as e:
|
|
648
627
|
error_msg = f"Failed to initialize Futu connection: {str(e)}"
|
|
649
628
|
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
629
|
return False
|
|
654
630
|
|
|
655
631
|
@asynccontextmanager
|
|
656
632
|
async def lifespan(server: Server):
|
|
657
|
-
# Startup -
|
|
658
|
-
|
|
659
|
-
logger.error("Failed to initialize quote connection")
|
|
660
|
-
raise Exception("Quote connection failed")
|
|
633
|
+
# Startup - connections are already initialized in main()
|
|
634
|
+
# No need to initialize here as it's done before mcp.run()
|
|
661
635
|
try:
|
|
662
636
|
yield
|
|
663
637
|
finally:
|
|
@@ -1779,12 +1753,15 @@ def main():
|
|
|
1779
1753
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1780
1754
|
epilog="""
|
|
1781
1755
|
Examples:
|
|
1782
|
-
futu-mcp-server
|
|
1783
|
-
futu-mcp-server --
|
|
1756
|
+
futu-mcp-server # Start the MCP server with default settings
|
|
1757
|
+
futu-mcp-server --host 192.168.1.100 --port 11111 # Connect to remote OpenD
|
|
1758
|
+
futu-mcp-server --help # Show this help message
|
|
1759
|
+
|
|
1760
|
+
Arguments:
|
|
1761
|
+
--host # Futu OpenD host (default: 127.0.0.1)
|
|
1762
|
+
--port # Futu OpenD port (default: 11111)
|
|
1784
1763
|
|
|
1785
1764
|
Environment Variables:
|
|
1786
|
-
FUTU_HOST # Futu OpenD host (default: 127.0.0.1)
|
|
1787
|
-
FUTU_PORT # Futu OpenD port (default: 11111)
|
|
1788
1765
|
FUTU_ENABLE_TRADING # Enable trading features (default: 0)
|
|
1789
1766
|
FUTU_TRADE_ENV # Trading environment: SIMULATE or REAL (default: SIMULATE)
|
|
1790
1767
|
FUTU_SECURITY_FIRM # Security firm: FUTUSECURITIES or FUTUINC (default: FUTUSECURITIES)
|
|
@@ -1793,6 +1770,19 @@ Environment Variables:
|
|
|
1793
1770
|
"""
|
|
1794
1771
|
)
|
|
1795
1772
|
|
|
1773
|
+
parser.add_argument(
|
|
1774
|
+
'--host',
|
|
1775
|
+
default='127.0.0.1',
|
|
1776
|
+
help='Futu OpenD host address (default: 127.0.0.1)'
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
parser.add_argument(
|
|
1780
|
+
'--port',
|
|
1781
|
+
type=int,
|
|
1782
|
+
default=11111,
|
|
1783
|
+
help='Futu OpenD port number (default: 11111)'
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1796
1786
|
parser.add_argument(
|
|
1797
1787
|
'--version',
|
|
1798
1788
|
action='version',
|
|
@@ -1831,13 +1821,15 @@ Environment Variables:
|
|
|
1831
1821
|
|
|
1832
1822
|
# Initialize Futu connection with file logging only
|
|
1833
1823
|
logger.info("Initializing Futu connection for MCP server...")
|
|
1834
|
-
if init_futu_connection():
|
|
1824
|
+
if init_futu_connection(args.host, args.port):
|
|
1835
1825
|
logger.info("Successfully initialized Futu connection")
|
|
1836
1826
|
logger.info("Starting MCP server in stdio mode - stdout reserved for JSON communication")
|
|
1837
1827
|
|
|
1838
1828
|
try:
|
|
1839
1829
|
# Run MCP server - stdout will be used for JSON communication only
|
|
1840
|
-
mcp.run(transport
|
|
1830
|
+
logger.info("About to call mcp.run() without transport parameter")
|
|
1831
|
+
mcp.run()
|
|
1832
|
+
logger.info("mcp.run() completed successfully")
|
|
1841
1833
|
except KeyboardInterrupt:
|
|
1842
1834
|
logger.info("Received keyboard interrupt, shutting down gracefully...")
|
|
1843
1835
|
cleanup_all()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: futu-stock-mcp-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
futu_stock_mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
futu_stock_mcp_server/server.py,sha256=p3wC7FE_QC2jEYgmYOSU6X4MA3Vg08rzoWAablVIdac,65854
|
|
3
|
+
futu_stock_mcp_server-0.1.6.dist-info/METADATA,sha256=zwCGxXZRQscuo1xa5kDMUr23gHc50oQW0CLOS3O6kQE,21041
|
|
4
|
+
futu_stock_mcp_server-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
futu_stock_mcp_server-0.1.6.dist-info/entry_points.txt,sha256=GAdKqPJD9dJ_fRA3e3m0NRia0elN5OcjEeAI30vOcIM,70
|
|
6
|
+
futu_stock_mcp_server-0.1.6.dist-info/licenses/LICENSE,sha256=XQBSQkjjpkymu_uLdyis4oNynV60VH1X7nS16uwM6g0,1069
|
|
7
|
+
futu_stock_mcp_server-0.1.6.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=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,,
|
|
File without changes
|
{futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{futu_stock_mcp_server-0.1.4.dist-info → futu_stock_mcp_server-0.1.6.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|