@youhaozhao/cninfo-mcp 1.2.0 → 1.4.0

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.
@@ -1,268 +1,194 @@
1
- #!/usr/bin/env python3
2
- """
3
- 巨潮资讯 MCP 服务器
4
- 用于查询和下载 A 股年度报告的 MCP 工具服务
5
- """
6
-
7
- import os
8
- import sys
9
- from typing import Optional
10
-
11
- # 将当前目录加入模块搜索路径
12
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
13
-
14
- from mcp.server import FastMCP
15
- from spider import (
16
- query_annual_reports,
17
- download_annual_reports,
18
- query_prospectus,
19
- download_prospectus,
20
- saving_path,
21
- )
22
-
23
- # 创建 MCP 服务器实例
24
- mcp = FastMCP(
25
- name="cninfo-server",
26
- instructions="CNINFO annual reports server - Query and download Chinese listed companies' annual reports from cninfo.com.cn",
27
- )
28
-
29
-
30
- @mcp.tool()
31
- def query_annual_reports_tool(stock_code: str, year: Optional[int] = None) -> dict:
32
- """
33
- Query annual reports for a Chinese listed company
34
-
35
- Args:
36
- stock_code: Stock code (e.g., '000888' for峨眉山, '688777' for 中科德芯)
37
- year: Optional year to filter (e.g., 2024). If not provided, returns all available years
38
-
39
- Returns:
40
- Dictionary containing:
41
- - success: Boolean indicating if the query was successful
42
- - stock_code: The queried stock code
43
- - year: The filtered year (if any)
44
- - count: Number of reports found
45
- - reports: List of report details (announcementTitle, announcementTime, secCode, secName)
46
- """
47
- try:
48
- reports = query_annual_reports(stock_code, year)
49
-
50
- if not reports:
51
- return {
52
- "success": False,
53
- "stock_code": stock_code,
54
- "year": year,
55
- "count": 0,
56
- "reports": [],
57
- "message": f"No annual reports found for stock {stock_code}"
58
- + (f" in year {year}" if year else ""),
59
- }
60
-
61
- # 提取关键字段
62
- base_url = "https://static.cninfo.com.cn/"
63
- report_details = []
64
- for report in reports:
65
- adj = report.get("adjunctUrl", "")
66
- report_details.append(
67
- {
68
- "announcementTitle": report.get("announcementTitle", ""),
69
- "announcementTime": report.get("announcementTime", ""),
70
- "secCode": report.get("secCode", ""),
71
- "secName": report.get("secName", ""),
72
- "adjunctUrl": base_url + adj if adj else "",
73
- }
74
- )
75
-
76
- return {
77
- "success": True,
78
- "stock_code": stock_code,
79
- "year": year,
80
- "count": len(reports),
81
- "reports": report_details,
82
- "message": f"Found {len(reports)} annual report(s)"
83
- + (f" for year {year}" if year else ""),
84
- }
85
-
86
- except Exception as e:
87
- return {
88
- "success": False,
89
- "stock_code": stock_code,
90
- "year": year,
91
- "count": 0,
92
- "reports": [],
93
- "error": str(e),
94
- "message": f"Error querying annual reports: {str(e)}",
95
- }
96
-
97
-
98
- @mcp.tool()
99
- def download_annual_reports_tool(
100
- stock_code: str, year: Optional[int] = None, save_path: Optional[str] = None
101
- ) -> dict:
102
- """
103
- Download annual reports for a Chinese listed company
104
-
105
- Args:
106
- stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
107
- year: Optional year to filter (e.g., 2024). If not provided, downloads all available years
108
- save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
109
-
110
- Returns:
111
- Dictionary containing:
112
- - success: Boolean indicating if download was successful
113
- - stock_code: The stock code
114
- - year: The filtered year (if any)
115
- - downloaded: Number of files downloaded
116
- - path: Directory where files were saved
117
- - message: Status message
118
- """
119
- try:
120
- output_dir = save_path or saving_path
121
- os.makedirs(output_dir, exist_ok=True)
122
-
123
- result = download_annual_reports(stock_code, year, save_path=output_dir)
124
- result["stock_code"] = stock_code
125
- result["year"] = year
126
-
127
- return result
128
-
129
- except Exception as e:
130
- return {
131
- "success": False,
132
- "stock_code": stock_code,
133
- "year": year,
134
- "downloaded": 0,
135
- "path": save_path or saving_path,
136
- "error": str(e),
137
- "message": f"Error downloading annual reports: {str(e)}",
138
- }
139
-
140
-
141
- @mcp.tool()
142
- def query_prospectus_tool(stock_code: str) -> dict:
143
- """
144
- Query prospectus documents for a Chinese listed company
145
-
146
- Args:
147
- stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
148
-
149
- Returns:
150
- Dictionary containing:
151
- - success: Boolean indicating if the query was successful
152
- - stock_code: The queried stock code
153
- - count: Number of documents found
154
- - reports: List of document details (announcementTitle, announcementTime, secCode, secName)
155
- """
156
- try:
157
- reports = query_prospectus(stock_code)
158
-
159
- if not reports:
160
- return {
161
- "success": False,
162
- "stock_code": stock_code,
163
- "count": 0,
164
- "reports": [],
165
- "message": f"No prospectus found for stock {stock_code}",
166
- }
167
-
168
- base_url = "https://static.cninfo.com.cn/"
169
- report_details = [
170
- {
171
- "announcementTitle": r.get("announcementTitle", ""),
172
- "announcementTime": r.get("announcementTime", ""),
173
- "secCode": r.get("secCode", ""),
174
- "secName": r.get("secName", ""),
175
- "adjunctUrl": base_url + r.get("adjunctUrl", "")
176
- if r.get("adjunctUrl")
177
- else "",
178
- }
179
- for r in reports
180
- ]
181
-
182
- return {
183
- "success": True,
184
- "stock_code": stock_code,
185
- "count": len(reports),
186
- "reports": report_details,
187
- "message": f"Found {len(reports)} prospectus document(s)",
188
- }
189
-
190
- except Exception as e:
191
- return {
192
- "success": False,
193
- "stock_code": stock_code,
194
- "count": 0,
195
- "reports": [],
196
- "error": str(e),
197
- "message": f"Error querying prospectus: {str(e)}",
198
- }
199
-
200
-
201
- @mcp.tool()
202
- def download_prospectus_tool(stock_code: str, save_path: Optional[str] = None) -> dict:
203
- """
204
- Download prospectus documents for a Chinese listed company
205
-
206
- Args:
207
- stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
208
- save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
209
-
210
- Returns:
211
- Dictionary containing:
212
- - success: Boolean indicating if download was successful
213
- - stock_code: The stock code
214
- - downloaded: Number of files downloaded
215
- - path: Directory where files were saved
216
- - message: Status message
217
- """
218
- try:
219
- output_dir = save_path or saving_path
220
- os.makedirs(output_dir, exist_ok=True)
221
-
222
- result = download_prospectus(stock_code, save_path=output_dir)
223
- result["stock_code"] = stock_code
224
-
225
- return result
226
-
227
- except Exception as e:
228
- return {
229
- "success": False,
230
- "stock_code": stock_code,
231
- "downloaded": 0,
232
- "path": save_path or saving_path,
233
- "error": str(e),
234
- "message": f"Error downloading prospectus: {str(e)}",
235
- }
236
-
237
-
238
- @mcp.resource("annual-reports-list://{stock_code}")
239
- def get_annual_reports_list(stock_code: str) -> str:
240
- """返回指定股票代码的年度报告格式化列表"""
241
- try:
242
- reports = query_annual_reports(stock_code)
243
-
244
- if not reports:
245
- return f"No annual reports found for stock {stock_code}"
246
-
247
- output = [f"Annual Reports for {stock_code}:", "=" * 60]
248
-
249
- for report in reports:
250
- title = report.get("announcementTitle", "N/A")
251
- time = report.get("announcementTime", "N/A")
252
- name = report.get("secName", "N/A")
253
- output.append(f"\n📄 {title}")
254
- output.append(f" Company: {name}")
255
- output.append(f" Date: {time}")
256
-
257
- output.append("\n" + "=" * 60)
258
- output.append(f"Total: {len(reports)} report(s)")
259
-
260
- return "\n".join(output)
261
-
262
- except Exception as e:
263
- return f"Error retrieving annual reports: {str(e)}"
264
-
265
-
266
- if __name__ == "__main__":
267
- # 以 stdio 方式运行服务器
268
- mcp.run()
1
+ #!/usr/bin/env python3
2
+ """
3
+ 巨潮资讯 MCP 服务器
4
+ 用于查询和下载 A 股定期报告、招股书的 MCP 工具服务
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from typing import Optional
11
+
12
+ # 将当前目录加入模块搜索路径
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+
15
+ from mcp.server.mcpserver import MCPServer
16
+ from spider import (
17
+ query_reports,
18
+ download_reports,
19
+ format_reports,
20
+ saving_path,
21
+ supported_report_types,
22
+ )
23
+
24
+ def _package_version(default: str = "0.0.0") -> str:
25
+ """从 package.json 读取版本,避免与 npm 包版本各写一份而漂移。
26
+
27
+ package.json 始终随 npm 包发布(不受 files 白名单影响),因此在已安装的
28
+ 包里也能读到。读取失败时退回 default,不让版本号问题挡住服务器启动。
29
+ """
30
+ manifest = os.path.join(
31
+ os.path.dirname(os.path.abspath(__file__)), "..", "package.json"
32
+ )
33
+ try:
34
+ with open(manifest, encoding="utf-8") as fh:
35
+ return json.load(fh).get("version") or default
36
+ except (OSError, ValueError):
37
+ return default
38
+
39
+
40
+ # 创建 MCP 服务器实例
41
+ mcp = MCPServer(
42
+ name="cninfo-server",
43
+ instructions="CNINFO reports server - Query and download Chinese listed companies' periodic reports from cninfo.com.cn",
44
+ version=_package_version(),
45
+ )
46
+
47
+
48
+ def _supported_report_types_text() -> str:
49
+ return ", ".join(supported_report_types().keys())
50
+
51
+
52
+ @mcp.tool()
53
+ def query_annual_reports_tool(
54
+ stock_code: str, year: Optional[int] = None, report_type: str = "annual"
55
+ ) -> dict:
56
+ """
57
+ Query periodic reports for a Chinese listed company.
58
+
59
+ Args:
60
+ stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
61
+ year: Optional year to filter (e.g., 2024). If not provided, returns all available years
62
+ report_type: Optional report type. Supported values: annual, semiannual, q1, q3, prospectus. Defaults to annual for backward compatibility.
63
+
64
+ Returns:
65
+ Dictionary containing:
66
+ - success: Boolean indicating if the query was successful
67
+ - stock_code: The queried stock code
68
+ - report_type: The requested report type
69
+ - year: The filtered year (if any)
70
+ - count: Number of reports found
71
+ - reports: List of report details (announcementTitle, announcementTime, secCode, secName, adjunctUrl)
72
+ """
73
+ try:
74
+ reports = query_reports(stock_code, report_type, year)
75
+
76
+ if not reports:
77
+ return {
78
+ "success": False,
79
+ "stock_code": stock_code,
80
+ "report_type": report_type,
81
+ "year": year,
82
+ "count": 0,
83
+ "reports": [],
84
+ "message": f"No {report_type} reports found for stock {stock_code}"
85
+ + (f" in year {year}" if year else ""),
86
+ }
87
+
88
+ return {
89
+ "success": True,
90
+ "stock_code": stock_code,
91
+ "report_type": report_type,
92
+ "year": year,
93
+ "count": len(reports),
94
+ "reports": format_reports(reports),
95
+ "message": f"Found {len(reports)} {report_type} report(s)"
96
+ + (f" for year {year}" if year else ""),
97
+ }
98
+
99
+ except Exception as e:
100
+ return {
101
+ "success": False,
102
+ "stock_code": stock_code,
103
+ "report_type": report_type,
104
+ "year": year,
105
+ "count": 0,
106
+ "reports": [],
107
+ "error": str(e),
108
+ "message": f"Error querying reports: {str(e)}. Supported report_type values: {_supported_report_types_text()}",
109
+ }
110
+
111
+
112
+ @mcp.tool()
113
+ def download_annual_reports_tool(
114
+ stock_code: str,
115
+ year: Optional[int] = None,
116
+ save_path: Optional[str] = None,
117
+ report_type: str = "annual",
118
+ ) -> dict:
119
+ """
120
+ Download periodic reports for a Chinese listed company.
121
+
122
+ Args:
123
+ stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
124
+ year: Optional year to filter (e.g., 2024). If not provided, downloads all available years
125
+ save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
126
+ report_type: Optional report type. Supported values: annual, semiannual, q1, q3, prospectus. Defaults to annual for backward compatibility.
127
+
128
+ Returns:
129
+ Dictionary containing:
130
+ - success: Boolean indicating if download was successful
131
+ - stock_code: The stock code
132
+ - report_type: The requested report type
133
+ - year: The filtered year (if any)
134
+ - downloaded: Number of files downloaded
135
+ - path: Directory where files were saved
136
+ - message: Status message
137
+ """
138
+ try:
139
+ output_dir = save_path or saving_path
140
+ os.makedirs(output_dir, exist_ok=True)
141
+
142
+ result = download_reports(
143
+ stock_code, report_type, year=year, save_path=output_dir
144
+ )
145
+ result["stock_code"] = stock_code
146
+ result["report_type"] = report_type
147
+ result["year"] = year
148
+
149
+ return result
150
+
151
+ except Exception as e:
152
+ return {
153
+ "success": False,
154
+ "stock_code": stock_code,
155
+ "report_type": report_type,
156
+ "year": year,
157
+ "downloaded": 0,
158
+ "path": save_path or saving_path,
159
+ "error": str(e),
160
+ "message": f"Error downloading reports: {str(e)}. Supported report_type values: {_supported_report_types_text()}",
161
+ }
162
+
163
+
164
+ @mcp.resource("annual-reports-list://{stock_code}")
165
+ def get_annual_reports_list(stock_code: str) -> str:
166
+ """返回指定股票代码的年度报告格式化列表"""
167
+ try:
168
+ reports = query_reports(stock_code, "annual")
169
+
170
+ if not reports:
171
+ return f"No annual reports found for stock {stock_code}"
172
+
173
+ output = [f"Annual Reports for {stock_code}:", "=" * 60]
174
+
175
+ for report in reports:
176
+ title = report.get("announcementTitle", "N/A")
177
+ time = report.get("announcementTime", "N/A")
178
+ name = report.get("secName", "N/A")
179
+ output.append(f"\n📄 {title}")
180
+ output.append(f" Company: {name}")
181
+ output.append(f" Date: {time}")
182
+
183
+ output.append("\n" + "=" * 60)
184
+ output.append(f"Total: {len(reports)} report(s)")
185
+
186
+ return "\n".join(output)
187
+
188
+ except Exception as e:
189
+ return f"Error retrieving annual reports: {str(e)}"
190
+
191
+
192
+ if __name__ == "__main__":
193
+ # 以 stdio 方式运行服务器
194
+ mcp.run()
@@ -1,5 +1,5 @@
1
1
  # MCP Server dependencies
2
- mcp>=1.0.0
2
+ mcp~=2.1.1
3
3
 
4
4
  # Existing dependencies
5
5
  requests>=2.31.0