truesource-mcp 0.1.0__tar.gz

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,16 @@
1
+ # truesource-mcp · 远程传输形态(常驻服务 · 多客户端连)
2
+ #
3
+ # 只想在 Claude Desktop / Cursor 里用的话**不需要这个镜像** ——
4
+ # 那边走 stdio,`uvx truesource-mcp` 就够了。这个镜像是给「跑成一个服务」的场景。
5
+ FROM python:3.11-slim
6
+
7
+ WORKDIR /app
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+ COPY truesource_mcp/ ./truesource_mcp/
11
+
12
+ ENV TRUESOURCE_MCP_TRANSPORT=streamable-http TRUESOURCE_MCP_HOST=0.0.0.0 TRUESOURCE_MCP_PORT=8933
13
+
14
+ # HUNTER_API_KEY 必须在 run 的时候传进来 —— 不要 build 进镜像
15
+ EXPOSE 8933
16
+ CMD ["python", "-m", "truesource_mcp.server"]
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: truesource-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for TrueSource — verifiable first-hand market signals: filings, government procurement wins, R&D expansion, macro data
5
+ Project-URL: Homepage, https://github.com/agentpit-io/hunter-community
6
+ Project-URL: Repository, https://github.com/agentpit-io/hunter-community
7
+ Project-URL: Documentation, https://github.com/agentpit-io/hunter-community/blob/main/tools/truesource-mcp/README.md
8
+ Project-URL: Issues, https://github.com/agentpit-io/hunter-community/issues
9
+ Author-email: AgentPit <hangeaiagent@gmail.com>
10
+ License: Apache-2.0
11
+ Keywords: a-share,due-diligence,finance,mcp,model-context-protocol,procurement,research,signals,stock
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: mcp>=2.0.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # truesource-mcp
27
+
28
+ <!-- mcp-name: io.github.agentpit-io/truesource-mcp -->
29
+
30
+ **First-hand market signals your AI agent can actually cite.**
31
+
32
+ An [MCP](https://modelcontextprotocol.io) server wrapping **TrueSource** — a
33
+ crawler and AI-retrieval stack that watches things which have *already happened*
34
+ and carry a verifiable source: exchange filings, government procurement awards,
35
+ R&D expansion moves, northbound holdings, and official macro releases.
36
+
37
+ It does not summarise analyst reports and it does not emit buy/sell calls. Every
38
+ signal comes with a date and where it came from.
39
+
40
+ [![License](https://img.shields.io/badge/license-Apache_2.0-blue)](https://github.com/agentpit-io/hunter-community/blob/main/LICENSE)
41
+
42
+ ---
43
+
44
+ ## Tools
45
+
46
+ | Tool | What it does | Speed |
47
+ |---|---|---|
48
+ | `truesource_procurement(days, limit)` | Recent government procurement awards — who is actually winning contracts | fast |
49
+ | `truesource_macro(days)` | Macro releases from the statistics bureau, customs and industry bodies | fast |
50
+ | `truesource_daily_brief(symbols)` | Signal digest + alert level for a batch of tickers (last 3 days) | fast |
51
+ | `truesource_alert_signals(symbols)` | Only signals that crossed an alert threshold (last 26 hours) | fast |
52
+ | `truesource_report(symbol)` | Full research report — **35 pre-built AI-compute names only** | fast |
53
+ | `truesource_scout(symbol, name)` | Live full collection for **any A-share** | **30–60s** |
54
+
55
+ Procurement is the most distinctive one: a contract award is a **fact that has
56
+ already occurred**, which is a harder input than a forecast.
57
+
58
+ ---
59
+
60
+ ## Quick start
61
+
62
+ You need an API key first — see [below](#api-key).
63
+
64
+ ### Claude Desktop / Cursor
65
+
66
+ Add to `claude_desktop_config.json`:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "truesource": {
72
+ "command": "uvx",
73
+ "args": ["truesource-mcp"],
74
+ "env": { "HUNTER_API_KEY": "hunt_tools_xxx" }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Command line
81
+
82
+ ```bash
83
+ # no install needed
84
+ HUNTER_API_KEY=hunt_tools_xxx uvx truesource-mcp
85
+
86
+ # or install it
87
+ pip install truesource-mcp
88
+ HUNTER_API_KEY=hunt_tools_xxx truesource-mcp
89
+ ```
90
+
91
+ ---
92
+
93
+ ## API key
94
+
95
+ **This server has no keyless mode, and cannot have one** — the upstream gateway
96
+ returns 403 without a key.
97
+
98
+ The reason is cost: `truesource_scout` runs real crawlers plus a Gemini search on
99
+ every call, so **each invocation spends money on third-party APIs**. Anonymous
100
+ access would be trivially expensive to abuse and impossible to attribute.
101
+
102
+ Get one at <https://hunter.agentpit.io/dev/api-keys> — sign in, click
103
+ **申请 API Key**, copy the `hunt_tools_…` key (shown once), set it as
104
+ `HUNTER_API_KEY`.
105
+
106
+ If the key is missing, this server does **not** return sample data or a fake
107
+ success. It returns an error that tells you exactly where to get a key.
108
+
109
+ ---
110
+
111
+ ## Environment variables
112
+
113
+ | Variable | Default | Notes |
114
+ |---|---|---|
115
+ | `HUNTER_API_KEY` | — | **Required.** `hunt_tools_` key |
116
+ | `TRUESOURCE_URL` | `https://hunter.agentpit.io/api/saas/truesource` | Point at your own deployment if you self-host |
117
+ | `TRUESOURCE_TIMEOUT` | `20` | Seconds. `scout` gets its own 120s floor regardless |
118
+ | `TRUESOURCE_MAX_ITEMS` | `40` | Cap on items per response |
119
+ | `TRUESOURCE_MCP_TRANSPORT` | `stdio` | `stdio` \| `streamable-http` \| `sse` |
120
+ | `TRUESOURCE_MCP_HOST` / `_PORT` | `0.0.0.0` / `8933` | Remote transports only |
121
+
122
+ `stdio` is what Claude Desktop, Cursor and `uvx` use. Run a remote transport only
123
+ if one long-running server should serve several clients — and prefer
124
+ `streamable-http`, since the MCP spec marks SSE as deprecated.
125
+
126
+ ---
127
+
128
+ ## Three things that will bite you otherwise
129
+
130
+ **`grey` is not `green`.** In `daily_brief`, an alert level of `grey` means *no
131
+ signal was collected for this ticker in the last 3 days*. That may mean the stock
132
+ was quiet, or it may mean coverage missed it. It does not mean "safe".
133
+
134
+ **An empty result is a conclusion, not a failure.** `alert_signals` returning
135
+ nothing means these tickers crossed no threshold in the last 26 hours. That is
136
+ information. This server never dresses an empty result up as an error, and never
137
+ fills it with placeholder rows.
138
+
139
+ **Truncation is always disclosed.** Responses are capped at `TRUESOURCE_MAX_ITEMS`
140
+ (40 by default). When that bites, the payload carries `truncated: true`, the real
141
+ `total`, and a note not to draw conclusions from the visible slice. Raise the cap
142
+ or narrow the date range instead.
143
+
144
+ `truesource_report` also will **not** silently fall back to `scout` when a ticker
145
+ isn't one of the 35 pre-built names. Scout costs 30–60 seconds and real money —
146
+ that choice should be made deliberately, not happen behind your back.
147
+
148
+ ---
149
+
150
+ ## Errors you might hit
151
+
152
+ | Error | Meaning |
153
+ |---|---|
154
+ | `missing_api_key` | `HUNTER_API_KEY` not set — see [API key](#api-key) |
155
+ | `invalid_api_key` (401) | Key wrong, incomplete or revoked |
156
+ | `forbidden` (403) | Key valid but lacks access to this endpoint |
157
+ | `not_found` (404) | For `report`: ticker isn't a pre-built name — use `truesource_scout` |
158
+ | `timeout` | `scout` genuinely takes 30–60s; raise `TRUESOURCE_TIMEOUT` |
159
+ | `upstream_down` (502/503) | Crawler service is down; retry later |
160
+
161
+ ---
162
+
163
+ ## Related
164
+
165
+ - **[kronos-mcp](https://github.com/agentpit-io/hunter-community/tree/main/tools/kronos-mcp)** — K-line forecasting for A-shares, US and HK equities
166
+ - **[akshare-mcp](https://github.com/agentpit-io/hunter-community/tree/main/tools/akshare-mcp)** — lets a model explore AKShare's 1000+ China market data functions on its own
167
+ - **[hunter-community](https://github.com/agentpit-io/hunter-community)** — the open-source stack these come from
168
+
169
+ Apache-2.0.
@@ -0,0 +1,144 @@
1
+ # truesource-mcp
2
+
3
+ <!-- mcp-name: io.github.agentpit-io/truesource-mcp -->
4
+
5
+ **First-hand market signals your AI agent can actually cite.**
6
+
7
+ An [MCP](https://modelcontextprotocol.io) server wrapping **TrueSource** — a
8
+ crawler and AI-retrieval stack that watches things which have *already happened*
9
+ and carry a verifiable source: exchange filings, government procurement awards,
10
+ R&D expansion moves, northbound holdings, and official macro releases.
11
+
12
+ It does not summarise analyst reports and it does not emit buy/sell calls. Every
13
+ signal comes with a date and where it came from.
14
+
15
+ [![License](https://img.shields.io/badge/license-Apache_2.0-blue)](https://github.com/agentpit-io/hunter-community/blob/main/LICENSE)
16
+
17
+ ---
18
+
19
+ ## Tools
20
+
21
+ | Tool | What it does | Speed |
22
+ |---|---|---|
23
+ | `truesource_procurement(days, limit)` | Recent government procurement awards — who is actually winning contracts | fast |
24
+ | `truesource_macro(days)` | Macro releases from the statistics bureau, customs and industry bodies | fast |
25
+ | `truesource_daily_brief(symbols)` | Signal digest + alert level for a batch of tickers (last 3 days) | fast |
26
+ | `truesource_alert_signals(symbols)` | Only signals that crossed an alert threshold (last 26 hours) | fast |
27
+ | `truesource_report(symbol)` | Full research report — **35 pre-built AI-compute names only** | fast |
28
+ | `truesource_scout(symbol, name)` | Live full collection for **any A-share** | **30–60s** |
29
+
30
+ Procurement is the most distinctive one: a contract award is a **fact that has
31
+ already occurred**, which is a harder input than a forecast.
32
+
33
+ ---
34
+
35
+ ## Quick start
36
+
37
+ You need an API key first — see [below](#api-key).
38
+
39
+ ### Claude Desktop / Cursor
40
+
41
+ Add to `claude_desktop_config.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "truesource": {
47
+ "command": "uvx",
48
+ "args": ["truesource-mcp"],
49
+ "env": { "HUNTER_API_KEY": "hunt_tools_xxx" }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### Command line
56
+
57
+ ```bash
58
+ # no install needed
59
+ HUNTER_API_KEY=hunt_tools_xxx uvx truesource-mcp
60
+
61
+ # or install it
62
+ pip install truesource-mcp
63
+ HUNTER_API_KEY=hunt_tools_xxx truesource-mcp
64
+ ```
65
+
66
+ ---
67
+
68
+ ## API key
69
+
70
+ **This server has no keyless mode, and cannot have one** — the upstream gateway
71
+ returns 403 without a key.
72
+
73
+ The reason is cost: `truesource_scout` runs real crawlers plus a Gemini search on
74
+ every call, so **each invocation spends money on third-party APIs**. Anonymous
75
+ access would be trivially expensive to abuse and impossible to attribute.
76
+
77
+ Get one at <https://hunter.agentpit.io/dev/api-keys> — sign in, click
78
+ **申请 API Key**, copy the `hunt_tools_…` key (shown once), set it as
79
+ `HUNTER_API_KEY`.
80
+
81
+ If the key is missing, this server does **not** return sample data or a fake
82
+ success. It returns an error that tells you exactly where to get a key.
83
+
84
+ ---
85
+
86
+ ## Environment variables
87
+
88
+ | Variable | Default | Notes |
89
+ |---|---|---|
90
+ | `HUNTER_API_KEY` | — | **Required.** `hunt_tools_` key |
91
+ | `TRUESOURCE_URL` | `https://hunter.agentpit.io/api/saas/truesource` | Point at your own deployment if you self-host |
92
+ | `TRUESOURCE_TIMEOUT` | `20` | Seconds. `scout` gets its own 120s floor regardless |
93
+ | `TRUESOURCE_MAX_ITEMS` | `40` | Cap on items per response |
94
+ | `TRUESOURCE_MCP_TRANSPORT` | `stdio` | `stdio` \| `streamable-http` \| `sse` |
95
+ | `TRUESOURCE_MCP_HOST` / `_PORT` | `0.0.0.0` / `8933` | Remote transports only |
96
+
97
+ `stdio` is what Claude Desktop, Cursor and `uvx` use. Run a remote transport only
98
+ if one long-running server should serve several clients — and prefer
99
+ `streamable-http`, since the MCP spec marks SSE as deprecated.
100
+
101
+ ---
102
+
103
+ ## Three things that will bite you otherwise
104
+
105
+ **`grey` is not `green`.** In `daily_brief`, an alert level of `grey` means *no
106
+ signal was collected for this ticker in the last 3 days*. That may mean the stock
107
+ was quiet, or it may mean coverage missed it. It does not mean "safe".
108
+
109
+ **An empty result is a conclusion, not a failure.** `alert_signals` returning
110
+ nothing means these tickers crossed no threshold in the last 26 hours. That is
111
+ information. This server never dresses an empty result up as an error, and never
112
+ fills it with placeholder rows.
113
+
114
+ **Truncation is always disclosed.** Responses are capped at `TRUESOURCE_MAX_ITEMS`
115
+ (40 by default). When that bites, the payload carries `truncated: true`, the real
116
+ `total`, and a note not to draw conclusions from the visible slice. Raise the cap
117
+ or narrow the date range instead.
118
+
119
+ `truesource_report` also will **not** silently fall back to `scout` when a ticker
120
+ isn't one of the 35 pre-built names. Scout costs 30–60 seconds and real money —
121
+ that choice should be made deliberately, not happen behind your back.
122
+
123
+ ---
124
+
125
+ ## Errors you might hit
126
+
127
+ | Error | Meaning |
128
+ |---|---|
129
+ | `missing_api_key` | `HUNTER_API_KEY` not set — see [API key](#api-key) |
130
+ | `invalid_api_key` (401) | Key wrong, incomplete or revoked |
131
+ | `forbidden` (403) | Key valid but lacks access to this endpoint |
132
+ | `not_found` (404) | For `report`: ticker isn't a pre-built name — use `truesource_scout` |
133
+ | `timeout` | `scout` genuinely takes 30–60s; raise `TRUESOURCE_TIMEOUT` |
134
+ | `upstream_down` (502/503) | Crawler service is down; retry later |
135
+
136
+ ---
137
+
138
+ ## Related
139
+
140
+ - **[kronos-mcp](https://github.com/agentpit-io/hunter-community/tree/main/tools/kronos-mcp)** — K-line forecasting for A-shares, US and HK equities
141
+ - **[akshare-mcp](https://github.com/agentpit-io/hunter-community/tree/main/tools/akshare-mcp)** — lets a model explore AKShare's 1000+ China market data functions on its own
142
+ - **[hunter-community](https://github.com/agentpit-io/hunter-community)** — the open-source stack these come from
143
+
144
+ Apache-2.0.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "truesource-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for TrueSource — verifiable first-hand market signals: filings, government procurement wins, R&D expansion, macro data"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "AgentPit", email = "hangeaiagent@gmail.com" }]
13
+ keywords = [
14
+ "mcp", "model-context-protocol", "stock", "signals", "procurement",
15
+ "finance", "a-share", "research", "due-diligence",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Financial and Insurance Industry",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: Apache Software License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Office/Business :: Financial :: Investment",
27
+ ]
28
+ dependencies = [
29
+ "mcp>=2.0.0",
30
+ "httpx>=0.27.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/agentpit-io/hunter-community"
35
+ Repository = "https://github.com/agentpit-io/hunter-community"
36
+ Documentation = "https://github.com/agentpit-io/hunter-community/blob/main/tools/truesource-mcp/README.md"
37
+ Issues = "https://github.com/agentpit-io/hunter-community/issues"
38
+
39
+ [project.scripts]
40
+ truesource-mcp = "truesource_mcp.server:main"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["truesource_mcp"]
@@ -0,0 +1,3 @@
1
+ # MCP Python SDK · 2.x 起 FastMCP 更名为 MCPServer,本包用的是 2.x 的 API
2
+ mcp>=2.0.0
3
+ httpx>=0.27.0
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.agentpit-io/truesource-mcp",
4
+ "title": "TrueSource",
5
+ "description": "Verifiable first-hand market signals: exchange filings, government procurement awards, R&D expansion, macro releases",
6
+ "repository": {
7
+ "url": "https://github.com/agentpit-io/hunter-community",
8
+ "source": "github",
9
+ "subfolder": "tools/truesource-mcp"
10
+ },
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "pypi",
15
+ "registryBaseUrl": "https://pypi.org",
16
+ "identifier": "truesource-mcp",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "uvx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "HUNTER_API_KEY",
25
+ "description": "API key from https://hunter.agentpit.io/dev/api-keys",
26
+ "isRequired": true,
27
+ "isSecret": true
28
+ },
29
+ {
30
+ "name": "TRUESOURCE_URL",
31
+ "description": "Override the upstream endpoint if you self-host",
32
+ "default": "https://hunter.agentpit.io/api/saas/truesource"
33
+ },
34
+ {
35
+ "name": "TRUESOURCE_MAX_ITEMS",
36
+ "description": "Cap on items per response",
37
+ "default": "40"
38
+ }
39
+ ]
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,5 @@
1
+ """TrueSource MCP · 一手信号采集的 MCP 封装。"""
2
+ from .server import main
3
+
4
+ __all__ = ["main"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env python3
2
+ """TrueSource MCP —— 把一手信号采集变成 AI 能直接调的工具。
3
+
4
+ ## 它做什么
5
+
6
+ TrueSource 是一套爬虫 + AI 检索,盯的是**能查证出处的一手信号**:
7
+ 交易所公告、政府采购中标、研发扩张动向、北向持仓、宏观口径数据。
8
+ 它不做研报摘要,也不产生「买/卖」结论 —— 给的是带日期和来源的原始信号。
9
+
10
+ 包成六个工具:
11
+
12
+ truesource_procurement() 最近的政采中标(哪些公司在真拿单)
13
+ truesource_macro() 宏观信号(国统局 / 海关 / 行业协会)
14
+ truesource_daily_brief(symbols) 一批票的信号摘要 + 预警级别
15
+ truesource_alert_signals(symbols) 只要达到预警阈值的那些
16
+ truesource_report(symbol) 单票完整报告(35 只预制标的秒回)
17
+ truesource_scout(symbol) 单票实时全量采集(30-60 秒)
18
+
19
+ ## 为什么必须要 API key
20
+
21
+ 上游是我们自己跑的爬虫集群,`truesource_scout` 一次会并行跑价格、公告和
22
+ Gemini AI 搜索 —— **每次调用都产生真实的第三方 API 花费**,耗时 30-60 秒。
23
+
24
+ 没有 key 的话谁都能触发,几个循环就能把成本打上去,也没法定位是谁在打。
25
+ 所以**这个 MCP 不做无 key 模式**,也做不了 —— 上游网关直接返 403。
26
+ 没配 key 时它不会假装成功、不会返回样例数据,而是明确告诉你去哪申请。
27
+
28
+ 申请:https://hunter.agentpit.io/dev/api-keys
29
+ 拿到的 key 形如 `hunt_tools_` + 随机串。
30
+
31
+ ## 跑起来
32
+
33
+ # 方式一 · uvx(推荐 · 不用装)
34
+ HUNTER_API_KEY=hunt_tools_xxx uvx truesource-mcp
35
+
36
+ # 方式二 · pip
37
+ pip install truesource-mcp
38
+ HUNTER_API_KEY=hunt_tools_xxx truesource-mcp
39
+
40
+ Claude Desktop / Cursor 的配置(`claude_desktop_config.json`):
41
+
42
+ {
43
+ "mcpServers": {
44
+ "truesource": {
45
+ "command": "uvx",
46
+ "args": ["truesource-mcp"],
47
+ "env": { "HUNTER_API_KEY": "hunt_tools_xxx" }
48
+ }
49
+ }
50
+ }
51
+
52
+ ## 环境变量
53
+
54
+ HUNTER_API_KEY 必填 · hunt_tools_ 开头
55
+ TRUESOURCE_URL 可选 · 默认 https://hunter.agentpit.io/api/saas/truesource
56
+ 自建部署或直连上游时改这个
57
+ TRUESOURCE_TIMEOUT 可选 · 默认 20 秒(scout 单独用 120 秒)
58
+ TRUESOURCE_MAX_ITEMS 可选 · 默认 40 · 单次返回的最大条目数
59
+ TRUESOURCE_MCP_TRANSPORT 可选 · stdio(默认)| streamable-http | sse
60
+ TRUESOURCE_MCP_HOST/PORT 仅远程传输时用 · 默认 0.0.0.0:8933
61
+ """
62
+ from __future__ import annotations
63
+
64
+ import json
65
+ import os
66
+
67
+ import httpx
68
+ from mcp.server.mcpserver import MCPServer
69
+
70
+ mcp = MCPServer("truesource-mcp")
71
+
72
+ DEFAULT_URL = "https://hunter.agentpit.io/api/saas/truesource"
73
+ APPLY_URL = "https://hunter.agentpit.io/dev/api-keys"
74
+
75
+ # 单次返回的最大条目数。爬虫接口一次能吐几百条,原样塞进模型上下文
76
+ # 会把真正有用的挤掉 —— 和 akshare-mcp 的 MAX_ROWS 同一个考虑。
77
+ MAX_ITEMS = int(os.getenv("TRUESOURCE_MAX_ITEMS", "40"))
78
+
79
+ _READ = float(os.getenv("TRUESOURCE_TIMEOUT", "20"))
80
+ # scout 要并行跑爬虫 + Gemini 搜索,30-60 秒是常态,单独给一档
81
+ _SCOUT_READ = max(_READ, 120.0)
82
+
83
+
84
+ def _base_url() -> str:
85
+ return (os.getenv("TRUESOURCE_URL") or DEFAULT_URL).rstrip("/")
86
+
87
+
88
+ def _key() -> str:
89
+ return (os.getenv("HUNTER_API_KEY") or "").strip()
90
+
91
+
92
+ def _no_key_error() -> str:
93
+ """没配 key 时的回话。**不返回假数据。**"""
94
+ return json.dumps({
95
+ "error": "missing_api_key",
96
+ "message": "没有配置 HUNTER_API_KEY,无法调用 TrueSource。",
97
+ "how_to_fix": [
98
+ f"1. 打开 {APPLY_URL} 登录",
99
+ "2. 点「申请 API Key」",
100
+ "3. 拿到 hunt_tools_ 开头的 key(只显示一次,立即复制)",
101
+ "4. 把它设成环境变量 HUNTER_API_KEY 后重启这个 MCP",
102
+ ],
103
+ "note": "这个服务没有免 key 模式 —— scout 每次调用都会跑真实的爬虫和 "
104
+ "Gemini 搜索,产生第三方 API 花费。",
105
+ }, ensure_ascii=False)
106
+
107
+
108
+ def _http_error(status: int, body: str, path: str) -> str:
109
+ table = {
110
+ 401: ("invalid_api_key",
111
+ "key 无效、缺失或已撤销。检查 HUNTER_API_KEY 是否复制完整。"),
112
+ 403: ("forbidden",
113
+ f"key 有效但没有访问这个接口的权限。到 {APPLY_URL} 确认 key 的类型和状态。"),
114
+ 404: ("not_found",
115
+ "上游没有这条数据。如果查的是个股报告,说明它不在预制的 35 只标的里 —— "
116
+ "改用 truesource_scout 做实时采集。"),
117
+ 429: ("rate_limited", "触发速率限制,稍后重试。"),
118
+ 502: ("upstream_down", "上游爬虫服务异常,稍后重试。"),
119
+ 503: ("upstream_unavailable", "上游暂时不可用,稍后重试。"),
120
+ 504: ("upstream_timeout",
121
+ "上游超时。scout 走 Gemini 搜索,慢的时候会这样 —— 稍后重试。"),
122
+ }
123
+ code, msg = table.get(status, ("http_error", f"上游返回 HTTP {status}。"))
124
+ return json.dumps({
125
+ "error": code, "http_status": status, "path": path,
126
+ "message": msg, "upstream_body": body[:300],
127
+ }, ensure_ascii=False)
128
+
129
+
130
+ def _request(method: str, path: str, params: dict | None = None,
131
+ read_timeout: float | None = None) -> str | dict | list:
132
+ """统一请求出口。失败返回 JSON 字符串,成功返回上游的 dict/list。"""
133
+ key = _key()
134
+ if not key:
135
+ return _no_key_error()
136
+
137
+ url = f"{_base_url()}{path}"
138
+ timeout = httpx.Timeout(connect=5.0, read=read_timeout or _READ,
139
+ write=5.0, pool=5.0)
140
+ try:
141
+ with httpx.Client(timeout=timeout) as client:
142
+ r = client.request(method, url, params=params or {},
143
+ headers={"Authorization": f"Bearer {key}"})
144
+ except httpx.TimeoutException:
145
+ return json.dumps({
146
+ "error": "timeout", "path": path,
147
+ "message": f"请求超时({(read_timeout or _READ):.0f}s)。"
148
+ f"scout 类接口本来就要 30-60 秒 —— 调大 TRUESOURCE_TIMEOUT 再试。",
149
+ }, ensure_ascii=False)
150
+ except httpx.HTTPError as e:
151
+ return json.dumps({
152
+ "error": "network_error", "url": url,
153
+ "message": f"{type(e).__name__}: {str(e)[:200]}",
154
+ "hint": "连不上服务。检查网络,或用 TRUESOURCE_URL 指到你自己的部署。",
155
+ }, ensure_ascii=False)
156
+
157
+ if r.status_code != 200:
158
+ return _http_error(r.status_code, r.text, path)
159
+
160
+ try:
161
+ return r.json()
162
+ except ValueError:
163
+ return json.dumps({
164
+ "error": "bad_response", "path": path,
165
+ "message": "上游返回的不是 JSON。", "body": r.text[:300],
166
+ }, ensure_ascii=False)
167
+
168
+
169
+ def _cap(data, label: str) -> str:
170
+ """裁剪到 MAX_ITEMS 并**明说裁了**。
171
+
172
+ 截断不说出来,模型会把「前 40 条」当成「全部 40 条」下结论 ——
173
+ 比如「最近 7 天只有 40 个中标」,而真相可能是 300 个。
174
+ """
175
+ if isinstance(data, str): # 上游已经是错误 JSON
176
+ return data
177
+
178
+ items = None
179
+ if isinstance(data, list):
180
+ items = data
181
+ elif isinstance(data, dict):
182
+ for k in ("items", "data", "signals", "results", "list"):
183
+ if isinstance(data.get(k), list):
184
+ items = data[k]
185
+ key_name = k
186
+ break
187
+
188
+ if items is None:
189
+ return json.dumps(data, ensure_ascii=False)
190
+
191
+ total = len(items)
192
+ if total <= MAX_ITEMS:
193
+ return json.dumps(data, ensure_ascii=False)
194
+
195
+ cut = items[:MAX_ITEMS]
196
+ if isinstance(data, list):
197
+ out = {"items": cut}
198
+ else:
199
+ out = dict(data)
200
+ out[key_name] = cut
201
+ out["truncated"] = True
202
+ out["returned"] = len(cut)
203
+ out["total"] = total
204
+ out["note"] = (f"只返回了前 {len(cut)} 条(共 {total} 条)。"
205
+ f"**不要把这 {len(cut)} 条当成全部来下结论。**"
206
+ f"缩小天数范围,或调大 TRUESOURCE_MAX_ITEMS。")
207
+ return json.dumps(out, ensure_ascii=False)
208
+
209
+
210
+ def _clean_symbols(symbols: str) -> str | None:
211
+ """规整逗号分隔的代码串。返回 None 表示输入不合法。"""
212
+ parts = [s.strip() for s in (symbols or "").replace(",", ",").split(",")]
213
+ parts = [s for s in parts if s]
214
+ return ",".join(parts) if parts else None
215
+
216
+
217
+ @mcp.tool()
218
+ def truesource_procurement(days: int = 7, limit: int = 15) -> str:
219
+ """最近的政府采购中标信号 —— 哪些公司在真拿单。
220
+
221
+ days 查最近几天,默认 7
222
+ limit 最多几条,默认 15
223
+
224
+ 这是 TrueSource 最独有的一块:中标公告是**已经发生的事实**,
225
+ 比研报的预测更硬。适合回答「谁在实际拿到订单」这类问题。
226
+ """
227
+ return _cap(_request("GET", "/api/procurement",
228
+ {"days": days, "limit": limit}), "procurement")
229
+
230
+
231
+ @mcp.tool()
232
+ def truesource_macro(days: int = 30) -> str:
233
+ """宏观信号 —— 国家统计局 + 海关 + 行业协会的口径数据。
234
+
235
+ days 查最近几天,默认 30
236
+
237
+ 给的是有明确发布方和日期的原始口径,不是二手解读。
238
+ """
239
+ return _cap(_request("GET", "/api/macro", {"days": days}), "macro")
240
+
241
+
242
+ @mcp.tool()
243
+ def truesource_daily_brief(symbols: str) -> str:
244
+ """一批股票的信号摘要 + 预警级别(最近 3 天)。
245
+
246
+ symbols 逗号分隔的代码,如 `300308,688041`。一次问一批比逐个问快得多。
247
+
248
+ 每只票会带一个 alert_level:
249
+ red 有需要立刻看的负面信号
250
+ yellow 有变化,值得留意
251
+ green 正常
252
+ grey 这几天没采到信号(**不等于没事,是没数据**)
253
+
254
+ ⚠️ grey 和 green 是两件事。grey 表示这只票最近 3 天没有采到任何信号,
255
+ 可能是它确实安静,也可能是爬虫没覆盖到 —— 别把 grey 读成「安全」。
256
+ """
257
+ syms = _clean_symbols(symbols)
258
+ if not syms:
259
+ return json.dumps({
260
+ "error": "bad_symbols",
261
+ "message": "symbols 不能为空。格式:逗号分隔的股票代码,如 300308,688041",
262
+ }, ensure_ascii=False)
263
+ return _cap(_request("GET", "/api/hunter/daily-brief", {"symbols": syms}),
264
+ "daily_brief")
265
+
266
+
267
+ @mcp.tool()
268
+ def truesource_alert_signals(symbols: str) -> str:
269
+ """只返回达到预警阈值的信号(最近 26 小时)。
270
+
271
+ symbols 逗号分隔的代码
272
+
273
+ 比 daily_brief 更窄 —— 它只给「需要动作」的那些。
274
+ **空结果是有意义的结论**:这批票最近 26 小时没有触发预警,
275
+ 不是查询失败。
276
+ """
277
+ syms = _clean_symbols(symbols)
278
+ if not syms:
279
+ return json.dumps({
280
+ "error": "bad_symbols",
281
+ "message": "symbols 不能为空。格式:逗号分隔的股票代码,如 300308,688041",
282
+ }, ensure_ascii=False)
283
+ return _cap(_request("GET", "/api/hunter/alert-signals", {"symbols": syms}),
284
+ "alert_signals")
285
+
286
+
287
+ @mcp.tool()
288
+ def truesource_report(symbol: str) -> str:
289
+ """单只标的的完整研究报告 —— 公告、研发扩张、北向持仓、AI 搜索分类汇总。
290
+
291
+ symbol 单个股票代码,如 `300308`
292
+
293
+ **只有 35 只预制标的(AI 算力链)能秒回。** 其它票会返回 not_found,
294
+ 那时改用 `truesource_scout` 做实时采集(慢,但任意 A 股都能查)。
295
+
296
+ 这个工具不会自动帮你 fallback 到 scout —— 因为 scout 要跑 30-60 秒
297
+ 并产生真实花费,那个决定应该由你(或用户)明确做出,不该悄悄发生。
298
+ """
299
+ sym = (symbol or "").strip()
300
+ if not sym:
301
+ return json.dumps({"error": "bad_symbol",
302
+ "message": "symbol 不能为空"}, ensure_ascii=False)
303
+ return _cap(_request("GET", f"/api/hunter/report/{sym}"), "report")
304
+
305
+
306
+ @mcp.tool()
307
+ def truesource_scout(symbol: str, name: str = "") -> str:
308
+ """对一只票做**实时全量采集** —— 任意 A 股都能查。
309
+
310
+ symbol 股票代码,如 `600519`
311
+ name 股票名称(可选,能提高 AI 搜索的命中率)
312
+
313
+ 并行跑价格 + 公告 + 研发扩张 + 北向 + Gemini AI 搜索。
314
+
315
+ ⚠️ **一次要 30-60 秒,且产生真实的第三方 API 花费。**
316
+ 先试 `truesource_report`(预制标的秒回),它 not_found 了再用这个。
317
+ 不要在循环里对几十只票调用它。
318
+ """
319
+ sym = (symbol or "").strip()
320
+ if not sym:
321
+ return json.dumps({"error": "bad_symbol",
322
+ "message": "symbol 不能为空"}, ensure_ascii=False)
323
+ params = {"name": name.strip()} if name and name.strip() else None
324
+ return _cap(_request("POST", f"/api/hunter/scout/{sym}", params,
325
+ read_timeout=_SCOUT_READ), "scout")
326
+
327
+
328
+ def main() -> None:
329
+ """入口 —— PyPI 的 console_scripts 指向这里。
330
+
331
+ 默认 stdio:Claude Desktop / Cursor / uvx 都走这个。
332
+ streamable-http / sse 留给常驻服务的场景(比如接进 Hunter 的
333
+ 「能力 → 接入一个工具」)。MCP 规范已把 SSE 标记为过时,新部署优先用
334
+ streamable-http。
335
+ """
336
+ transport = (os.getenv("TRUESOURCE_MCP_TRANSPORT") or "stdio").strip().lower()
337
+ if transport == "stdio":
338
+ mcp.run(transport="stdio")
339
+ return
340
+
341
+ # 远程传输 · host/port 在 mcp 2.x 里是 run() 的关键字参数,
342
+ # 不再是 1.x 那个 mcp.settings.host/port
343
+ host = os.getenv("TRUESOURCE_MCP_HOST", "0.0.0.0")
344
+ port = int(os.getenv("TRUESOURCE_MCP_PORT", "8933"))
345
+ if transport in ("http", "streamable-http"):
346
+ mcp.run(transport="streamable-http", host=host, port=port)
347
+ elif transport == "sse":
348
+ mcp.run(transport="sse", host=host, port=port)
349
+ else:
350
+ raise SystemExit(
351
+ f"未知的 TRUESOURCE_MCP_TRANSPORT={transport!r} —— "
352
+ f"只支持 stdio(默认)/ streamable-http / sse")
353
+
354
+
355
+ if __name__ == "__main__":
356
+ main()