@youhaozhao/cninfo-mcp 1.2.0 → 1.3.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.
package/README.md CHANGED
@@ -1,54 +1,64 @@
1
- # cninfo-mcp
2
-
3
- [![npm version](https://img.shields.io/npm/v/@youhaozhao/cninfo-mcp)](https://www.npmjs.com/package/@youhaozhao/cninfo-mcp)
4
-
5
- 通过 MCP 协议查询和下载巨潮资讯网上市公司年报 PDF 的工具,适用于 Claude Desktop。
6
-
7
- ## 使用方法
8
-
9
- 在 Claude Desktop / Claude Code 配置文件中添加:
10
-
11
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
12
-
13
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
14
-
15
- ```json
16
- {
17
- "mcpServers": {
18
- "cninfo": {
19
- "command": "npx",
20
- "args": ["-y", "@youhaozhao/cninfo-mcp"]
21
- }
22
- }
23
- }
24
- ```
25
-
26
- 重启 Claude Desktop 后即可使用。
27
-
28
- ## 可用工具
29
-
30
- - **`query_annual_reports_tool`** — 查询年报列表,参数:股票代码(必填)、年份(可选)
31
- - **`download_annual_reports_tool`** — 下载年报 PDF,参数:股票代码(必填)、年份(可选)
32
-
33
- 示例对话:
34
-
35
- ```
36
- 查询 000888 2024 年报
37
- 下载 688777 的年报
38
- 查询 920185 的年报 # 北交所,新旧代码(如 835185)均可
39
- ```
40
-
41
- ## 系统要求
42
-
43
- - Node.js 18+
44
- - Python 3.10+(Python 依赖会自动安装)
45
-
46
- ## 数据来源
47
-
48
- [巨潮资讯网](https://www.cninfo.com.cn) 支持沪深两市(主板、创业板、科创板)及北京证券交易所(北交所)
49
-
50
- ## Credits
51
-
52
- 爬虫逻辑基于 [gaodechen/cninfo_process](https://github.com/gaodechen/cninfo_process)。
53
-
54
-
1
+ # cninfo-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@youhaozhao/cninfo-mcp)](https://www.npmjs.com/package/@youhaozhao/cninfo-mcp)
4
+
5
+ 通过 MCP 协议查询和下载巨潮资讯网上市公司定期报告及招股书 PDF 的工具,适用于 Claude Desktop / Claude Code
6
+
7
+ ## 使用方法
8
+
9
+ 在 Claude Desktop / Claude Code 配置文件中添加:
10
+
11
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
12
+
13
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "cninfo": {
19
+ "command": "npx",
20
+ "args": ["-y", "@youhaozhao/cninfo-mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ 重启 Claude Desktop 后即可使用。
27
+
28
+ ## 可用工具
29
+
30
+ - **`query_annual_reports_tool`** — 查询报告列表,参数:股票代码(必填)、年份(可选)、报告类型(可选,默认 `annual`)
31
+ - **`download_annual_reports_tool`** — 下载报告 PDF,参数:股票代码(必填)、年份(可选)、保存路径(可选)、报告类型(可选,默认 `annual`)
32
+
33
+ 支持的 `report_type`:
34
+
35
+ - `annual` — 年度报告 / 年报
36
+ - `semiannual` 半年度报告 / 半年报 / 中报
37
+ - `q1` — 第一季度报告 / 一季报
38
+ - `q3` 第三季度报告 / 三季报
39
+ - `prospectus` — 招股书 / 招股说明书 / 招股意向书(招股书无固定年份,省略年份参数即可)
40
+
41
+ 示例对话:
42
+
43
+ ```
44
+ 查询 000888 2024 年报
45
+ 查询 000001 的 2024 半年报
46
+ 查询 600519 的 2024 一季报
47
+ 下载 300750 的 2023 三季报
48
+ 下载 688777 的年报
49
+ 查询 920185 的年报 # 北交所,新旧代码(如 835185)均可
50
+ 查询 688777 的招股书
51
+ ```
52
+
53
+ ## 系统要求
54
+
55
+ - Node.js 18+
56
+ - Python 3.10+(Python 依赖会自动安装)
57
+
58
+ ## 数据来源
59
+
60
+ [巨潮资讯网](https://www.cninfo.com.cn) — 支持沪深两市(主板、创业板、科创板)及北京证券交易所(北交所)
61
+
62
+ ## Credits
63
+
64
+ 爬虫逻辑基于 [gaodechen/cninfo_process](https://github.com/gaodechen/cninfo_process)。
package/package.json CHANGED
@@ -1,15 +1,28 @@
1
1
  {
2
2
  "name": "@youhaozhao/cninfo-mcp",
3
- "version": "1.2.0",
4
- "description": "MCP Server for querying and downloading Chinese listed companies' annual reports from CNINFO (巨潮资讯网)",
3
+ "version": "1.3.0",
4
+ "description": "MCP Server for querying and downloading Chinese listed companies' periodic reports from CNINFO (巨潮资讯网)",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "mcp-server",
8
8
  "cninfo",
9
9
  "chinese-stock",
10
10
  "annual-reports",
11
+ "quarterly-reports",
12
+ "semiannual-reports",
13
+ "periodic-reports",
11
14
  "finance",
12
- "python"
15
+ "python",
16
+ "巨潮",
17
+ "巨潮资讯",
18
+ "定期报告",
19
+ "年报",
20
+ "半年报",
21
+ "季报",
22
+ "招股书",
23
+ "A股",
24
+ "上市公司",
25
+ "财报"
13
26
  ],
14
27
  "homepage": "https://github.com/youhaozhao/cninfo-mcp#readme",
15
28
  "repository": {
@@ -27,7 +40,7 @@
27
40
  "postinstall": "node scripts/install-python-deps.js",
28
41
  "start": "node bin/cninfo-mcp.js",
29
42
  "dev": "node bin/cninfo-mcp.js",
30
- "test": "echo \"No tests configured\""
43
+ "test": "python3 -m pytest python/test_spider.py -q"
31
44
  },
32
45
  "dependencies": {
33
46
  "spawn-please": "^2.0.2"
@@ -1,268 +1,193 @@
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 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_reports,
17
+ download_reports,
18
+ saving_path,
19
+ supported_report_types,
20
+ )
21
+
22
+ # 创建 MCP 服务器实例
23
+ mcp = FastMCP(
24
+ name="cninfo-server",
25
+ instructions="CNINFO reports server - Query and download Chinese listed companies' periodic reports from cninfo.com.cn",
26
+ )
27
+
28
+
29
+ def _format_reports(reports: list) -> list:
30
+ """提取 MCP 返回中稳定、有用的公告字段。"""
31
+ base_url = "https://static.cninfo.com.cn/"
32
+ report_details = []
33
+ for report in reports:
34
+ adj = report.get("adjunctUrl", "")
35
+ report_details.append(
36
+ {
37
+ "announcementTitle": report.get("announcementTitle", ""),
38
+ "announcementTime": report.get("announcementTime", ""),
39
+ "secCode": report.get("secCode", ""),
40
+ "secName": report.get("secName", ""),
41
+ "adjunctUrl": base_url + adj if adj else "",
42
+ }
43
+ )
44
+ return report_details
45
+
46
+
47
+ def _supported_report_types_text() -> str:
48
+ return ", ".join(supported_report_types().keys())
49
+
50
+
51
+ @mcp.tool()
52
+ def query_annual_reports_tool(
53
+ stock_code: str, year: Optional[int] = None, report_type: str = "annual"
54
+ ) -> dict:
55
+ """
56
+ Query periodic reports for a Chinese listed company.
57
+
58
+ Args:
59
+ stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
60
+ year: Optional year to filter (e.g., 2024). If not provided, returns all available years
61
+ report_type: Optional report type. Supported values: annual, semiannual, q1, q3, prospectus. Defaults to annual for backward compatibility.
62
+
63
+ Returns:
64
+ Dictionary containing:
65
+ - success: Boolean indicating if the query was successful
66
+ - stock_code: The queried stock code
67
+ - report_type: The requested report type
68
+ - year: The filtered year (if any)
69
+ - count: Number of reports found
70
+ - reports: List of report details (announcementTitle, announcementTime, secCode, secName, adjunctUrl)
71
+ """
72
+ try:
73
+ reports = query_reports(stock_code, report_type, year)
74
+
75
+ if not reports:
76
+ return {
77
+ "success": False,
78
+ "stock_code": stock_code,
79
+ "report_type": report_type,
80
+ "year": year,
81
+ "count": 0,
82
+ "reports": [],
83
+ "message": f"No {report_type} reports found for stock {stock_code}"
84
+ + (f" in year {year}" if year else ""),
85
+ }
86
+
87
+ return {
88
+ "success": True,
89
+ "stock_code": stock_code,
90
+ "report_type": report_type,
91
+ "year": year,
92
+ "count": len(reports),
93
+ "reports": _format_reports(reports),
94
+ "message": f"Found {len(reports)} {report_type} report(s)"
95
+ + (f" for year {year}" if year else ""),
96
+ }
97
+
98
+ except Exception as e:
99
+ return {
100
+ "success": False,
101
+ "stock_code": stock_code,
102
+ "report_type": report_type,
103
+ "year": year,
104
+ "count": 0,
105
+ "reports": [],
106
+ "error": str(e),
107
+ "message": f"Error querying reports: {str(e)}. Supported report_type values: {_supported_report_types_text()}",
108
+ }
109
+
110
+
111
+ @mcp.tool()
112
+ def download_annual_reports_tool(
113
+ stock_code: str,
114
+ year: Optional[int] = None,
115
+ save_path: Optional[str] = None,
116
+ report_type: str = "annual",
117
+ ) -> dict:
118
+ """
119
+ Download periodic reports for a Chinese listed company.
120
+
121
+ Args:
122
+ stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
123
+ year: Optional year to filter (e.g., 2024). If not provided, downloads all available years
124
+ save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
125
+ report_type: Optional report type. Supported values: annual, semiannual, q1, q3, prospectus. Defaults to annual for backward compatibility.
126
+
127
+ Returns:
128
+ Dictionary containing:
129
+ - success: Boolean indicating if download was successful
130
+ - stock_code: The stock code
131
+ - report_type: The requested report type
132
+ - year: The filtered year (if any)
133
+ - downloaded: Number of files downloaded
134
+ - path: Directory where files were saved
135
+ - message: Status message
136
+ """
137
+ try:
138
+ output_dir = save_path or saving_path
139
+ os.makedirs(output_dir, exist_ok=True)
140
+
141
+ result = download_reports(
142
+ stock_code, report_type, year=year, save_path=output_dir
143
+ )
144
+ result["stock_code"] = stock_code
145
+ result["report_type"] = report_type
146
+ result["year"] = year
147
+
148
+ return result
149
+
150
+ except Exception as e:
151
+ return {
152
+ "success": False,
153
+ "stock_code": stock_code,
154
+ "report_type": report_type,
155
+ "year": year,
156
+ "downloaded": 0,
157
+ "path": save_path or saving_path,
158
+ "error": str(e),
159
+ "message": f"Error downloading reports: {str(e)}. Supported report_type values: {_supported_report_types_text()}",
160
+ }
161
+
162
+
163
+ @mcp.resource("annual-reports-list://{stock_code}")
164
+ def get_annual_reports_list(stock_code: str) -> str:
165
+ """返回指定股票代码的年度报告格式化列表"""
166
+ try:
167
+ reports = query_reports(stock_code, "annual")
168
+
169
+ if not reports:
170
+ return f"No annual reports found for stock {stock_code}"
171
+
172
+ output = [f"Annual Reports for {stock_code}:", "=" * 60]
173
+
174
+ for report in reports:
175
+ title = report.get("announcementTitle", "N/A")
176
+ time = report.get("announcementTime", "N/A")
177
+ name = report.get("secName", "N/A")
178
+ output.append(f"\n📄 {title}")
179
+ output.append(f" Company: {name}")
180
+ output.append(f" Date: {time}")
181
+
182
+ output.append("\n" + "=" * 60)
183
+ output.append(f"Total: {len(reports)} report(s)")
184
+
185
+ return "\n".join(output)
186
+
187
+ except Exception as e:
188
+ return f"Error retrieving annual reports: {str(e)}"
189
+
190
+
191
+ if __name__ == "__main__":
192
+ # 以 stdio 方式运行服务器
193
+ mcp.run()