@youhaozhao/cninfo-mcp 1.3.0 → 1.4.1
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 +4 -1
- package/bin/cninfo-mcp.js +5 -2
- package/package.json +2 -1
- package/python/check_deps.py +89 -0
- package/python/mcp_server.py +22 -21
- package/python/requirements.txt +1 -1
- package/python/spider.py +21 -0
- package/scripts/install-python-deps.js +5 -2
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
通过 MCP 协议查询和下载巨潮资讯网上市公司定期报告及招股书 PDF 的工具,适用于 Claude Desktop / Claude Code。
|
|
6
6
|
|
|
7
|
+
## 并发限制
|
|
8
|
+
巨潮资讯网后端禁止大量并发,推荐将并发数设置为 4 以防止后端返回大量 403 导致 IP 短暂被封
|
|
9
|
+
|
|
7
10
|
## 使用方法
|
|
8
11
|
|
|
9
12
|
在 Claude Desktop / Claude Code 配置文件中添加:
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
## 系统要求
|
|
54
57
|
|
|
55
58
|
- Node.js 18+
|
|
56
|
-
- Python 3.10+(Python
|
|
59
|
+
- Python 3.10+(Python 依赖会自动安装;需要 MCP Python SDK v2,旧环境会在下次启动时自动升级)
|
|
57
60
|
|
|
58
61
|
## 数据来源
|
|
59
62
|
|
package/bin/cninfo-mcp.js
CHANGED
|
@@ -73,6 +73,9 @@ async function ensureVenv(systemPythonCmd) {
|
|
|
73
73
|
return venvPython;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// 依赖探针:校验 venv 是否满足 requirements.txt 的全部约束
|
|
77
|
+
const DEPS_CHECK = path.join(__dirname, "..", "python", "check_deps.py");
|
|
78
|
+
|
|
76
79
|
// 检查并安装 Python 依赖(使用 venv 中的 python)
|
|
77
80
|
async function ensureDependencies(venvPython) {
|
|
78
81
|
const requirementsPath = PYTHON_REQUIREMENTS;
|
|
@@ -83,8 +86,8 @@ async function ensureDependencies(venvPython) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
try {
|
|
86
|
-
//
|
|
87
|
-
await spawnAsync(venvPython, [
|
|
89
|
+
// 校验依赖是否满足约束(不满足时抛错,转入下方安装流程)
|
|
90
|
+
await spawnAsync(venvPython, [DEPS_CHECK]);
|
|
88
91
|
} catch (error) {
|
|
89
92
|
// 未安装,执行安装
|
|
90
93
|
console.error("Installing Python dependencies...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youhaozhao/cninfo-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "MCP Server for querying and downloading Chinese listed companies' periodic reports from CNINFO (巨潮资讯网)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"files": [
|
|
60
60
|
"bin/",
|
|
61
61
|
"python/mcp_server.py",
|
|
62
|
+
"python/check_deps.py",
|
|
62
63
|
"python/spider.py",
|
|
63
64
|
"python/requirements.txt",
|
|
64
65
|
"scripts/install-python-deps.js",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""校验当前解释器是否满足 requirements.txt 的全部约束。
|
|
3
|
+
|
|
4
|
+
退出码 0 表示全部满足,调用方可以跳过 pip 安装;非 0 表示需要执行
|
|
5
|
+
`pip install -r requirements.txt`。
|
|
6
|
+
|
|
7
|
+
这个脚本只是一个优化——用来省掉一次很慢的 pip 调用,pip 自身才是约束的
|
|
8
|
+
最终权威。因此所有无法确定的情况(依赖缺失、版本不符、约束解析失败、
|
|
9
|
+
连比较版本号的库都找不到)一律返回非 0,让 pip 去做真正的判断。宁可多跑
|
|
10
|
+
一次 pip,也不能放行一个不满足约束的环境。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
REQUIREMENTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_requirement_parser():
|
|
21
|
+
"""返回 packaging 的 Requirement 类,找不到时返回 None。
|
|
22
|
+
|
|
23
|
+
packaging 不是本项目的直接依赖,全新 venv 里通常不存在,因此回退到 pip
|
|
24
|
+
自带的 vendored 副本。部分发行版(如 Debian)会去除 vendoring,那时两者
|
|
25
|
+
都不可用,由调用方按“未满足”处理。
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
from packaging.requirements import Requirement
|
|
29
|
+
|
|
30
|
+
return Requirement
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
try:
|
|
34
|
+
from pip._vendor.packaging.requirements import Requirement
|
|
35
|
+
|
|
36
|
+
return Requirement
|
|
37
|
+
except ImportError:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unsatisfied(requirements_path=None):
|
|
42
|
+
"""返回未满足的约束列表;无法判断时抛出异常由调用方兜底。"""
|
|
43
|
+
# 默认值在调用时解析,而不是在 def 时绑定,便于测试替换 REQUIREMENTS
|
|
44
|
+
if requirements_path is None:
|
|
45
|
+
requirements_path = REQUIREMENTS
|
|
46
|
+
|
|
47
|
+
Requirement = _load_requirement_parser()
|
|
48
|
+
if Requirement is None:
|
|
49
|
+
raise RuntimeError("packaging is unavailable; cannot verify requirements")
|
|
50
|
+
|
|
51
|
+
missing = []
|
|
52
|
+
with open(requirements_path, encoding="utf-8") as fh:
|
|
53
|
+
for raw in fh:
|
|
54
|
+
line = raw.split("#", 1)[0].strip()
|
|
55
|
+
# 跳过空行,以及 -r/-e/--index-url 这类非约束指令
|
|
56
|
+
if not line or line.startswith("-"):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
req = Requirement(line)
|
|
60
|
+
try:
|
|
61
|
+
installed = version(req.name)
|
|
62
|
+
except PackageNotFoundError:
|
|
63
|
+
missing.append(f"{req.name} (not installed)")
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# prereleases=True:已装的预发布版本若落在区间内也算满足,
|
|
67
|
+
# 避免把 pip 已经接受的环境反复重装。
|
|
68
|
+
if not req.specifier.contains(installed, prereleases=True):
|
|
69
|
+
missing.append(f"{req.name} {installed} does not satisfy {req.specifier}")
|
|
70
|
+
|
|
71
|
+
return missing
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
try:
|
|
76
|
+
missing = unsatisfied()
|
|
77
|
+
except Exception as exc: # 解析失败、文件缺失等一律按“需要安装”处理
|
|
78
|
+
print(f"dependency check inconclusive: {exc}", file=sys.stderr)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
if missing:
|
|
82
|
+
for item in missing:
|
|
83
|
+
print(f"dependency not satisfied: {item}", file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
raise SystemExit(main())
|
package/python/mcp_server.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
用于查询和下载 A 股定期报告、招股书的 MCP 工具服务
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
import sys
|
|
9
10
|
from typing import Optional
|
|
@@ -11,39 +12,39 @@ from typing import Optional
|
|
|
11
12
|
# 将当前目录加入模块搜索路径
|
|
12
13
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
14
|
|
|
14
|
-
from mcp.server import
|
|
15
|
+
from mcp.server.mcpserver import MCPServer
|
|
15
16
|
from spider import (
|
|
16
17
|
query_reports,
|
|
17
18
|
download_reports,
|
|
19
|
+
format_reports,
|
|
18
20
|
saving_path,
|
|
19
21
|
supported_report_types,
|
|
20
22
|
)
|
|
21
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
|
+
|
|
22
40
|
# 创建 MCP 服务器实例
|
|
23
|
-
mcp =
|
|
41
|
+
mcp = MCPServer(
|
|
24
42
|
name="cninfo-server",
|
|
25
43
|
instructions="CNINFO reports server - Query and download Chinese listed companies' periodic reports from cninfo.com.cn",
|
|
44
|
+
version=_package_version(),
|
|
26
45
|
)
|
|
27
46
|
|
|
28
47
|
|
|
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
48
|
def _supported_report_types_text() -> str:
|
|
48
49
|
return ", ".join(supported_report_types().keys())
|
|
49
50
|
|
|
@@ -90,7 +91,7 @@ def query_annual_reports_tool(
|
|
|
90
91
|
"report_type": report_type,
|
|
91
92
|
"year": year,
|
|
92
93
|
"count": len(reports),
|
|
93
|
-
"reports":
|
|
94
|
+
"reports": format_reports(reports),
|
|
94
95
|
"message": f"Found {len(reports)} {report_type} report(s)"
|
|
95
96
|
+ (f" for year {year}" if year else ""),
|
|
96
97
|
}
|
package/python/requirements.txt
CHANGED
package/python/spider.py
CHANGED
|
@@ -167,6 +167,27 @@ def normalize_report_type(report_type: Optional[str]) -> str:
|
|
|
167
167
|
return normalized
|
|
168
168
|
|
|
169
169
|
|
|
170
|
+
def format_reports(reports: list) -> list:
|
|
171
|
+
"""提取公告中稳定、有用的字段,并把附件路径补全为可访问的绝对 URL。
|
|
172
|
+
|
|
173
|
+
放在 spider.py 是为了紧邻 download_path 前缀定义,避免调用方各自
|
|
174
|
+
硬编码一份基址。
|
|
175
|
+
"""
|
|
176
|
+
formatted = []
|
|
177
|
+
for report in reports:
|
|
178
|
+
adj = report.get("adjunctUrl", "")
|
|
179
|
+
formatted.append(
|
|
180
|
+
{
|
|
181
|
+
"announcementTitle": report.get("announcementTitle", ""),
|
|
182
|
+
"announcementTime": report.get("announcementTime", ""),
|
|
183
|
+
"secCode": report.get("secCode", ""),
|
|
184
|
+
"secName": report.get("secName", ""),
|
|
185
|
+
"adjunctUrl": download_path + adj if adj else "",
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
return formatted
|
|
189
|
+
|
|
190
|
+
|
|
170
191
|
def _build_headers() -> dict:
|
|
171
192
|
"""构造请求头,避免在并发场景下修改全局字典。"""
|
|
172
193
|
headers = BASE_HEADERS.copy()
|
|
@@ -18,6 +18,9 @@ const REQUIREMENTS_FILE = path.join(
|
|
|
18
18
|
|
|
19
19
|
const VENV_DIR = path.join(os.homedir(), ".cninfo-mcp", "venv");
|
|
20
20
|
|
|
21
|
+
// 依赖探针:校验 venv 是否满足 requirements.txt 的全部约束
|
|
22
|
+
const DEPS_CHECK = path.join(__dirname, "..", "python", "check_deps.py");
|
|
23
|
+
|
|
21
24
|
function getVenvPython() {
|
|
22
25
|
if (process.platform === "win32") {
|
|
23
26
|
return path.join(VENV_DIR, "Scripts", "python.exe");
|
|
@@ -101,8 +104,8 @@ async function main() {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
try {
|
|
104
|
-
//
|
|
105
|
-
await spawnCommand(venvPython, [
|
|
107
|
+
// 校验依赖是否满足约束(用 venv 的 python)
|
|
108
|
+
await spawnCommand(venvPython, [DEPS_CHECK]);
|
|
106
109
|
console.log("✅ Python dependencies already installed");
|
|
107
110
|
} catch (error) {
|
|
108
111
|
// 执行安装(用 venv 的 pip)
|