iwencai-mcp 0.2.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 +63 -0
- package/dist/client.js +82 -0
- package/dist/server.js +109 -0
- package/package.json +49 -0
- package/src/client.ts +121 -0
- package/src/server.ts +142 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# iwencai-mcp
|
|
2
|
+
|
|
3
|
+
问财 (Iwencai / 同花顺) MCP server —— 聚合同花顺问财 SkillHub 的 **25 个官方技能**为一个 MCP 服务,不含任何第三方技能。
|
|
4
|
+
|
|
5
|
+
底层只有两种 HTTP 操作,全部技能收敛其上:
|
|
6
|
+
|
|
7
|
+
- `POST /v1/query2data` → 22 个工具(查询 + 筛选)
|
|
8
|
+
- `POST /v1/comprehensive/search` → 3 个工具(新闻 / 公告 / 研报,`channel` 区分)
|
|
9
|
+
|
|
10
|
+
## 工具一览 (25)
|
|
11
|
+
|
|
12
|
+
| 类别 | 工具 |
|
|
13
|
+
|---|---|
|
|
14
|
+
| 查询 (12) | `query_market_data` 行情 · `query_industry_data` 行业 · `query_finance_data` 财务 · `query_macro_data` 宏观 · `query_index_data` 指数 · `query_fund_data` 基金 · `query_futures_data` 期货期权 · `query_insresearch_data` 机构研究与评级 · `query_event_data` 事件 · `query_business_data` 经营 · `query_management_data` 股东股本 · `query_basicinfo_data` 基本资料 |
|
|
15
|
+
| 筛选 (10) | `select_astock` A股 · `select_etf` ETF · `select_cb` 可转债 · `select_fund` 基金 · `select_fundcompany` 基金公司 · `select_fundmanager` 基金经理 · `select_futures` 期货期权 · `select_sector` 板块 · `select_hkstock` 港股 · `select_usstock` 美股 |
|
|
16
|
+
| 搜索 (3) | `search_news` 资讯 · `search_announcements` 公告 · `search_reports` 研报 |
|
|
17
|
+
|
|
18
|
+
数据来源: **同花顺问财** https://www.iwencai.com/unifiedwap/chat
|
|
19
|
+
|
|
20
|
+
## 环境变量
|
|
21
|
+
|
|
22
|
+
| 变量 | 必填 | 说明 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `IWENCAI_API_KEY` | ✅ | API Key,从同花顺问财 SkillHub 获取 |
|
|
25
|
+
| `IWENCAI_BASE_URL` | 否 | 默认 `https://openapi.iwencai.com` |
|
|
26
|
+
|
|
27
|
+
## 启动
|
|
28
|
+
|
|
29
|
+
### stdio MCP(开发 / 本地)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx -y tsx src/server.ts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 通过 npm 安装后运行
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx iwencai-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 注册到 Claude Code(user scope)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
claude mcp add iwencai \
|
|
45
|
+
--env IWENCAI_API_KEY=xxx \
|
|
46
|
+
--env IWENCAI_BASE_URL=https://openapi.iwencai.com \
|
|
47
|
+
-- npx -y tsx /path/to/iwencai-mcp/src/server.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 发布 (npm)
|
|
51
|
+
|
|
52
|
+
CI 在推送 `v*` 标签或手动触发时发布:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git tag v0.2.0
|
|
56
|
+
git push origin v0.2.0
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
需要先在 GitHub 仓库设置 secret `NPM_TOKEN`(npm 访问令牌)。
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 问财 (Iwencai) OpenAPI 客户端 — 两个真实 HTTP 操作。
|
|
3
|
+
*
|
|
4
|
+
* 四个 skill 底层的全部请求都收敛到这里:
|
|
5
|
+
* - query2data(行情/行业): POST /v1/query2data
|
|
6
|
+
* - search(资讯/公告): POST /v1/comprehensive/search
|
|
7
|
+
*
|
|
8
|
+
* 严格遵循网关规范: 每次请求携带 X-Claw-* headers,
|
|
9
|
+
* X-Claw-Trace-Id 为 64 字符十六进制唯一 ID, Authorization Bearer 仅从环境变量读取。
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
const DEFAULT_BASE_URL = "https://openapi.iwencai.com";
|
|
13
|
+
function traceId() {
|
|
14
|
+
return randomBytes(32).toString("hex"); // 64 个十六进制字符
|
|
15
|
+
}
|
|
16
|
+
function clawHeaders(apiKey, skillId) {
|
|
17
|
+
return {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
"X-Claw-Call-Type": "normal",
|
|
21
|
+
"X-Claw-Skill-Id": skillId,
|
|
22
|
+
"X-Claw-Skill-Version": "1.0.0",
|
|
23
|
+
"X-Claw-Plugin-Id": "none",
|
|
24
|
+
"X-Claw-Plugin-Version": "none",
|
|
25
|
+
"X-Claw-Trace-Id": traceId(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function post(path, body, opts, skillId) {
|
|
29
|
+
const url = `${opts.baseUrl ?? DEFAULT_BASE_URL}${path}`;
|
|
30
|
+
const headers = clawHeaders(opts.apiKey, skillId);
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30000);
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
let payload = { _http_status: response.status };
|
|
42
|
+
if (text) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(text);
|
|
45
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
46
|
+
payload = { ...parsed, _http_status: response.status };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
payload = { data: parsed, _http_status: response.status };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
payload = { text_response: text, _http_status: response.status };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ...payload, trace_id: headers["X-Claw-Trace-Id"], _http_status: response.status };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
throw new Error(`[${skillId}] 请求失败: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** 行情 / 行业查询: POST /v1/query2data */
|
|
67
|
+
export function query2data(args, opts, skillId) {
|
|
68
|
+
const { query, page = "1", limit = "10" } = args;
|
|
69
|
+
return post("/v1/query2data", { query, page, limit, is_cache: "1", expand_index: "true" }, opts, skillId);
|
|
70
|
+
}
|
|
71
|
+
/** 资讯 / 研报 / 公告搜索: POST /v1/comprehensive/search */
|
|
72
|
+
export function search(args, opts, channel) {
|
|
73
|
+
const { query, size = "10" } = args;
|
|
74
|
+
const body = { query, channels: [channel], app_id: "AIME_SKILL", size };
|
|
75
|
+
const skillIdMap = {
|
|
76
|
+
news: "news-search",
|
|
77
|
+
announcement: "announcement-search",
|
|
78
|
+
report: "report-search",
|
|
79
|
+
};
|
|
80
|
+
const skillId = skillIdMap[channel];
|
|
81
|
+
return post("/v1/comprehensive/search", body, opts, skillId);
|
|
82
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 问财 MCP server — 聚合 25 个官方技能为 1 个 MCP 服务(不含任何第三方技能)。
|
|
4
|
+
*
|
|
5
|
+
* 官方技能清单来源: 同花顺问财 SkillHub (www.iwencai.com/skillhub) square 接口,
|
|
6
|
+
* 仅收录 classify=OFFICIAL 且 is_public=1 的 25 个.
|
|
7
|
+
*
|
|
8
|
+
* 全部技能只有两种 HTTP 操作:
|
|
9
|
+
* - query2data(22 个: hithink-*-query + hithink-*-selector): POST /v1/query2data
|
|
10
|
+
* - search(3 个: news / announcement / report): POST /v1/comprehensive/search
|
|
11
|
+
*
|
|
12
|
+
* 启动: npx -y tsx src/server.ts (dev / 注册用)
|
|
13
|
+
* npm run build && node dist/server.js (编译产物)
|
|
14
|
+
*/
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { query2data, search } from "./client.js";
|
|
19
|
+
const apiKey = process.env.IWENCAI_API_KEY;
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
process.stderr.write("IWENCAI_API_KEY is not set. 请把它放进环境(如 ~/.zshenv),或在注册 MCP 时用 --env 传入。\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const options = {
|
|
25
|
+
apiKey,
|
|
26
|
+
baseUrl: process.env.IWENCAI_BASE_URL || undefined,
|
|
27
|
+
};
|
|
28
|
+
const ATTRIBUTION = "数据来源于同花顺问财 (https://www.iwencai.com/unifiedwap/chat)";
|
|
29
|
+
function payload(result) {
|
|
30
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
31
|
+
return { ...result, _attribution: ATTRIBUTION };
|
|
32
|
+
}
|
|
33
|
+
return { data: result, _attribution: ATTRIBUTION };
|
|
34
|
+
}
|
|
35
|
+
function textResult(data, isError = false) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
isError,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function safe(fn) {
|
|
47
|
+
try {
|
|
48
|
+
return textResult(payload(await fn()));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return textResult({ success: false, error: err instanceof Error ? err.message : String(err) }, true);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---- query2data 系(22 个) ----
|
|
55
|
+
const QUERY_TOOLS = [
|
|
56
|
+
{ tool: "query_market_data", skillId: "hithink-market-query", title: "行情数据查询(股票/ETF/指数)", desc: "获取股票、ETF、指数等实时行情:最新价、涨跌幅、涨跌额、成交量、换手率、主力资金流向、大小单、技术指标(MACD/KDJ/RSI/布林线)等。输入自然语言问句。" },
|
|
57
|
+
{ tool: "query_industry_data", skillId: "hithink-industry-query", title: "行业数据查询", desc: "查询行业估值、财务、盈利、行情、板块排名等数据。输入自然语言问句。" },
|
|
58
|
+
{ tool: "query_finance_data", skillId: "hithink-finance-query", title: "财务数据查询", desc: "查询全市场个股营业收入、净利润、ROE、负债率、现金流等财务指标。输入自然语言问句。" },
|
|
59
|
+
{ tool: "query_macro_data", skillId: "hithink-macro-query", title: "宏观数据查询", desc: "查询 GDP、CPI、PPI、利率、汇率、社融、M2 等宏观经济指标。输入自然语言问句。" },
|
|
60
|
+
{ tool: "query_index_data", skillId: "hithink-zhishu-query", title: "指数数据查询", desc: "查询上证指数、沪深300、创业板指、恒生指数、纳斯达克指数等指数行情(涨跌幅/成交量/点位)。输入自然语言问句。" },
|
|
61
|
+
{ tool: "query_fund_data", skillId: "hithink-fund-query", title: "基金理财查询", desc: "对基金做业绩、持仓、风险、评级、获奖、基金经理、基金公司综合分析。输入自然语言问句。" },
|
|
62
|
+
{ tool: "query_futures_data", skillId: "hithink-futures-query", title: "期货期权数据查询", desc: "查询期货期权行情、波动率、产销、会员持仓、会员榜单、行权等数据。输入自然语言问句。" },
|
|
63
|
+
{ tool: "query_insresearch_data", skillId: "hithink-insresearch-query", title: "机构研究与评级查询", desc: "查询研报评级、业绩预测、ESG、信用评级、主体评级、基金评级、券商金股等机构观点数据。输入自然语言问句。" },
|
|
64
|
+
{ tool: "query_event_data", skillId: "hithink-event-query", title: "事件数据查询", desc: "查询个股业绩预告、增发、质押、解禁、调研、监管函等事件数据。输入自然语言问句。" },
|
|
65
|
+
{ tool: "query_business_data", skillId: "hithink-business-query", title: "公司经营数据查询", desc: "查询主营业务构成、主要客户、供应商、参控股公司、股权投资、重大合同等经营数据。输入自然语言问句。" },
|
|
66
|
+
{ tool: "query_management_data", skillId: "hithink-management-query", title: "公司股东股本查询", desc: "查询股本结构、股权结构、股东户数、前十大股东/流通股东、主要持有人、实控人等股权信息。输入自然语言问句。" },
|
|
67
|
+
{ tool: "query_basicinfo_data", skillId: "hithink-basicinfo-query", title: "基本资料查询", desc: "查询全品类标的(股票、指数、基金、期货、期权、转债、债券、理财、保险等)基础信息、发行主体、机构资料、费率、上市地点/日期等静态信息。输入自然语言问句。" },
|
|
68
|
+
{ tool: "select_astock", skillId: "hithink-astock-selector", title: "问财选A股", desc: "通过自然语言查询进行 A 股筛选,支持行情指标、技术形态、财务指标、行业概念等多条件组合筛选。" },
|
|
69
|
+
{ tool: "select_etf", skillId: "hithink-etf-selector", title: "问财选ETF", desc: "根据行情、跟踪指数基本面、规模、风格类型等条件筛选 ETF。" },
|
|
70
|
+
{ tool: "select_cb", skillId: "hithink-cb-selector", title: "问财选可转债", desc: "通过转股溢价率、正股表现、评级、剩余期限等多条件组合筛选可转债。" },
|
|
71
|
+
{ tool: "select_fund", skillId: "hithink-fund-selector", title: "问财选基金", desc: "根据基金类型、业绩、基金经理、风险、持仓、资产配置等维度筛选公募基金。" },
|
|
72
|
+
{ tool: "select_fundcompany", skillId: "hithink-fundcompany-selector", title: "问财选基金公司", desc: "根据管理规模、旗下产品业绩、投研实力、风险评级等维度筛选公募基金公司。" },
|
|
73
|
+
{ tool: "select_fundmanager", skillId: "hithink-fundmanager-selector", title: "问财选基金经理", desc: "根据历史业绩、管理规模、投资风格、风险控制等维度筛选公募基金经理。" },
|
|
74
|
+
{ tool: "select_futures", skillId: "hithink-futures-selector", title: "问财选期货期权", desc: "通过行情、波动率、产销、会员持仓、会员榜单、行权等多条件组合筛选期货期权。" },
|
|
75
|
+
{ tool: "select_sector", skillId: "hithink-sector-selector", title: "问财选板块", desc: "通过行业估值、资金流向、涨跌幅、板块类型等多条件组合筛选市场板块。" },
|
|
76
|
+
{ tool: "select_hkstock", skillId: "hithink-hkstock-selector", title: "问财选港股", desc: "通过自然语言查询进行港股筛选,支持行情指标、财务指标、行业概念、陆港通等多条件组合筛选。" },
|
|
77
|
+
{ tool: "select_usstock", skillId: "hithink-usstock-selector", title: "问财选美股", desc: "通过自然语言查询进行美股筛选,支持行情指标、财务指标、行业概念、业绩预测、研报评级等多条件组合筛选。" },
|
|
78
|
+
];
|
|
79
|
+
const SEARCH_TOOLS = [
|
|
80
|
+
{ tool: "search_news", channel: "news", skillId: "news-search", title: "财经资讯搜索", desc: "财经为主的资讯搜索引擎,覆盖官媒、主流财经媒体、垂直行业网站、知名上市公司/非上市公司官网。返回标题、URL、摘要、来源、发布时间。" },
|
|
81
|
+
{ tool: "search_announcements", channel: "announcement", skillId: "announcement-search", title: "公告搜索", desc: "查询 A股、港股、基金、ETF 等金融标的公告,类型含定期财务报告、分红派息、回购增持、资产重组等。返回标题、链接、摘要、公告日期。" },
|
|
82
|
+
{ tool: "search_reports", channel: "report", skillId: "report-search", title: "研报搜索", desc: "收录主流投研机构发布的研究报告,返回分析逻辑、投资评级、目标价等投研决策信息。" },
|
|
83
|
+
];
|
|
84
|
+
const querySchema = {
|
|
85
|
+
query: z.string().describe("自然语言查询问句(可被改写为标准金融问句)"),
|
|
86
|
+
page: z.string().optional().describe("分页参数,默认 1"),
|
|
87
|
+
limit: z.string().optional().describe("每页条数,默认 10"),
|
|
88
|
+
};
|
|
89
|
+
const searchSchema = {
|
|
90
|
+
query: z.string().describe("自然语言搜索问句"),
|
|
91
|
+
size: z.string().optional().describe("返回条数,默认 10"),
|
|
92
|
+
};
|
|
93
|
+
const server = new McpServer({ name: "iwencai", version: "0.2.0" });
|
|
94
|
+
for (const t of QUERY_TOOLS) {
|
|
95
|
+
server.registerTool(t.tool, {
|
|
96
|
+
title: `${t.title}(同花顺问财)`,
|
|
97
|
+
description: `${t.desc}\n${ATTRIBUTION}`,
|
|
98
|
+
inputSchema: querySchema,
|
|
99
|
+
}, async (args) => safe(() => query2data(args, options, t.skillId)));
|
|
100
|
+
}
|
|
101
|
+
for (const t of SEARCH_TOOLS) {
|
|
102
|
+
server.registerTool(t.tool, {
|
|
103
|
+
title: `${t.title}(同花顺问财)`,
|
|
104
|
+
description: `${t.desc}\n${ATTRIBUTION}`,
|
|
105
|
+
inputSchema: searchSchema,
|
|
106
|
+
}, async (args) => safe(() => search(args, options, t.channel)));
|
|
107
|
+
}
|
|
108
|
+
const transport = new StdioServerTransport();
|
|
109
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iwencai-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "问财 (Iwencai) MCP server — 聚合同花顺问财 25 个官方技能(行情/行业/财务/宏观/指数/基金/期货/研报/事件/经营/股东/基本资料 + 选股筛选器 + 新闻/公告/研报搜索)为一个 MCP 服务",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"iwencai-mcp": "./dist/server.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/server.ts",
|
|
13
|
+
"start": "node dist/server.js",
|
|
14
|
+
"smoke": "node scripts/smoke.mjs",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"iwencai",
|
|
24
|
+
"同花顺",
|
|
25
|
+
"问财",
|
|
26
|
+
"stock",
|
|
27
|
+
"行情",
|
|
28
|
+
"finance",
|
|
29
|
+
"a-share"
|
|
30
|
+
],
|
|
31
|
+
"author": "Huang Tianhui <huangtianhui@xiaobangtouzi.com>",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/homeant/iwencai-mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
39
|
+
"zod": "^3.23.8"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.10.0",
|
|
43
|
+
"tsx": "^4.19.0",
|
|
44
|
+
"typescript": "^5.6.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 问财 (Iwencai) OpenAPI 客户端 — 两个真实 HTTP 操作。
|
|
3
|
+
*
|
|
4
|
+
* 四个 skill 底层的全部请求都收敛到这里:
|
|
5
|
+
* - query2data(行情/行业): POST /v1/query2data
|
|
6
|
+
* - search(资讯/公告): POST /v1/comprehensive/search
|
|
7
|
+
*
|
|
8
|
+
* 严格遵循网关规范: 每次请求携带 X-Claw-* headers,
|
|
9
|
+
* X-Claw-Trace-Id 为 64 字符十六进制唯一 ID, Authorization Bearer 仅从环境变量读取。
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
export interface IWENCaiOptions {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** 默认 https://openapi.iwencai.com */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/** 默认 30000 (ms) */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Query2DataArgs {
|
|
22
|
+
query: string;
|
|
23
|
+
page?: string;
|
|
24
|
+
limit?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchArgs {
|
|
28
|
+
query: string;
|
|
29
|
+
size?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_BASE_URL = "https://openapi.iwencai.com";
|
|
33
|
+
|
|
34
|
+
function traceId(): string {
|
|
35
|
+
return randomBytes(32).toString("hex"); // 64 个十六进制字符
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clawHeaders(apiKey: string, skillId: string): Record<string, string> {
|
|
39
|
+
return {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Authorization: `Bearer ${apiKey}`,
|
|
42
|
+
"X-Claw-Call-Type": "normal",
|
|
43
|
+
"X-Claw-Skill-Id": skillId,
|
|
44
|
+
"X-Claw-Skill-Version": "1.0.0",
|
|
45
|
+
"X-Claw-Plugin-Id": "none",
|
|
46
|
+
"X-Claw-Plugin-Version": "none",
|
|
47
|
+
"X-Claw-Trace-Id": traceId(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function post(
|
|
52
|
+
path: string,
|
|
53
|
+
body: Record<string, unknown>,
|
|
54
|
+
opts: IWENCaiOptions,
|
|
55
|
+
skillId: string
|
|
56
|
+
): Promise<Record<string, unknown>> {
|
|
57
|
+
const url = `${opts.baseUrl ?? DEFAULT_BASE_URL}${path}`;
|
|
58
|
+
const headers = clawHeaders(opts.apiKey, skillId);
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30000);
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers,
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
let payload: Record<string, unknown> = { _http_status: response.status };
|
|
70
|
+
if (text) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed: unknown = JSON.parse(text);
|
|
73
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
74
|
+
payload = { ...(parsed as Record<string, unknown>), _http_status: response.status };
|
|
75
|
+
} else {
|
|
76
|
+
payload = { data: parsed, _http_status: response.status };
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
payload = { text_response: text, _http_status: response.status };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { ...payload, trace_id: headers["X-Claw-Trace-Id"], _http_status: response.status };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
throw new Error(`[${skillId}] 请求失败: ${message}`);
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 行情 / 行业查询: POST /v1/query2data */
|
|
92
|
+
export function query2data(
|
|
93
|
+
args: Query2DataArgs,
|
|
94
|
+
opts: IWENCaiOptions,
|
|
95
|
+
skillId: string
|
|
96
|
+
): Promise<Record<string, unknown>> {
|
|
97
|
+
const { query, page = "1", limit = "10" } = args;
|
|
98
|
+
return post(
|
|
99
|
+
"/v1/query2data",
|
|
100
|
+
{ query, page, limit, is_cache: "1", expand_index: "true" },
|
|
101
|
+
opts,
|
|
102
|
+
skillId
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 资讯 / 研报 / 公告搜索: POST /v1/comprehensive/search */
|
|
107
|
+
export function search(
|
|
108
|
+
args: SearchArgs,
|
|
109
|
+
opts: IWENCaiOptions,
|
|
110
|
+
channel: "news" | "announcement" | "report"
|
|
111
|
+
): Promise<Record<string, unknown>> {
|
|
112
|
+
const { query, size = "10" } = args;
|
|
113
|
+
const body = { query, channels: [channel], app_id: "AIME_SKILL", size };
|
|
114
|
+
const skillIdMap: Record<string, string> = {
|
|
115
|
+
news: "news-search",
|
|
116
|
+
announcement: "announcement-search",
|
|
117
|
+
report: "report-search",
|
|
118
|
+
};
|
|
119
|
+
const skillId = skillIdMap[channel];
|
|
120
|
+
return post("/v1/comprehensive/search", body, opts, skillId);
|
|
121
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 问财 MCP server — 聚合 25 个官方技能为 1 个 MCP 服务(不含任何第三方技能)。
|
|
4
|
+
*
|
|
5
|
+
* 官方技能清单来源: 同花顺问财 SkillHub (www.iwencai.com/skillhub) square 接口,
|
|
6
|
+
* 仅收录 classify=OFFICIAL 且 is_public=1 的 25 个.
|
|
7
|
+
*
|
|
8
|
+
* 全部技能只有两种 HTTP 操作:
|
|
9
|
+
* - query2data(22 个: hithink-*-query + hithink-*-selector): POST /v1/query2data
|
|
10
|
+
* - search(3 个: news / announcement / report): POST /v1/comprehensive/search
|
|
11
|
+
*
|
|
12
|
+
* 启动: npx -y tsx src/server.ts (dev / 注册用)
|
|
13
|
+
* npm run build && node dist/server.js (编译产物)
|
|
14
|
+
*/
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { query2data, search, type IWENCaiOptions } from "./client.js";
|
|
19
|
+
|
|
20
|
+
const apiKey = process.env.IWENCAI_API_KEY;
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
"IWENCAI_API_KEY is not set. 请把它放进环境(如 ~/.zshenv),或在注册 MCP 时用 --env 传入。\n"
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const options: IWENCaiOptions = {
|
|
29
|
+
apiKey,
|
|
30
|
+
baseUrl: process.env.IWENCAI_BASE_URL || undefined,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ATTRIBUTION = "数据来源于同花顺问财 (https://www.iwencai.com/unifiedwap/chat)";
|
|
34
|
+
|
|
35
|
+
function payload(result: unknown): Record<string, unknown> {
|
|
36
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
37
|
+
return { ...(result as Record<string, unknown>), _attribution: ATTRIBUTION };
|
|
38
|
+
}
|
|
39
|
+
return { data: result, _attribution: ATTRIBUTION };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function textResult(data: unknown, isError = false) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text" as const,
|
|
47
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
isError,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function safe(fn: () => Promise<Record<string, unknown>>) {
|
|
55
|
+
try {
|
|
56
|
+
return textResult(payload(await fn()));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return textResult(
|
|
59
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
60
|
+
true
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---- query2data 系(22 个) ----
|
|
66
|
+
const QUERY_TOOLS = [
|
|
67
|
+
{ tool: "query_market_data", skillId: "hithink-market-query", title: "行情数据查询(股票/ETF/指数)", desc: "获取股票、ETF、指数等实时行情:最新价、涨跌幅、涨跌额、成交量、换手率、主力资金流向、大小单、技术指标(MACD/KDJ/RSI/布林线)等。输入自然语言问句。" },
|
|
68
|
+
{ tool: "query_industry_data", skillId: "hithink-industry-query", title: "行业数据查询", desc: "查询行业估值、财务、盈利、行情、板块排名等数据。输入自然语言问句。" },
|
|
69
|
+
{ tool: "query_finance_data", skillId: "hithink-finance-query", title: "财务数据查询", desc: "查询全市场个股营业收入、净利润、ROE、负债率、现金流等财务指标。输入自然语言问句。" },
|
|
70
|
+
{ tool: "query_macro_data", skillId: "hithink-macro-query", title: "宏观数据查询", desc: "查询 GDP、CPI、PPI、利率、汇率、社融、M2 等宏观经济指标。输入自然语言问句。" },
|
|
71
|
+
{ tool: "query_index_data", skillId: "hithink-zhishu-query", title: "指数数据查询", desc: "查询上证指数、沪深300、创业板指、恒生指数、纳斯达克指数等指数行情(涨跌幅/成交量/点位)。输入自然语言问句。" },
|
|
72
|
+
{ tool: "query_fund_data", skillId: "hithink-fund-query", title: "基金理财查询", desc: "对基金做业绩、持仓、风险、评级、获奖、基金经理、基金公司综合分析。输入自然语言问句。" },
|
|
73
|
+
{ tool: "query_futures_data", skillId: "hithink-futures-query", title: "期货期权数据查询", desc: "查询期货期权行情、波动率、产销、会员持仓、会员榜单、行权等数据。输入自然语言问句。" },
|
|
74
|
+
{ tool: "query_insresearch_data", skillId: "hithink-insresearch-query", title: "机构研究与评级查询", desc: "查询研报评级、业绩预测、ESG、信用评级、主体评级、基金评级、券商金股等机构观点数据。输入自然语言问句。" },
|
|
75
|
+
{ tool: "query_event_data", skillId: "hithink-event-query", title: "事件数据查询", desc: "查询个股业绩预告、增发、质押、解禁、调研、监管函等事件数据。输入自然语言问句。" },
|
|
76
|
+
{ tool: "query_business_data", skillId: "hithink-business-query", title: "公司经营数据查询", desc: "查询主营业务构成、主要客户、供应商、参控股公司、股权投资、重大合同等经营数据。输入自然语言问句。" },
|
|
77
|
+
{ tool: "query_management_data", skillId: "hithink-management-query", title: "公司股东股本查询", desc: "查询股本结构、股权结构、股东户数、前十大股东/流通股东、主要持有人、实控人等股权信息。输入自然语言问句。" },
|
|
78
|
+
{ tool: "query_basicinfo_data", skillId: "hithink-basicinfo-query", title: "基本资料查询", desc: "查询全品类标的(股票、指数、基金、期货、期权、转债、债券、理财、保险等)基础信息、发行主体、机构资料、费率、上市地点/日期等静态信息。输入自然语言问句。" },
|
|
79
|
+
{ tool: "select_astock", skillId: "hithink-astock-selector", title: "问财选A股", desc: "通过自然语言查询进行 A 股筛选,支持行情指标、技术形态、财务指标、行业概念等多条件组合筛选。" },
|
|
80
|
+
{ tool: "select_etf", skillId: "hithink-etf-selector", title: "问财选ETF", desc: "根据行情、跟踪指数基本面、规模、风格类型等条件筛选 ETF。" },
|
|
81
|
+
{ tool: "select_cb", skillId: "hithink-cb-selector", title: "问财选可转债", desc: "通过转股溢价率、正股表现、评级、剩余期限等多条件组合筛选可转债。" },
|
|
82
|
+
{ tool: "select_fund", skillId: "hithink-fund-selector", title: "问财选基金", desc: "根据基金类型、业绩、基金经理、风险、持仓、资产配置等维度筛选公募基金。" },
|
|
83
|
+
{ tool: "select_fundcompany", skillId: "hithink-fundcompany-selector", title: "问财选基金公司", desc: "根据管理规模、旗下产品业绩、投研实力、风险评级等维度筛选公募基金公司。" },
|
|
84
|
+
{ tool: "select_fundmanager", skillId: "hithink-fundmanager-selector", title: "问财选基金经理", desc: "根据历史业绩、管理规模、投资风格、风险控制等维度筛选公募基金经理。" },
|
|
85
|
+
{ tool: "select_futures", skillId: "hithink-futures-selector", title: "问财选期货期权", desc: "通过行情、波动率、产销、会员持仓、会员榜单、行权等多条件组合筛选期货期权。" },
|
|
86
|
+
{ tool: "select_sector", skillId: "hithink-sector-selector", title: "问财选板块", desc: "通过行业估值、资金流向、涨跌幅、板块类型等多条件组合筛选市场板块。" },
|
|
87
|
+
{ tool: "select_hkstock", skillId: "hithink-hkstock-selector", title: "问财选港股", desc: "通过自然语言查询进行港股筛选,支持行情指标、财务指标、行业概念、陆港通等多条件组合筛选。" },
|
|
88
|
+
{ tool: "select_usstock", skillId: "hithink-usstock-selector", title: "问财选美股", desc: "通过自然语言查询进行美股筛选,支持行情指标、财务指标、行业概念、业绩预测、研报评级等多条件组合筛选。" },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// ---- search 系(3 个) ----
|
|
92
|
+
type SearchChannel = "news" | "announcement" | "report";
|
|
93
|
+
const SEARCH_TOOLS: {
|
|
94
|
+
tool: string;
|
|
95
|
+
channel: SearchChannel;
|
|
96
|
+
skillId: string;
|
|
97
|
+
title: string;
|
|
98
|
+
desc: string;
|
|
99
|
+
}[] = [
|
|
100
|
+
{ tool: "search_news", channel: "news", skillId: "news-search", title: "财经资讯搜索", desc: "财经为主的资讯搜索引擎,覆盖官媒、主流财经媒体、垂直行业网站、知名上市公司/非上市公司官网。返回标题、URL、摘要、来源、发布时间。" },
|
|
101
|
+
{ tool: "search_announcements", channel: "announcement", skillId: "announcement-search", title: "公告搜索", desc: "查询 A股、港股、基金、ETF 等金融标的公告,类型含定期财务报告、分红派息、回购增持、资产重组等。返回标题、链接、摘要、公告日期。" },
|
|
102
|
+
{ tool: "search_reports", channel: "report", skillId: "report-search", title: "研报搜索", desc: "收录主流投研机构发布的研究报告,返回分析逻辑、投资评级、目标价等投研决策信息。" },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const querySchema = {
|
|
106
|
+
query: z.string().describe("自然语言查询问句(可被改写为标准金融问句)"),
|
|
107
|
+
page: z.string().optional().describe("分页参数,默认 1"),
|
|
108
|
+
limit: z.string().optional().describe("每页条数,默认 10"),
|
|
109
|
+
};
|
|
110
|
+
const searchSchema = {
|
|
111
|
+
query: z.string().describe("自然语言搜索问句"),
|
|
112
|
+
size: z.string().optional().describe("返回条数,默认 10"),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const server = new McpServer({ name: "iwencai", version: "0.2.0" });
|
|
116
|
+
|
|
117
|
+
for (const t of QUERY_TOOLS) {
|
|
118
|
+
server.registerTool(
|
|
119
|
+
t.tool,
|
|
120
|
+
{
|
|
121
|
+
title: `${t.title}(同花顺问财)`,
|
|
122
|
+
description: `${t.desc}\n${ATTRIBUTION}`,
|
|
123
|
+
inputSchema: querySchema,
|
|
124
|
+
},
|
|
125
|
+
async (args) => safe(() => query2data(args, options, t.skillId))
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const t of SEARCH_TOOLS) {
|
|
130
|
+
server.registerTool(
|
|
131
|
+
t.tool,
|
|
132
|
+
{
|
|
133
|
+
title: `${t.title}(同花顺问财)`,
|
|
134
|
+
description: `${t.desc}\n${ATTRIBUTION}`,
|
|
135
|
+
inputSchema: searchSchema,
|
|
136
|
+
},
|
|
137
|
+
async (args) => safe(() => search(args, options, t.channel))
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const transport = new StdioServerTransport();
|
|
142
|
+
await server.connect(transport);
|