linker-wind-mcp 0.1.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 +250 -0
- package/dist/index.js +339 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# linker-wind-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for Wind 金融数据终端,提供 6 个 Wind API 工具。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
| 工具 | Wind API | 格式 | 用途 |
|
|
8
|
+
|------|----------|------|------|
|
|
9
|
+
| `get_wind_daily` | w.wsd() | TSV | 日频历史数据(开/高/低/收/量/仓) |
|
|
10
|
+
| `get_wind_minutes` | w.wsi() | TSV | 分钟级K线(1/5/15/30/60分钟) |
|
|
11
|
+
| `get_wind_realtime` | w.wsq() | JSON | 实时行情快照(最新价/买卖盘/成交量) |
|
|
12
|
+
| `get_wind_cross_section` | w.wss() | TSV | 横截面批量数据(多合约同一时点) |
|
|
13
|
+
| `get_wind_sector` | w.wset() | JSON | 板块成分列表(交易所合约/指数) |
|
|
14
|
+
| `get_wind_edb` | w.edb() | TSV | 宏观/产业经济指标时间序列 |
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
npm run build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 配置
|
|
24
|
+
|
|
25
|
+
### Claude Desktop
|
|
26
|
+
|
|
27
|
+
编辑 `claude_desktop_config.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"linker-wind": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "linker-wind-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"WIND_API_BASE": "http://api_ai.linker.net/wind-api"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**本地调试**:将 `WIND_API_BASE` 改为 `http://192.168.10.200:10010`
|
|
44
|
+
|
|
45
|
+
### Cursor
|
|
46
|
+
|
|
47
|
+
编辑项目根目录 `.cursor/mcp.json`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"linker-wind": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "linker-wind-mcp"],
|
|
55
|
+
"env": {
|
|
56
|
+
"WIND_API_BASE": "http://192.168.10.200:10010"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Claude Code
|
|
64
|
+
|
|
65
|
+
在项目根目录创建 `.mcp.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"linker-wind": {
|
|
71
|
+
"type": "stdio",
|
|
72
|
+
"command": "npx",
|
|
73
|
+
"args": ["-y", "linker-wind-mcp"],
|
|
74
|
+
"env": {
|
|
75
|
+
"WIND_API_BASE": "http://192.168.10.200:10010"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
或使用命令行:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp add linker-wind --env WIND_API_BASE=http://192.168.10.200:10010 -- npx -y linker-wind-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 使用示例
|
|
89
|
+
|
|
90
|
+
### 1. 日频行情(wsd)
|
|
91
|
+
|
|
92
|
+
**单合约多字段**:
|
|
93
|
+
```
|
|
94
|
+
用户: PG2609.DCE 最近5天的开高低收量仓
|
|
95
|
+
AI调用: get_wind_daily({
|
|
96
|
+
codes: ["PG2609.DCE"],
|
|
97
|
+
fields: ["open","high","low","close","volume","oi"],
|
|
98
|
+
start_date: "2026-07-14",
|
|
99
|
+
end_date: "2026-07-18"
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**多合约单字段**:
|
|
104
|
+
```
|
|
105
|
+
用户: 对比 PG2609、PG2611、PG2701 三个合约的收盘价
|
|
106
|
+
AI调用: get_wind_daily({
|
|
107
|
+
codes: ["PG2609.DCE","PG2611.DCE","PG2701.DCE"],
|
|
108
|
+
fields: ["close"],
|
|
109
|
+
start_date: "2026-07-14",
|
|
110
|
+
end_date: "2026-07-18"
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. 分钟行情(wsi)
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
用户: PG2609 今天上午9点到10点的5分钟K线
|
|
118
|
+
AI调用: get_wind_minutes({
|
|
119
|
+
codes: ["PG2609.DCE"],
|
|
120
|
+
fields: ["open","high","low","close","volume"],
|
|
121
|
+
start_time: "2026-07-17 09:00:00",
|
|
122
|
+
end_time: "2026-07-17 10:00:00",
|
|
123
|
+
options: "BarSize=5"
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3. 实时行情(wsq)
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
用户: PG2609、M2509、PP2509 现在的价格和成交量
|
|
131
|
+
AI调用: get_wind_realtime({
|
|
132
|
+
codes: ["PG2609.DCE","M2509.DCE","PP2509.DCE"]
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
返回 JSON,默认字段:`rt_last`(最新价)、`rt_bid1`(买一)、`rt_ask1`(卖一)、`rt_vol`(成交量)
|
|
137
|
+
|
|
138
|
+
### 4. 横截面数据(wss)
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
用户: 今天所有 PG 合约的收盘价和持仓量
|
|
142
|
+
AI调用: get_wind_cross_section({
|
|
143
|
+
codes: ["PG2609.DCE","PG2611.DCE","PG2701.DCE"],
|
|
144
|
+
fields: ["close","oi"],
|
|
145
|
+
options: "tradeDate=2026-07-17"
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 5. 板块成分(wset)
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
用户: PG(液化石油气)有哪些期货合约
|
|
153
|
+
AI调用: get_wind_sector({
|
|
154
|
+
set_name: "futurecc",
|
|
155
|
+
options: "wind_code=PG.DCE;date=2026-07-17"
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> `futurecc` 用**品种代码**(如 `PG.DCE`、`CU.SHF`)而非交易所名。查指数成分用 `sectorconstituent` + `windcode=000300.SH`。
|
|
160
|
+
|
|
161
|
+
### 6. 经济指标(edb)
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
用户: 查询 M0067855 指标最近一周的数据
|
|
165
|
+
AI调用: get_wind_edb({
|
|
166
|
+
codes: ["M0067855"],
|
|
167
|
+
start_date: "2026-07-14",
|
|
168
|
+
end_date: "2026-07-18"
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 测试
|
|
173
|
+
|
|
174
|
+
### 方法一:直接测试 API 逻辑
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
node test_tool.mjs
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 方法二:curl 测后端
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
curl -X POST http://192.168.10.200:10010/execute \
|
|
184
|
+
-H "Content-Type: application/json" \
|
|
185
|
+
-d '{"code":"w.wsd(\"PG2609.DCE\", \"close\", \"2026-07-14\", \"2026-07-18\")","timeout":30}'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 方法三:MCP stdio 协议测试
|
|
189
|
+
|
|
190
|
+
参考 `testing.md` 中的 JSON-RPC 测试脚本。
|
|
191
|
+
|
|
192
|
+
## 实测结果
|
|
193
|
+
|
|
194
|
+
| 测试项 | 结果 |
|
|
195
|
+
|-------|------|
|
|
196
|
+
| wsd 单合约多字段 | ✅ 6字段4天数据 |
|
|
197
|
+
| wsd 多合约单字段 | ✅ 3合约4天收盘价(M2509/PP2509全null - 非交易日) |
|
|
198
|
+
| wsi 5分钟K线 | ✅ 12条5分钟数据 |
|
|
199
|
+
| wsq 实时行情 | ✅ 3合约实时报价(bid/ask为0 - 非交易时段) |
|
|
200
|
+
| wss 横截面 | ✅ 3合约收盘价+持仓量 |
|
|
201
|
+
| MCP stdio 端到端 | ✅ readOnlyHint标注生效,TSV/JSON格式正确 |
|
|
202
|
+
|
|
203
|
+
## 已知限制
|
|
204
|
+
|
|
205
|
+
| 限制 | 说明 |
|
|
206
|
+
|------|------|
|
|
207
|
+
| **多合约+多字段** | Wind 不支持同时多合约多字段(error_code=-40522018),单次只能"单合约多字段"或"多合约单字段" |
|
|
208
|
+
| **精度** | 数值保留2位小数(遵循规范),汇率等高精度数据会从 6.7779 截为 6.78 |
|
|
209
|
+
| **非交易时段** | wsq 实时行情在非交易时段返回上一交易日收盘数据,bid/ask 为 0 |
|
|
210
|
+
| **到期合约** | 已到期合约查询返回全 null,TSV 会跳过整行(null行不输出) |
|
|
211
|
+
| **无鉴权** | Wind API 当前无认证机制,依赖网络隔离保障安全 |
|
|
212
|
+
|
|
213
|
+
## 数据格式
|
|
214
|
+
|
|
215
|
+
### TSV(时间序列)
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
# wsd | PG2609.DCE | close,volume | 2026-07-14~2026-07-18
|
|
219
|
+
date CLOSE VOLUME
|
|
220
|
+
2026-07-14 5031 10825
|
|
221
|
+
2026-07-15 5060 6325
|
|
222
|
+
2026-07-16 5061 7945
|
|
223
|
+
2026-07-17 5102 8136
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- 首行 `#` 元信息供 LLM 引用
|
|
227
|
+
- 列名行仅一次
|
|
228
|
+
- 数值 2 位小数
|
|
229
|
+
- null 行跳过
|
|
230
|
+
|
|
231
|
+
### JSON(快照/变结构)
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
[
|
|
235
|
+
{"contract":"PG2609.DCE","rt_last":5558,"rt_bid1":5556,"rt_ask1":5559,"rt_vol":15329}
|
|
236
|
+
]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
用于 wsq 实时快照和 wset 板块成分(字段不固定)。
|
|
240
|
+
|
|
241
|
+
## 开发
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm run build # 编译
|
|
245
|
+
npm run watch # 监听模式
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const BASE = (process.env.WIND_API_BASE ?? "").replace(/\/$/, "");
|
|
6
|
+
if (!BASE) {
|
|
7
|
+
process.stderr.write("WIND_API_BASE is required\n");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
new URL(BASE);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
process.stderr.write(`WIND_API_BASE 格式无效: "${BASE}",必须是合法的 http/https URL\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
async function windExecute(code, timeout = 30) {
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetch(`${BASE}/execute`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ code, timeout }),
|
|
24
|
+
signal: AbortSignal.timeout((timeout + 5) * 1000),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e instanceof Error && e.name === "TimeoutError")
|
|
29
|
+
throw new Error(`Wind服务响应超时(>${timeout + 5}秒),请检查服务是否可用或缩小查询范围`);
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
throw new Error(`Wind服务返回 ${res.status}`);
|
|
34
|
+
try {
|
|
35
|
+
return (await res.json());
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Wind服务返回了非JSON响应(可能是网关错误或服务异常),HTTP ${res.status}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function text(data) {
|
|
42
|
+
return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data) }] };
|
|
43
|
+
}
|
|
44
|
+
function errText(msg) {
|
|
45
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
46
|
+
}
|
|
47
|
+
function fmt(v) {
|
|
48
|
+
if (v == null)
|
|
49
|
+
return null;
|
|
50
|
+
const n = Number(v);
|
|
51
|
+
if (isNaN(n))
|
|
52
|
+
return null;
|
|
53
|
+
return Number.isInteger(n) ? n : parseFloat(n.toFixed(2));
|
|
54
|
+
}
|
|
55
|
+
// 单元格格式化:数值走 fmt,非数值(合约名、状态等文本)原样保留,避免静默丢失
|
|
56
|
+
function fmtCell(v) {
|
|
57
|
+
if (v == null)
|
|
58
|
+
return "";
|
|
59
|
+
const f = fmt(v);
|
|
60
|
+
return f != null ? String(f) : String(v);
|
|
61
|
+
}
|
|
62
|
+
// 转义拼入 Wind 代码串的字符串,防止引号破坏语法或注入
|
|
63
|
+
function esc(s) {
|
|
64
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
65
|
+
}
|
|
66
|
+
// 日期格式校验 YYYY-MM-DD
|
|
67
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
68
|
+
// 日期时间格式校验 YYYY-MM-DD HH:MM:SS
|
|
69
|
+
const DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
70
|
+
function validateDate(v, label) {
|
|
71
|
+
if (!DATE_RE.test(v))
|
|
72
|
+
return `${label} 格式错误:"${v}",需要 YYYY-MM-DD 格式`;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function validateDatetime(v, label) {
|
|
76
|
+
if (!DATETIME_RE.test(v))
|
|
77
|
+
return `${label} 格式错误:"${v}",需要 YYYY-MM-DD HH:MM:SS 格式`;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// 每 step 行取最后一行做降采样(收盘价语义取周期末值)
|
|
81
|
+
function downsample(times, data, colCount, step) {
|
|
82
|
+
const outTimes = [];
|
|
83
|
+
const outData = Array.from({ length: colCount }, () => []);
|
|
84
|
+
for (let i = step - 1; i < times.length; i += step) {
|
|
85
|
+
outTimes.push(times[i]);
|
|
86
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
87
|
+
outData[ci].push(data[ci]?.[i] ?? null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { times: outTimes, data: outData };
|
|
91
|
+
}
|
|
92
|
+
const TSV_LIMIT = 200;
|
|
93
|
+
// 将 WindResponse 的 times+codes+fields+data 转为 TSV
|
|
94
|
+
// data 实际结构:二维数组 data[i][t]
|
|
95
|
+
// 单合约多字段:i = field 索引,列头用 fields
|
|
96
|
+
// 多合约单字段:i = code 索引,列头用 codes
|
|
97
|
+
function toTsv(res, meta) {
|
|
98
|
+
let times = res.times ?? [];
|
|
99
|
+
const codes = res.codes ?? [];
|
|
100
|
+
const fields = res.fields ?? [];
|
|
101
|
+
let data = res.data;
|
|
102
|
+
if (!data || !times.length)
|
|
103
|
+
return `# ${meta}\n暂无数据`;
|
|
104
|
+
// 列标签:多合约时用 code,否则用 field
|
|
105
|
+
const colLabels = codes.length > 1 ? codes : fields;
|
|
106
|
+
let sampledNote = "";
|
|
107
|
+
// 超200点自动降采样(规范要求);step 动态计算确保输出不超过 TSV_LIMIT
|
|
108
|
+
if (times.length > TSV_LIMIT) {
|
|
109
|
+
const orig = times.length;
|
|
110
|
+
const step = Math.ceil(orig / TSV_LIMIT);
|
|
111
|
+
const ds = downsample(times, data, colLabels.length, step);
|
|
112
|
+
times = ds.times;
|
|
113
|
+
data = ds.data;
|
|
114
|
+
sampledNote = ` [已降采样 ${orig}点→${times.length}点,建议改用 period=W 或 period=M]`;
|
|
115
|
+
}
|
|
116
|
+
const lines = [`# ${meta}${sampledNote}`, `date\t${colLabels.join("\t")}`];
|
|
117
|
+
for (let ti = 0; ti < times.length; ti++) {
|
|
118
|
+
const row = [times[ti]];
|
|
119
|
+
let allNull = true;
|
|
120
|
+
for (let i = 0; i < colLabels.length; i++) {
|
|
121
|
+
const cell = fmtCell(data[i]?.[ti]);
|
|
122
|
+
if (cell !== "")
|
|
123
|
+
allNull = false;
|
|
124
|
+
row.push(cell);
|
|
125
|
+
}
|
|
126
|
+
if (!allNull)
|
|
127
|
+
lines.push(row.join("\t"));
|
|
128
|
+
}
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
132
|
+
const server = new McpServer({ name: "linker-wind-mcp", version: "0.1.0" });
|
|
133
|
+
// 1. get_wind_daily — 日频历史数据 (w.wsd)
|
|
134
|
+
server.tool("get_wind_daily", "查询期货/股票/指数的Wind历史数据(w.wsd),支持日/周/月频率。注意:多合约只能查单字段,单合约可查多字段。返回TSV格式时间序列。超过200点自动降采样并提示。适合'某合约历史价格'、'近一个月涨跌幅'、'一年周频数据'等场景。", {
|
|
135
|
+
codes: z.array(z.string()).describe("合约/证券代码列表,如 [\"PG2606.DCE\", \"M2509.DCE\"]"),
|
|
136
|
+
fields: z.array(z.string()).describe("字段列表,如 [\"close\",\"volume\",\"oi\"],常用:close=收盘价 open=开盘价 high=最高 low=最低 volume=成交量 oi=持仓量"),
|
|
137
|
+
start_date: z.string().describe("开始日期 YYYY-MM-DD"),
|
|
138
|
+
end_date: z.string().describe("结束日期 YYYY-MM-DD"),
|
|
139
|
+
period: z.enum(["D", "W", "M"]).optional().describe("频率:D=日(默认), W=周, M=月。查询超过3个月建议用W,超过1年建议用M"),
|
|
140
|
+
options: z.string().optional().describe("Wind附加参数,如 \"PriceAdj=F\" 表示前复权,period参数已覆盖频率设置"),
|
|
141
|
+
}, { readOnlyHint: true }, async ({ codes, fields, start_date, end_date, period, options }) => {
|
|
142
|
+
if (!codes.length)
|
|
143
|
+
return errText("codes 不能为空");
|
|
144
|
+
if (!fields.length)
|
|
145
|
+
return errText("fields 不能为空");
|
|
146
|
+
const dateErr = validateDate(start_date, "start_date") ?? validateDate(end_date, "end_date");
|
|
147
|
+
if (dateErr)
|
|
148
|
+
return errText(dateErr);
|
|
149
|
+
if (codes.length > 1 && fields.length > 1)
|
|
150
|
+
return errText("Wind 不支持同时多合约+多字段(error_code=-40522018),请改为单合约多字段或多合约单字段");
|
|
151
|
+
const codesStr = esc(codes.join(","));
|
|
152
|
+
const fieldsStr = esc(fields.join(","));
|
|
153
|
+
const periodOpt = period && period !== "D" ? `Period=${period}` : null;
|
|
154
|
+
const allOpts = [periodOpt, options].filter(Boolean).join(";");
|
|
155
|
+
const opts = allOpts ? `, "${esc(allOpts)}"` : "";
|
|
156
|
+
const code = `w.wsd('${codesStr}', '${fieldsStr}', '${esc(start_date)}', '${esc(end_date)}'${opts})`;
|
|
157
|
+
try {
|
|
158
|
+
const res = await windExecute(code);
|
|
159
|
+
if (!res.success)
|
|
160
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
161
|
+
const meta = `wsd | ${codesStr} | ${fieldsStr} | ${start_date}~${end_date}${period ? ` | period=${period}` : ""}`;
|
|
162
|
+
return text(toTsv(res, meta));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// 2. get_wind_minutes — 分钟级数据 (w.wsi)
|
|
169
|
+
server.tool("get_wind_minutes", "查询期货合约的Wind分钟级行情数据(w.wsi),支持1/5/15/30/60分钟频率。返回TSV格式时间序列。适合'今日盘中走势'、'某时间段分钟行情'等场景。", {
|
|
170
|
+
codes: z.array(z.string()).describe("合约代码列表,如 [\"PG2606.DCE\"]"),
|
|
171
|
+
fields: z.array(z.string()).describe("字段列表,如 [\"close\",\"volume\"]"),
|
|
172
|
+
start_time: z.string().describe("开始时间 YYYY-MM-DD HH:MM:SS"),
|
|
173
|
+
end_time: z.string().describe("结束时间 YYYY-MM-DD HH:MM:SS"),
|
|
174
|
+
bar_size: z.enum(["1", "5", "15", "30", "60"]).optional().describe("K线周期(分钟):1/5/15/30/60,默认1分钟"),
|
|
175
|
+
options: z.string().optional().describe("Wind附加参数,bar_size已覆盖BarSize设置"),
|
|
176
|
+
}, { readOnlyHint: true }, async ({ codes, fields, start_time, end_time, bar_size, options }) => {
|
|
177
|
+
if (!codes.length)
|
|
178
|
+
return errText("codes 不能为空");
|
|
179
|
+
if (!fields.length)
|
|
180
|
+
return errText("fields 不能为空");
|
|
181
|
+
const dtErr = validateDatetime(start_time, "start_time") ?? validateDatetime(end_time, "end_time");
|
|
182
|
+
if (dtErr)
|
|
183
|
+
return errText(dtErr);
|
|
184
|
+
const codesStr = esc(codes.join(","));
|
|
185
|
+
const fieldsStr = esc(fields.join(","));
|
|
186
|
+
const barOpt = bar_size && bar_size !== "1" ? `BarSize=${bar_size}` : null;
|
|
187
|
+
const allOpts = [barOpt, options].filter(Boolean).join(";");
|
|
188
|
+
const opts = allOpts ? `, "${esc(allOpts)}"` : "";
|
|
189
|
+
const code = `w.wsi('${codesStr}', '${fieldsStr}', '${esc(start_time)}', '${esc(end_time)}'${opts})`;
|
|
190
|
+
try {
|
|
191
|
+
const res = await windExecute(code);
|
|
192
|
+
if (!res.success)
|
|
193
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
194
|
+
const meta = `wsi | ${codesStr} | ${fieldsStr} | ${start_time}~${end_time}${bar_size ? ` | bar=${bar_size}m` : ""}`;
|
|
195
|
+
return text(toTsv(res, meta));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// 3. get_wind_realtime — 实时行情快照 (w.wsq)
|
|
202
|
+
server.tool("get_wind_realtime", "查询期货/股票合约的Wind实时行情快照(w.wsq),含最新价、买卖价、成交量等。适合'某合约现价'、'多个合约实时报价对比'等场景。", {
|
|
203
|
+
codes: z.array(z.string()).describe("合约代码列表,如 [\"PG2606.DCE\", \"M2509.DCE\"]"),
|
|
204
|
+
fields: z.array(z.string()).optional().describe("字段列表,留空默认查 [\"rt_last\",\"rt_bid1\",\"rt_ask1\",\"rt_vol\"],rt_last=最新价 rt_bid1=买一 rt_ask1=卖一 rt_vol=成交量"),
|
|
205
|
+
}, { readOnlyHint: true }, async ({ codes, fields }) => {
|
|
206
|
+
if (!codes.length)
|
|
207
|
+
return errText("codes 不能为空");
|
|
208
|
+
const codesStr = esc(codes.join(","));
|
|
209
|
+
const fieldsStr = esc((fields ?? ["rt_last", "rt_bid1", "rt_ask1", "rt_vol"]).join(","));
|
|
210
|
+
const code = `w.wsq('${codesStr}', '${fieldsStr}')`;
|
|
211
|
+
try {
|
|
212
|
+
const res = await windExecute(code);
|
|
213
|
+
if (!res.success)
|
|
214
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
215
|
+
const resCodes = res.codes ?? codes;
|
|
216
|
+
const resFields = res.fields ?? [];
|
|
217
|
+
const data = res.data;
|
|
218
|
+
if (!data)
|
|
219
|
+
return text("暂无实时行情数据");
|
|
220
|
+
// wsq: data[fi] = 第 fi 个字段在所有合约上的值数组,data[fi][ci] = 第 fi 字段第 ci 个合约的值
|
|
221
|
+
// 外层遍历合约(ci),内层遍历字段(fi),与 wss 的 data[fi][ci] 结构一致
|
|
222
|
+
const result = resCodes.map((c, ci) => {
|
|
223
|
+
const row = { contract: c };
|
|
224
|
+
for (let fi = 0; fi < resFields.length; fi++) {
|
|
225
|
+
const val = data[fi]?.[ci] ?? null;
|
|
226
|
+
row[resFields[fi].toLowerCase()] = fmt(val) ?? val;
|
|
227
|
+
}
|
|
228
|
+
return row;
|
|
229
|
+
});
|
|
230
|
+
return text(result);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// 4. get_wind_cross_section — 横截面数据 (w.wss)
|
|
237
|
+
server.tool("get_wind_cross_section", "查询多个合约在同一时点的横截面数据(w.wss),适合批量获取某日的基本面、财务或行情字段。返回TSV格式。适合'某日全市场持仓量排名'、'批量查合约基本信息'等场景。", {
|
|
238
|
+
codes: z.array(z.string()).describe("合约/证券代码列表,如 [\"PG2609.DCE\", \"PG2611.DCE\"]"),
|
|
239
|
+
fields: z.array(z.string()).describe("字段列表,如 [\"close\",\"oi\",\"contractmultiplier\"]"),
|
|
240
|
+
options: z.string().optional().describe("Wind附加参数,如 \"tradeDate=2026-07-18\" 指定查询日期"),
|
|
241
|
+
}, { readOnlyHint: true }, async ({ codes, fields, options }) => {
|
|
242
|
+
if (!codes.length)
|
|
243
|
+
return errText("codes 不能为空");
|
|
244
|
+
if (!fields.length)
|
|
245
|
+
return errText("fields 不能为空");
|
|
246
|
+
const codesStr = esc(codes.join(","));
|
|
247
|
+
const fieldsStr = esc(fields.join(","));
|
|
248
|
+
const opts = options ? `, "${esc(options)}"` : "";
|
|
249
|
+
const code = `w.wss('${codesStr}', '${fieldsStr}'${opts})`;
|
|
250
|
+
try {
|
|
251
|
+
const res = await windExecute(code);
|
|
252
|
+
if (!res.success)
|
|
253
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
254
|
+
const resCodes = res.codes ?? codes;
|
|
255
|
+
const resFields = res.fields ?? fields;
|
|
256
|
+
const data = res.data;
|
|
257
|
+
if (!data)
|
|
258
|
+
return text("暂无横截面数据");
|
|
259
|
+
// wss: data[fi][ci] = 第 fi 个 field,第 ci 个 code 的值
|
|
260
|
+
const lines = [`# wss | ${codesStr} | ${fieldsStr}${options ? ` | ${options}` : ""}`, `code\t${resFields.join("\t")}`];
|
|
261
|
+
for (let ci = 0; ci < resCodes.length; ci++) {
|
|
262
|
+
const row = [resCodes[ci]];
|
|
263
|
+
let allNull = true;
|
|
264
|
+
for (let fi = 0; fi < resFields.length; fi++) {
|
|
265
|
+
const cell = fmtCell(data[fi]?.[ci]);
|
|
266
|
+
if (cell !== "")
|
|
267
|
+
allNull = false;
|
|
268
|
+
row.push(cell);
|
|
269
|
+
}
|
|
270
|
+
if (!allNull)
|
|
271
|
+
lines.push(row.join("\t"));
|
|
272
|
+
}
|
|
273
|
+
return text(lines.join("\n"));
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// 5. get_wind_sector — 板块成分 (w.wset)
|
|
280
|
+
server.tool("get_wind_sector", "查询Wind板块成分列表(w.wset),如某品种所有期货合约、某指数成分股等。适合'PG有哪些合约'、'铜期货合约列表'等场景。", {
|
|
281
|
+
set_name: z.string().describe("板块名称,如 \"futurecc\" 表示期货合约列表,\"sectorconstituent\" 表示指数成分"),
|
|
282
|
+
options: z.string().optional().describe("Wind附加参数。futurecc用 \"wind_code=PG.DCE\" 查PG所有合约,\"wind_code=CU.SHF\" 查铜;sectorconstituent用 \"date=2026-07-17;windcode=000300.SH\" 查沪深300成分"),
|
|
283
|
+
}, { readOnlyHint: true }, async ({ set_name, options }) => {
|
|
284
|
+
const opts = options ? `, "${esc(options)}"` : "";
|
|
285
|
+
const code = `w.wset('${esc(set_name)}'${opts})`;
|
|
286
|
+
try {
|
|
287
|
+
const res = await windExecute(code);
|
|
288
|
+
if (!res.success)
|
|
289
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
290
|
+
const fields = res.fields ?? [];
|
|
291
|
+
const data = res.data;
|
|
292
|
+
if (!data || !fields.length)
|
|
293
|
+
return text("暂无板块数据");
|
|
294
|
+
// wset: data[fi] = 第 fi 个字段的所有行值,codes = 行序号
|
|
295
|
+
const rowCount = data[0]?.length ?? 0;
|
|
296
|
+
if (!rowCount)
|
|
297
|
+
return text("暂无板块数据");
|
|
298
|
+
const meta = `# wset | ${set_name}${options ? ` | ${options}` : ""} | ${rowCount}条`;
|
|
299
|
+
const lines = [meta, fields.join("\t")];
|
|
300
|
+
for (let ri = 0; ri < rowCount; ri++) {
|
|
301
|
+
const row = fields.map((_f, fi) => fmtCell(data[fi]?.[ri]));
|
|
302
|
+
lines.push(row.join("\t"));
|
|
303
|
+
}
|
|
304
|
+
return text(lines.join("\n"));
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
// 6. get_wind_edb — 经济数据库 (w.edb)
|
|
311
|
+
server.tool("get_wind_edb", "查询Wind经济数据库(w.edb)的宏观/产业时间序列数据,支持化工、能源、汇率、原油等品类指标。返回TSV格式。适合'丁二烯近一个月价格'、'布伦特原油历史数据'、'某宏观指标走势'等场景。", {
|
|
312
|
+
codes: z.array(z.string()).describe("EDB指标代码列表,如 [\"M0067855\",\"S0059742\"],代码可从Wind终端「数据浏览器」查询获取"),
|
|
313
|
+
start_date: z.string().describe("开始日期 YYYY-MM-DD"),
|
|
314
|
+
end_date: z.string().describe("结束日期 YYYY-MM-DD"),
|
|
315
|
+
options: z.string().optional().describe("Wind附加参数,如 \"Period=M\" 表示月频"),
|
|
316
|
+
}, { readOnlyHint: true }, async ({ codes, start_date, end_date, options }) => {
|
|
317
|
+
if (!codes.length)
|
|
318
|
+
return errText("codes 不能为空");
|
|
319
|
+
const dateErr = validateDate(start_date, "start_date") ?? validateDate(end_date, "end_date");
|
|
320
|
+
if (dateErr)
|
|
321
|
+
return errText(dateErr);
|
|
322
|
+
const codesStr = esc(codes.join(","));
|
|
323
|
+
const opts = options ? `, "${esc(options)}"` : "";
|
|
324
|
+
const code = `w.edb('${codesStr}', '${esc(start_date)}', '${esc(end_date)}'${opts})`;
|
|
325
|
+
try {
|
|
326
|
+
const res = await windExecute(code);
|
|
327
|
+
if (!res.success)
|
|
328
|
+
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
329
|
+
const meta = `edb | ${codesStr} | ${start_date}~${end_date}`;
|
|
330
|
+
return text(toTsv(res, meta));
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
return errText(`请求Wind服务失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
// ── start ─────────────────────────────────────────────────────────────────────
|
|
337
|
+
const transport = new StdioServerTransport();
|
|
338
|
+
await server.connect(transport);
|
|
339
|
+
console.error("linker-wind-mcp 已启动, WIND_API_BASE=" + BASE);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linker-wind-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Wind financial data (wsd/wsi/wsq/wss/wset/edb)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linker-wind-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"watch": "tsc --watch"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
17
|
+
"zod": "^3.24.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22",
|
|
21
|
+
"shx": "^0.3.4",
|
|
22
|
+
"typescript": "^5.8.2"
|
|
23
|
+
},
|
|
24
|
+
"engines": { "node": ">=18" },
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|