mseep-a-share-mcp 0.1.1__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.
@@ -0,0 +1,99 @@
1
+ """
2
+ Market overview tools for MCP server.
3
+ Contains tools for fetching trading dates and all stock data.
4
+ """
5
+ import logging
6
+ from typing import Optional
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+ from src.data_source_interface import FinancialDataSource, NoDataFoundError, LoginError, DataSourceError
10
+ from src.formatting.markdown_formatter import format_df_to_markdown
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def register_market_overview_tools(app: FastMCP, active_data_source: FinancialDataSource):
16
+ """
17
+ Register market overview tools with the MCP app.
18
+
19
+ Args:
20
+ app: The FastMCP app instance
21
+ active_data_source: The active financial data source
22
+ """
23
+
24
+ @app.tool()
25
+ def get_trade_dates(start_date: Optional[str] = None, end_date: Optional[str] = None) -> str:
26
+ """
27
+ Fetches trading dates information within a specified range.
28
+
29
+ Args:
30
+ start_date: Optional. Start date in 'YYYY-MM-DD' format. Defaults to 2015-01-01 if None.
31
+ end_date: Optional. End date in 'YYYY-MM-DD' format. Defaults to the current date if None.
32
+
33
+ Returns:
34
+ Markdown table indicating whether each date in the range was a trading day (1) or not (0).
35
+ """
36
+ logger.info(
37
+ f"Tool 'get_trade_dates' called for range {start_date or 'default'} to {end_date or 'default'}")
38
+ try:
39
+ # Add date validation if desired
40
+ df = active_data_source.get_trade_dates(
41
+ start_date=start_date, end_date=end_date)
42
+ logger.info("Successfully retrieved trade dates.")
43
+ # Trade dates table can be long, apply standard truncation
44
+ return format_df_to_markdown(df)
45
+
46
+ except NoDataFoundError as e:
47
+ logger.warning(f"NoDataFoundError: {e}")
48
+ return f"Error: {e}"
49
+ except LoginError as e:
50
+ logger.error(f"LoginError: {e}")
51
+ return f"Error: Could not connect to data source. {e}"
52
+ except DataSourceError as e:
53
+ logger.error(f"DataSourceError: {e}")
54
+ return f"Error: An error occurred while fetching data. {e}"
55
+ except ValueError as e:
56
+ logger.warning(f"ValueError: {e}")
57
+ return f"Error: Invalid input parameter. {e}"
58
+ except Exception as e:
59
+ logger.exception(
60
+ f"Unexpected Exception processing get_trade_dates: {e}")
61
+ return f"Error: An unexpected error occurred: {e}"
62
+
63
+ @app.tool()
64
+ def get_all_stock(date: Optional[str] = None) -> str:
65
+ """
66
+ Fetches a list of all stocks (A-shares and indices) and their trading status for a given date.
67
+
68
+ Args:
69
+ date: Optional. The date in 'YYYY-MM-DD' format. If None, uses the current date.
70
+
71
+ Returns:
72
+ Markdown table listing stock codes, names, and their trading status (1=trading, 0=suspended).
73
+ """
74
+ logger.info(
75
+ f"Tool 'get_all_stock' called for date={date or 'default'}")
76
+ try:
77
+ # Add date validation if desired
78
+ df = active_data_source.get_all_stock(date=date)
79
+ logger.info(
80
+ f"Successfully retrieved stock list for {date or 'default'}.")
81
+ # This list can be very long, apply standard truncation
82
+ return format_df_to_markdown(df)
83
+
84
+ except NoDataFoundError as e:
85
+ logger.warning(f"NoDataFoundError: {e}")
86
+ return f"Error: {e}"
87
+ except LoginError as e:
88
+ logger.error(f"LoginError: {e}")
89
+ return f"Error: Could not connect to data source. {e}"
90
+ except DataSourceError as e:
91
+ logger.error(f"DataSourceError: {e}")
92
+ return f"Error: An error occurred while fetching data. {e}"
93
+ except ValueError as e:
94
+ logger.warning(f"ValueError: {e}")
95
+ return f"Error: Invalid input parameter. {e}"
96
+ except Exception as e:
97
+ logger.exception(
98
+ f"Unexpected Exception processing get_all_stock: {e}")
99
+ return f"Error: An unexpected error occurred: {e}"
tools/stock_market.py ADDED
@@ -0,0 +1,242 @@
1
+ """
2
+ Stock market data tools for MCP server.
3
+ """
4
+ import logging
5
+ from typing import List, Optional
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ from src.data_source_interface import FinancialDataSource, NoDataFoundError, LoginError, DataSourceError
9
+ from src.formatting.markdown_formatter import format_df_to_markdown
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def register_stock_market_tools(app: FastMCP, active_data_source: FinancialDataSource):
15
+ """
16
+ Register stock market data tools with the MCP app.
17
+
18
+ Args:
19
+ app: The FastMCP app instance
20
+ active_data_source: The active financial data source
21
+ """
22
+
23
+ @app.tool()
24
+ def get_historical_k_data(
25
+ code: str,
26
+ start_date: str,
27
+ end_date: str,
28
+ frequency: str = "d",
29
+ adjust_flag: str = "3",
30
+ fields: Optional[List[str]] = None,
31
+ ) -> str:
32
+ """
33
+ Fetches historical K-line (OHLCV) data for a Chinese A-share stock.
34
+
35
+ Args:
36
+ code: The stock code in Baostock format (e.g., 'sh.600000', 'sz.000001').
37
+ start_date: Start date in 'YYYY-MM-DD' format.
38
+ end_date: End date in 'YYYY-MM-DD' format.
39
+ frequency: Data frequency. Valid options (from Baostock):
40
+ 'd': daily
41
+ 'w': weekly
42
+ 'm': monthly
43
+ '5': 5 minutes
44
+ '15': 15 minutes
45
+ '30': 30 minutes
46
+ '60': 60 minutes
47
+ Defaults to 'd'.
48
+ adjust_flag: Adjustment flag for price/volume. Valid options (from Baostock):
49
+ '1': Forward adjusted (后复权)
50
+ '2': Backward adjusted (前复权)
51
+ '3': Non-adjusted (不复权)
52
+ Defaults to '3'.
53
+ fields: Optional list of specific data fields to retrieve (must be valid Baostock fields).
54
+ If None or empty, default fields will be used (e.g., date, code, open, high, low, close, volume, amount, pctChg).
55
+
56
+ Returns:
57
+ A Markdown formatted string containing the K-line data table, or an error message.
58
+ The table might be truncated if the result set is too large.
59
+ """
60
+ logger.info(
61
+ f"Tool 'get_historical_k_data' called for {code} ({start_date}-{end_date}, freq={frequency}, adj={adjust_flag}, fields={fields})")
62
+ try:
63
+ # Validate frequency and adjust_flag if necessary (basic example)
64
+ valid_freqs = ['d', 'w', 'm', '5', '15', '30', '60']
65
+ valid_adjusts = ['1', '2', '3']
66
+ if frequency not in valid_freqs:
67
+ logger.warning(f"Invalid frequency requested: {frequency}")
68
+ return f"Error: Invalid frequency '{frequency}'. Valid options are: {valid_freqs}"
69
+ if adjust_flag not in valid_adjusts:
70
+ logger.warning(f"Invalid adjust_flag requested: {adjust_flag}")
71
+ return f"Error: Invalid adjust_flag '{adjust_flag}'. Valid options are: {valid_adjusts}"
72
+
73
+ # Call the injected data source
74
+ df = active_data_source.get_historical_k_data(
75
+ code=code,
76
+ start_date=start_date,
77
+ end_date=end_date,
78
+ frequency=frequency,
79
+ adjust_flag=adjust_flag,
80
+ fields=fields,
81
+ )
82
+ # Format the result
83
+ logger.info(
84
+ f"Successfully retrieved K-data for {code}, formatting to Markdown.")
85
+ return format_df_to_markdown(df)
86
+
87
+ except NoDataFoundError as e:
88
+ logger.warning(f"NoDataFoundError for {code}: {e}")
89
+ return f"Error: {e}"
90
+ except LoginError as e:
91
+ logger.error(f"LoginError for {code}: {e}")
92
+ return f"Error: Could not connect to data source. {e}"
93
+ except DataSourceError as e:
94
+ logger.error(f"DataSourceError for {code}: {e}")
95
+ return f"Error: An error occurred while fetching data. {e}"
96
+ except ValueError as e:
97
+ logger.warning(f"ValueError processing request for {code}: {e}")
98
+ return f"Error: Invalid input parameter. {e}"
99
+ except Exception as e:
100
+ # Catch-all for unexpected errors
101
+ logger.exception(
102
+ f"Unexpected Exception processing get_historical_k_data for {code}: {e}")
103
+ return f"Error: An unexpected error occurred: {e}"
104
+
105
+ @app.tool()
106
+ def get_stock_basic_info(code: str, fields: Optional[List[str]] = None) -> str:
107
+ """
108
+ Fetches basic information for a given Chinese A-share stock.
109
+
110
+ Args:
111
+ code: The stock code in Baostock format (e.g., 'sh.600000', 'sz.000001').
112
+ fields: Optional list to select specific columns from the available basic info
113
+ (e.g., ['code', 'code_name', 'industry', 'listingDate']).
114
+ If None or empty, returns all available basic info columns from Baostock.
115
+
116
+ Returns:
117
+ A Markdown formatted string containing the basic stock information table,
118
+ or an error message.
119
+ """
120
+ logger.info(
121
+ f"Tool 'get_stock_basic_info' called for {code} (fields={fields})")
122
+ try:
123
+ # Call the injected data source
124
+ # Pass fields along; BaostockDataSource implementation handles selection
125
+ df = active_data_source.get_stock_basic_info(
126
+ code=code, fields=fields)
127
+
128
+ # Format the result (basic info usually small, use default truncation)
129
+ logger.info(
130
+ f"Successfully retrieved basic info for {code}, formatting to Markdown.")
131
+ # Smaller limits for basic info
132
+ return format_df_to_markdown(df)
133
+
134
+ except NoDataFoundError as e:
135
+ logger.warning(f"NoDataFoundError for {code}: {e}")
136
+ return f"Error: {e}"
137
+ except LoginError as e:
138
+ logger.error(f"LoginError for {code}: {e}")
139
+ return f"Error: Could not connect to data source. {e}"
140
+ except DataSourceError as e:
141
+ logger.error(f"DataSourceError for {code}: {e}")
142
+ return f"Error: An error occurred while fetching data. {e}"
143
+ except ValueError as e:
144
+ logger.warning(f"ValueError processing request for {code}: {e}")
145
+ return f"Error: Invalid input parameter or requested field not available. {e}"
146
+ except Exception as e:
147
+ logger.exception(
148
+ f"Unexpected Exception processing get_stock_basic_info for {code}: {e}")
149
+ return f"Error: An unexpected error occurred: {e}"
150
+
151
+ @app.tool()
152
+ def get_dividend_data(code: str, year: str, year_type: str = "report") -> str:
153
+ """
154
+ Fetches dividend information for a given stock code and year.
155
+
156
+ Args:
157
+ code: The stock code in Baostock format (e.g., 'sh.600000', 'sz.000001').
158
+ year: The year to query (e.g., '2023').
159
+ year_type: Type of year. Valid options (from Baostock):
160
+ 'report': Announcement year (预案公告年份)
161
+ 'operate': Ex-dividend year (除权除息年份)
162
+ Defaults to 'report'.
163
+
164
+ Returns:
165
+ A Markdown formatted string containing the dividend data table,
166
+ or an error message.
167
+ """
168
+ logger.info(
169
+ f"Tool 'get_dividend_data' called for {code}, year={year}, year_type={year_type}")
170
+ try:
171
+ # Basic validation
172
+ if year_type not in ['report', 'operate']:
173
+ logger.warning(f"Invalid year_type requested: {year_type}")
174
+ return f"Error: Invalid year_type '{year_type}'. Valid options are: 'report', 'operate'"
175
+ if not year.isdigit() or len(year) != 4:
176
+ logger.warning(f"Invalid year format requested: {year}")
177
+ return f"Error: Invalid year '{year}'. Please provide a 4-digit year."
178
+
179
+ df = active_data_source.get_dividend_data(
180
+ code=code, year=year, year_type=year_type)
181
+ logger.info(
182
+ f"Successfully retrieved dividend data for {code}, year {year}.")
183
+ return format_df_to_markdown(df)
184
+
185
+ except NoDataFoundError as e:
186
+ logger.warning(f"NoDataFoundError for {code}, year {year}: {e}")
187
+ return f"Error: {e}"
188
+ except LoginError as e:
189
+ logger.error(f"LoginError for {code}: {e}")
190
+ return f"Error: Could not connect to data source. {e}"
191
+ except DataSourceError as e:
192
+ logger.error(f"DataSourceError for {code}: {e}")
193
+ return f"Error: An error occurred while fetching data. {e}"
194
+ except ValueError as e:
195
+ logger.warning(f"ValueError processing request for {code}: {e}")
196
+ return f"Error: Invalid input parameter. {e}"
197
+ except Exception as e:
198
+ logger.exception(
199
+ f"Unexpected Exception processing get_dividend_data for {code}: {e}")
200
+ return f"Error: An unexpected error occurred: {e}"
201
+
202
+ @app.tool()
203
+ def get_adjust_factor_data(code: str, start_date: str, end_date: str) -> str:
204
+ """
205
+ Fetches adjustment factor data for a given stock code and date range.
206
+ Uses Baostock's "涨跌幅复权算法" factors. Useful for calculating adjusted prices.
207
+
208
+ Args:
209
+ code: The stock code in Baostock format (e.g., 'sh.600000', 'sz.000001').
210
+ start_date: Start date in 'YYYY-MM-DD' format.
211
+ end_date: End date in 'YYYY-MM-DD' format.
212
+
213
+ Returns:
214
+ A Markdown formatted string containing the adjustment factor data table,
215
+ or an error message.
216
+ """
217
+ logger.info(
218
+ f"Tool 'get_adjust_factor_data' called for {code} ({start_date} to {end_date})")
219
+ try:
220
+ # Basic date validation could be added here if desired
221
+ df = active_data_source.get_adjust_factor_data(
222
+ code=code, start_date=start_date, end_date=end_date)
223
+ logger.info(
224
+ f"Successfully retrieved adjustment factor data for {code}.")
225
+ return format_df_to_markdown(df)
226
+
227
+ except NoDataFoundError as e:
228
+ logger.warning(f"NoDataFoundError for {code}: {e}")
229
+ return f"Error: {e}"
230
+ except LoginError as e:
231
+ logger.error(f"LoginError for {code}: {e}")
232
+ return f"Error: Could not connect to data source. {e}"
233
+ except DataSourceError as e:
234
+ logger.error(f"DataSourceError for {code}: {e}")
235
+ return f"Error: An error occurred while fetching data. {e}"
236
+ except ValueError as e:
237
+ logger.warning(f"ValueError processing request for {code}: {e}")
238
+ return f"Error: Invalid input parameter. {e}"
239
+ except Exception as e:
240
+ logger.exception(
241
+ f"Unexpected Exception processing get_adjust_factor_data for {code}: {e}")
242
+ return f"Error: An unexpected error occurred: {e}"
utils.py ADDED
@@ -0,0 +1,69 @@
1
+ # Utility functions, including the Baostock login context manager and logging setup
2
+ import baostock as bs
3
+ import os
4
+ import sys
5
+ import logging
6
+ from contextlib import contextmanager
7
+ from .data_source_interface import LoginError
8
+
9
+ # --- Logging Setup ---
10
+ def setup_logging(level=logging.INFO):
11
+ """Configures basic logging for the application."""
12
+ logging.basicConfig(
13
+ level=level,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
15
+ datefmt='%Y-%m-%d %H:%M:%S'
16
+ )
17
+ # Optionally silence logs from dependencies if they are too verbose
18
+ # logging.getLogger("mcp").setLevel(logging.WARNING)
19
+
20
+ # Get a logger instance for this module (optional, but good practice)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # --- Baostock Context Manager ---
24
+ @contextmanager
25
+ def baostock_login_context():
26
+ """Context manager to handle Baostock login and logout, suppressing stdout messages."""
27
+ # Redirect stdout to suppress login/logout messages
28
+ original_stdout_fd = sys.stdout.fileno()
29
+ saved_stdout_fd = os.dup(original_stdout_fd)
30
+ devnull_fd = os.open(os.devnull, os.O_WRONLY)
31
+
32
+ os.dup2(devnull_fd, original_stdout_fd)
33
+ os.close(devnull_fd)
34
+
35
+ logger.debug("Attempting Baostock login...")
36
+ lg = bs.login()
37
+ logger.debug(f"Login result: code={lg.error_code}, msg={lg.error_msg}")
38
+
39
+ # Restore stdout
40
+ os.dup2(saved_stdout_fd, original_stdout_fd)
41
+ os.close(saved_stdout_fd)
42
+
43
+ if lg.error_code != '0':
44
+ # Log error before raising
45
+ logger.error(f"Baostock login failed: {lg.error_msg}")
46
+ raise LoginError(f"Baostock login failed: {lg.error_msg}")
47
+
48
+ logger.info("Baostock login successful.")
49
+ try:
50
+ yield # API calls happen here
51
+ finally:
52
+ # Redirect stdout again for logout
53
+ original_stdout_fd = sys.stdout.fileno()
54
+ saved_stdout_fd = os.dup(original_stdout_fd)
55
+ devnull_fd = os.open(os.devnull, os.O_WRONLY)
56
+
57
+ os.dup2(devnull_fd, original_stdout_fd)
58
+ os.close(devnull_fd)
59
+
60
+ logger.debug("Attempting Baostock logout...")
61
+ bs.logout()
62
+ logger.debug("Logout completed.")
63
+
64
+ # Restore stdout
65
+ os.dup2(saved_stdout_fd, original_stdout_fd)
66
+ os.close(saved_stdout_fd)
67
+ logger.info("Baostock logout successful.")
68
+
69
+ # You can add other utility functions or classes here if needed