linker-wind-mcp 0.1.0 → 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/dist/index.js +26 -44
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
// 自有后端 wind 域入口(经网关)。正式:http://hub.linker.net/api/v1/mcp/wind
|
|
6
|
+
// 后端负责拼 Wind 代码串(含转义防注入)+ 调上游,回传 Wind 原始结构;
|
|
7
|
+
// 本客户端只传结构化参数 + 渲染。上游 wind-api 地址已收进服务端,客户端不再持有。
|
|
5
8
|
const BASE = (process.env.WIND_API_BASE ?? "").replace(/\/$/, "");
|
|
9
|
+
const API_KEY = process.env.WIND_API_KEY ?? "";
|
|
6
10
|
if (!BASE) {
|
|
7
11
|
process.stderr.write("WIND_API_BASE is required\n");
|
|
8
12
|
process.exit(1);
|
|
@@ -14,13 +18,13 @@ catch {
|
|
|
14
18
|
process.stderr.write(`WIND_API_BASE 格式无效: "${BASE}",必须是合法的 http/https URL\n`);
|
|
15
19
|
process.exit(1);
|
|
16
20
|
}
|
|
17
|
-
async function
|
|
21
|
+
async function windCall(path, body, timeout = 30) {
|
|
18
22
|
let res;
|
|
19
23
|
try {
|
|
20
|
-
res = await fetch(`${BASE}
|
|
24
|
+
res = await fetch(`${BASE}${path}`, {
|
|
21
25
|
method: "POST",
|
|
22
|
-
headers: { "Content-Type": "application/json" },
|
|
23
|
-
body: JSON.stringify(
|
|
26
|
+
headers: { "Content-Type": "application/json", "X-MCP-Key": API_KEY },
|
|
27
|
+
body: JSON.stringify(body),
|
|
24
28
|
signal: AbortSignal.timeout((timeout + 5) * 1000),
|
|
25
29
|
});
|
|
26
30
|
}
|
|
@@ -31,12 +35,18 @@ async function windExecute(code, timeout = 30) {
|
|
|
31
35
|
}
|
|
32
36
|
if (!res.ok)
|
|
33
37
|
throw new Error(`Wind服务返回 ${res.status}`);
|
|
38
|
+
let body_json;
|
|
34
39
|
try {
|
|
35
|
-
|
|
40
|
+
body_json = (await res.json());
|
|
36
41
|
}
|
|
37
42
|
catch {
|
|
38
43
|
throw new Error(`Wind服务返回了非JSON响应(可能是网关错误或服务异常),HTTP ${res.status}`);
|
|
39
44
|
}
|
|
45
|
+
// 后端连接/入参错 → code=502 + message;Wind 业务失败原样在 data 里(success=false)
|
|
46
|
+
if (body_json.code != null && body_json.code !== 200) {
|
|
47
|
+
throw new Error(body_json.message || `后端 code=${body_json.code}`);
|
|
48
|
+
}
|
|
49
|
+
return (body_json.data ?? body_json);
|
|
40
50
|
}
|
|
41
51
|
function text(data) {
|
|
42
52
|
return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data) }] };
|
|
@@ -59,10 +69,6 @@ function fmtCell(v) {
|
|
|
59
69
|
const f = fmt(v);
|
|
60
70
|
return f != null ? String(f) : String(v);
|
|
61
71
|
}
|
|
62
|
-
// 转义拼入 Wind 代码串的字符串,防止引号破坏语法或注入
|
|
63
|
-
function esc(s) {
|
|
64
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
65
|
-
}
|
|
66
72
|
// 日期格式校验 YYYY-MM-DD
|
|
67
73
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
68
74
|
// 日期时间格式校验 YYYY-MM-DD HH:MM:SS
|
|
@@ -148,17 +154,11 @@ server.tool("get_wind_daily", "查询期货/股票/指数的Wind历史数据(w
|
|
|
148
154
|
return errText(dateErr);
|
|
149
155
|
if (codes.length > 1 && fields.length > 1)
|
|
150
156
|
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
157
|
try {
|
|
158
|
-
const res = await
|
|
158
|
+
const res = await windCall("/daily", { codes, fields, start_date, end_date, period, options });
|
|
159
159
|
if (!res.success)
|
|
160
160
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
161
|
-
const meta = `wsd | ${
|
|
161
|
+
const meta = `wsd | ${codes.join(",")} | ${fields.join(",")} | ${start_date}~${end_date}${period ? ` | period=${period}` : ""}`;
|
|
162
162
|
return text(toTsv(res, meta));
|
|
163
163
|
}
|
|
164
164
|
catch (e) {
|
|
@@ -181,17 +181,11 @@ server.tool("get_wind_minutes", "查询期货合约的Wind分钟级行情数据
|
|
|
181
181
|
const dtErr = validateDatetime(start_time, "start_time") ?? validateDatetime(end_time, "end_time");
|
|
182
182
|
if (dtErr)
|
|
183
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
184
|
try {
|
|
191
|
-
const res = await
|
|
185
|
+
const res = await windCall("/minutes", { codes, fields, start_time, end_time, bar_size, options });
|
|
192
186
|
if (!res.success)
|
|
193
187
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
194
|
-
const meta = `wsi | ${
|
|
188
|
+
const meta = `wsi | ${codes.join(",")} | ${fields.join(",")} | ${start_time}~${end_time}${bar_size ? ` | bar=${bar_size}m` : ""}`;
|
|
195
189
|
return text(toTsv(res, meta));
|
|
196
190
|
}
|
|
197
191
|
catch (e) {
|
|
@@ -205,11 +199,8 @@ server.tool("get_wind_realtime", "查询期货/股票合约的Wind实时行情
|
|
|
205
199
|
}, { readOnlyHint: true }, async ({ codes, fields }) => {
|
|
206
200
|
if (!codes.length)
|
|
207
201
|
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
202
|
try {
|
|
212
|
-
const res = await
|
|
203
|
+
const res = await windCall("/realtime", { codes, fields });
|
|
213
204
|
if (!res.success)
|
|
214
205
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
215
206
|
const resCodes = res.codes ?? codes;
|
|
@@ -243,12 +234,8 @@ server.tool("get_wind_cross_section", "查询多个合约在同一时点的横
|
|
|
243
234
|
return errText("codes 不能为空");
|
|
244
235
|
if (!fields.length)
|
|
245
236
|
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
237
|
try {
|
|
251
|
-
const res = await
|
|
238
|
+
const res = await windCall("/cross-section", { codes, fields, options });
|
|
252
239
|
if (!res.success)
|
|
253
240
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
254
241
|
const resCodes = res.codes ?? codes;
|
|
@@ -257,7 +244,7 @@ server.tool("get_wind_cross_section", "查询多个合约在同一时点的横
|
|
|
257
244
|
if (!data)
|
|
258
245
|
return text("暂无横截面数据");
|
|
259
246
|
// wss: data[fi][ci] = 第 fi 个 field,第 ci 个 code 的值
|
|
260
|
-
const lines = [`# wss | ${
|
|
247
|
+
const lines = [`# wss | ${codes.join(",")} | ${fields.join(",")}${options ? ` | ${options}` : ""}`, `code\t${resFields.join("\t")}`];
|
|
261
248
|
for (let ci = 0; ci < resCodes.length; ci++) {
|
|
262
249
|
const row = [resCodes[ci]];
|
|
263
250
|
let allNull = true;
|
|
@@ -281,10 +268,8 @@ server.tool("get_wind_sector", "查询Wind板块成分列表(w.wset),如
|
|
|
281
268
|
set_name: z.string().describe("板块名称,如 \"futurecc\" 表示期货合约列表,\"sectorconstituent\" 表示指数成分"),
|
|
282
269
|
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
270
|
}, { readOnlyHint: true }, async ({ set_name, options }) => {
|
|
284
|
-
const opts = options ? `, "${esc(options)}"` : "";
|
|
285
|
-
const code = `w.wset('${esc(set_name)}'${opts})`;
|
|
286
271
|
try {
|
|
287
|
-
const res = await
|
|
272
|
+
const res = await windCall("/sector", { set_name, options });
|
|
288
273
|
if (!res.success)
|
|
289
274
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
290
275
|
const fields = res.fields ?? [];
|
|
@@ -319,14 +304,11 @@ server.tool("get_wind_edb", "查询Wind经济数据库(w.edb)的宏观/产
|
|
|
319
304
|
const dateErr = validateDate(start_date, "start_date") ?? validateDate(end_date, "end_date");
|
|
320
305
|
if (dateErr)
|
|
321
306
|
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
307
|
try {
|
|
326
|
-
const res = await
|
|
308
|
+
const res = await windCall("/edb", { codes, start_date, end_date, options });
|
|
327
309
|
if (!res.success)
|
|
328
310
|
return errText(`Wind查询失败: ${res.error ?? `error_code=${res.error_code}`}`);
|
|
329
|
-
const meta = `edb | ${
|
|
311
|
+
const meta = `edb | ${codes.join(",")} | ${start_date}~${end_date}`;
|
|
330
312
|
return text(toTsv(res, meta));
|
|
331
313
|
}
|
|
332
314
|
catch (e) {
|
|
@@ -336,4 +318,4 @@ server.tool("get_wind_edb", "查询Wind经济数据库(w.edb)的宏观/产
|
|
|
336
318
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
337
319
|
const transport = new StdioServerTransport();
|
|
338
320
|
await server.connect(transport);
|
|
339
|
-
console.error("linker-wind-mcp 已启动, WIND_API_BASE=" + BASE);
|
|
321
|
+
console.error("linker-wind-mcp 已启动, WIND_API_BASE=" + BASE + (API_KEY ? " (keyed)" : " (no key)"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linker-wind-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Wind financial data (wsd/wsi/wsq/wss/wset/edb)",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for Wind financial data (wsd/wsi/wsq/wss/wset/edb) via Linker gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"linker-wind-mcp": "dist/index.js"
|