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.
Files changed (2) hide show
  1. package/dist/index.js +26 -44
  2. 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 windExecute(code, timeout = 30) {
21
+ async function windCall(path, body, timeout = 30) {
18
22
  let res;
19
23
  try {
20
- res = await fetch(`${BASE}/execute`, {
24
+ res = await fetch(`${BASE}${path}`, {
21
25
  method: "POST",
22
- headers: { "Content-Type": "application/json" },
23
- body: JSON.stringify({ code, timeout }),
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
- return (await res.json());
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 windExecute(code);
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 | ${codesStr} | ${fieldsStr} | ${start_date}~${end_date}${period ? ` | period=${period}` : ""}`;
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 windExecute(code);
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 | ${codesStr} | ${fieldsStr} | ${start_time}~${end_time}${bar_size ? ` | bar=${bar_size}m` : ""}`;
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 windExecute(code);
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 windExecute(code);
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 | ${codesStr} | ${fieldsStr}${options ? ` | ${options}` : ""}`, `code\t${resFields.join("\t")}`];
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 windExecute(code);
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 windExecute(code);
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 | ${codesStr} | ${start_date}~${end_date}`;
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.1.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"