pingan-securities-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ping An Securities Co., Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # pingan-securities-mcp
2
+
3
+ 平安证券 MCP Server。当前提供热股风向标(用户关注榜单),后续持续扩展行情、资讯、筛选等能力。
4
+
5
+ > 数据仅供参考,不构成投资建议。
6
+
7
+ ## 前置:申请 API Key
8
+
9
+ 登录 [平安证券 AI Skill 开放平台](https://stock.pingan.com/huodong/aiskill/skillPage/index.html) 申请 API Key。
10
+
11
+ ## 配置
12
+
13
+ 在支持 MCP 的客户端(Claude Desktop、Cherry Studio 等)中加入:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "pingan-securities": {
19
+ "command": "npx",
20
+ "args": ["-y", "pingan-securities-mcp"],
21
+ "env": {
22
+ "PINGAN_SKILL_APIKEY": "你的 API Key"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ 需要 Node.js 18 或以上。
30
+
31
+ ## 工具
32
+
33
+ ### `pa_hot_ranking` — 热股风向标
34
+
35
+ 平安证券用户群体的实时关注榜单,反映"大家都在关注什么"。
36
+
37
+ | 参数 | 取值 | 默认 | 说明 |
38
+ |---|---|---|---|
39
+ | `board` | `search` / `watchlist` / `browse` / `surge` | 必填 | 热搜榜 / 加自选榜 / 浏览榜 / 飙升榜 |
40
+ | `category` | `a_stock` / `etf` / `sector` / `hk_stock` / `convertible_bond` | `a_stock` | A股 / ETF / 板块概念 / 港股 / 可转债 |
41
+ | `time_range` | `1h` / `24h` | `24h` | 统计时间窗 |
42
+ | `size` | 1~100 | 10 | 返回条数 |
43
+
44
+ 四类榜单的含义:
45
+
46
+ - **热搜榜 / 浏览榜 / 加自选榜**衡量绝对关注量,关注深度依次递增:看一眼 → 主动找 → 持续跟踪
47
+ - **飙升榜**衡量关注度的增速,上榜标的绝对热度未必高
48
+
49
+ 榜单按热度排序,不是涨幅排序;反映平安证券用户行为,不代表全市场。
50
+
51
+ **使用示例**
52
+
53
+ - 「今天什么股票最热」→ `board=search`
54
+ - 「大家都在加什么自选」→ `board=watchlist`
55
+ - 「哪些板块关注度飙升」→ `board=surge, category=sector`
56
+ - 「港股这一小时大家在看什么」→ `board=browse, category=hk_stock, time_range=1h`
57
+
58
+ ## 环境变量
59
+
60
+ | 变量 | 必填 | 默认值 | 说明 |
61
+ |---|---|---|---|
62
+ | `PINGAN_SKILL_APIKEY` | 是 | — | API Key |
63
+ | `PA_GATEWAY_BASE_URL` | 否 | `https://ai.stock.pingan.com` | 服务地址 |
64
+ | `PA_GATEWAY_TIMEOUT` | 否 | `20` | 请求超时(秒) |
65
+ | `PA_CHANNEL_ID` | 否 | — | 渠道标识 |
66
+
67
+ ## 许可
68
+
69
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 平安证券 MCP Server —— STDIO 传输。
4
+ *
5
+ * 本文件只做两件事:创建 server、逐个注册工具。业务逻辑全在 tools/ 下,每个工具一个文件。
6
+ * 新增工具:在 tools/ 下建文件导出 registerXxx(server),然后在下面加一行。
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { registerHotStock } from "./tools/hot-stock.js";
11
+ export const SERVER_NAME = "pingan-securities";
12
+ export const SERVER_VERSION = "1.0.0";
13
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
14
+ instructions: "平安证券数据服务。涉及市场热度、榜单、行情等问题时,必须调用工具获取实时数据," +
15
+ "不要凭记忆回答具体排名、数字或涨跌幅。所有数据仅供参考,不构成投资建议。",
16
+ });
17
+ // ---- 工具注册(按上线顺序追加)----
18
+ registerHotStock(server);
19
+ async function main() {
20
+ // STDIO 下 stdout 是协议通道,任何 console.log 都会破坏 JSON-RPC 消息流。日志只能走 stderr。
21
+ await server.connect(new StdioServerTransport());
22
+ console.error(`[${SERVER_NAME}] MCP server started (stdio) v${SERVER_VERSION}`);
23
+ }
24
+ main().catch((e) => {
25
+ console.error(`[${SERVER_NAME}] fatal:`, e);
26
+ process.exit(1);
27
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 认证与错误类型。所有工具共用。
3
+ */
4
+ export const APIKEY_ENV = "PINGAN_SKILL_APIKEY";
5
+ export const APPLY_URL = "https://stock.pingan.com/huodong/aiskill/skillPage/index.html";
6
+ /** 预期内的业务失败。返回 isError=true 让模型读到并自纠正,而不是抛成协议错误。 */
7
+ export class ToolError extends Error {
8
+ }
9
+ /** 取用户 Key。STDIO 模式下由宿主注入环境变量。绝不放进工具入参——那会让 Key 进模型上下文。 */
10
+ export function apiKey() {
11
+ const key = (process.env[APIKEY_ENV] ?? "").trim();
12
+ if (!key) {
13
+ throw new ToolError(`未配置平安证券 API Key。请设置环境变量 ${APIKEY_ENV}。申请地址:${APPLY_URL}`);
14
+ }
15
+ return key;
16
+ }
17
+ export const ok = (text) => ({ content: [{ type: "text", text }] });
18
+ export const err = (text) => ({ content: [{ type: "text", text }], isError: true });
19
+ /** 统一包装:所有预期内的失败都走 isError,不让异常冒泡成协议错误。 */
20
+ export async function guard(fn) {
21
+ try {
22
+ return ok(await fn());
23
+ }
24
+ catch (e) {
25
+ if (e instanceof ToolError)
26
+ return err(e.message);
27
+ return err(`查询失败:${e?.message ?? String(e)}`);
28
+ }
29
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 格式化工具。所有工具共用。
3
+ *
4
+ * 工具返回的每个字符都进模型上下文,所以统一走窄表、带单位、大数折算、null 省略。
5
+ */
6
+ export const DISCLAIMER = "数据来源:平安证券。仅供参考,不构成投资建议。";
7
+ const toNum = (v) => {
8
+ if (v === null || v === undefined || v === "")
9
+ return null;
10
+ const f = Number(v);
11
+ return Number.isFinite(f) ? f : null;
12
+ };
13
+ const group = (f, digits) => f.toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits });
14
+ /** digits 不传时按量级自适应:ETF/低价股 3 位小数,个股 2 位。 */
15
+ export function num(v, digits) {
16
+ const f = toNum(v);
17
+ if (f === null)
18
+ return typeof v === "string" && v ? v : "—";
19
+ return group(f, digits ?? (Math.abs(f) < 10 ? 3 : 2));
20
+ }
21
+ /** 输入已是百分数(1.48 表示 1.48%)。 */
22
+ export function pct(v) {
23
+ const f = toNum(v);
24
+ if (f === null)
25
+ return "—";
26
+ return `${f >= 0 ? "+" : ""}${f.toFixed(2)}%`;
27
+ }
28
+ /** 输入是小数比例(0.0148 表示 1.48%)。网关不同接口两种都有,调用方按接口文档选。 */
29
+ export function ratioPct(v) {
30
+ const f = toNum(v);
31
+ if (f === null)
32
+ return "—";
33
+ return pct(f * 100);
34
+ }
35
+ /** 元 / 股 折算成亿、万。 */
36
+ export function big(v) {
37
+ const f = toNum(v);
38
+ if (f === null)
39
+ return "—";
40
+ const a = Math.abs(f);
41
+ if (a >= 1e8)
42
+ return `${group(f / 1e8, 2)}亿`;
43
+ if (a >= 1e4)
44
+ return `${group(f / 1e4, 2)}万`;
45
+ return group(f, 0);
46
+ }
47
+ export function table(headers, rows) {
48
+ const out = [`| ${headers.join(" | ")} |`, `|${headers.map(() => "---").join("|")}|`];
49
+ for (const r of rows)
50
+ out.push(`| ${r.join(" | ")} |`);
51
+ return out.join("\n");
52
+ }
53
+ /** 免责声明在服务端强制注入,不依赖宿主平台的 system prompt。 */
54
+ export function withDisclaimer(text) {
55
+ return `${text}\n\n_${DISCLAIMER}_`;
56
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 网关调用层。所有工具共用,按 skill 参数化。
3
+ *
4
+ * 本进程是纯代理:Key 从环境变量进来,原样透传给网关,不解析、不落地、不写日志。
5
+ */
6
+ import { ToolError } from "./core.js";
7
+ const BASE_URL = (process.env.PA_GATEWAY_BASE_URL ?? "https://ai.stock.pingan.com").replace(/\/+$/, "");
8
+ const CHANNEL_ID = process.env.PA_CHANNEL_ID;
9
+ /** 宿主平台的工具超时通常 10~30s,这里必须短于它,否则模型只会看到一个没有语义的失败。 */
10
+ const TIMEOUT_MS = Number(process.env.PA_GATEWAY_TIMEOUT ?? 20) * 1000;
11
+ export async function call(ep, path, payload, apiKey) {
12
+ const requestId = globalThis.crypto.randomUUID();
13
+ const url = `${BASE_URL}/${ep.prefix}/${path}`;
14
+ const headers = {
15
+ "X-API-Key": apiKey,
16
+ "Content-Type": "application/json",
17
+ "X-Skill-ID": ep.skillId,
18
+ "X-Request-ID": requestId,
19
+ requestId,
20
+ };
21
+ if (CHANNEL_ID)
22
+ headers.channelId = CHANNEL_ID;
23
+ const ac = new AbortController();
24
+ const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
25
+ let resp;
26
+ try {
27
+ resp = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload), signal: ac.signal });
28
+ }
29
+ catch (e) {
30
+ if (e?.name === "AbortError")
31
+ throw new ToolError("服务响应超时,请稍后重试;不要重复发起同一查询。");
32
+ throw new ToolError(`网络异常:${e?.name ?? "Error"}`);
33
+ }
34
+ finally {
35
+ clearTimeout(timer);
36
+ }
37
+ if (resp.status === 401) {
38
+ throw new ToolError("未提供 API Key。请设置环境变量 PINGAN_SKILL_APIKEY," +
39
+ "申请地址:https://stock.pingan.com/huodong/aiskill/skillPage/index.html");
40
+ }
41
+ if (resp.status === 403)
42
+ throw new ToolError("API Key 无效或无此接口权限。请到平安证券开放平台确认 Key 状态与授权范围。");
43
+ if (resp.status === 429) {
44
+ const code = await safeField(resp, "error_code");
45
+ if (code === "RATE_LIMIT_EXCEEDED")
46
+ throw new ToolError("调用频率超限,请稍后重试;本轮不要再重复调用。");
47
+ if (code === "DAILY_QUOTA_EXCEEDED")
48
+ throw new ToolError("今日调用次数已达上限,请明日再试或联系平安证券提升配额。");
49
+ throw new ToolError("请求超过限制,请稍后重试。");
50
+ }
51
+ if (resp.status === 503)
52
+ throw new ToolError("服务暂时不可用,请稍后重试。");
53
+ if (resp.status >= 400)
54
+ throw new ToolError(`服务返回异常状态 ${resp.status}。`);
55
+ let body;
56
+ try {
57
+ body = await resp.json();
58
+ }
59
+ catch {
60
+ throw new ToolError("服务返回内容无法解析。");
61
+ }
62
+ // 网关层错误码。原文透出——这是模型自纠正参数的主要依据,不做脱敏。
63
+ if (body?.code !== undefined && body.code !== 0 && body.code !== null) {
64
+ throw new ToolError(body?.error_message || "接口调用失败。");
65
+ }
66
+ let data = body?.data ?? body;
67
+ // 业务层错误码:部分接口在 data 里再包一层 status/errmsg/results
68
+ if (data && typeof data === "object" && "status" in data && data.status !== 1) {
69
+ throw new ToolError(data.errmsg || "接口返回失败。");
70
+ }
71
+ if (data && typeof data === "object" && "results" in data) {
72
+ data = data.results ?? {};
73
+ }
74
+ return data;
75
+ }
76
+ async function safeField(resp, field) {
77
+ try {
78
+ return (await resp.json())?.[field] ?? "";
79
+ }
80
+ catch {
81
+ return "";
82
+ }
83
+ }
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { apiKey, guard, ToolError } from "../shared/core.js";
3
+ import { call } from "../shared/gateway.js";
4
+ import { ratioPct, table, withDisclaimer } from "../shared/format.js";
5
+ const EP = { prefix: "restapi/hotstock", skillId: "hot-stock" };
6
+ const PATH = "isspContentService/hotStock/list";
7
+ // ---- 语义化枚举 → 网关码 ----
8
+ const BOARDS = {
9
+ search: { code: "hot_stock", label: "热搜榜", note: "用户主动搜索的标的,衡量绝对关注量" },
10
+ watchlist: { code: "hot_select", label: "加自选榜", note: "用户加入自选持续跟踪的标的,衡量绝对关注量,是关注深度最高的一档" },
11
+ browse: { code: "hot_watch", label: "浏览榜", note: "用户浏览过的标的,衡量绝对关注量,是关注深度最浅的一档" },
12
+ surge: { code: "hot_surge", label: "飙升榜", note: "关注度增速最快的标的,衡量的是变化速度而非绝对量,上榜标的绝对热度未必高" },
13
+ };
14
+ const CATEGORIES = {
15
+ a_stock: { code: "aStock", label: "A股" },
16
+ etf: { code: "etf", label: "ETF" },
17
+ sector: { code: "block", label: "板块概念" },
18
+ hk_stock: { code: "hkStock", label: "港股" },
19
+ convertible_bond: { code: "bond", label: "可转债" },
20
+ };
21
+ const TIME_RANGES = {
22
+ "1h": { code: 1, label: "近 1 小时" },
23
+ "24h": { code: 2, label: "近 24 小时" },
24
+ };
25
+ // ---- 返参裁剪 ----
26
+ function format(data, board, category, timeRange) {
27
+ const list = data?.list ?? [];
28
+ const b = BOARDS[board];
29
+ const c = CATEGORIES[category];
30
+ const t = TIME_RANGES[timeRange];
31
+ if (!list.length) {
32
+ return `${b.label}(${c.label},${t.label})暂无数据。`;
33
+ }
34
+ // 网关返回 "2026-09-02 09:00:00",skill 脚本一直只保留日期部分,这里保持一致
35
+ const date = String(data?.updateTime ?? "—").slice(0, 10);
36
+ const head = `**${b.label} · ${c.label} · ${t.label}**(数据日期 ${date})\n` +
37
+ `${b.note}。按热度排序,不是涨幅排序。\n\n`;
38
+ const rows = list.map((it, i) => {
39
+ const code = `${it.marketType ?? ""}${it.stockCode ?? ""}`.trim();
40
+ const name = code ? `${it.name ?? "—"}(${code})` : (it.name ?? "—");
41
+ // chg 是小数比例字符串("-0.0391" = -3.91%),必须乘 100
42
+ return [String(i + 1), name, ratioPct(it.chg)];
43
+ });
44
+ return head + table(["排名", "名称(代码)", "涨跌幅"], rows);
45
+ }
46
+ // ---- 注册 ----
47
+ export function registerHotStock(server) {
48
+ server.registerTool("pa_hot_ranking", {
49
+ title: "热股风向标",
50
+ description: "查询平安证券用户群体的实时关注榜单,回答「今天什么最热」「大家都在搜/看/加自选什么」「哪些标的关注度飙升」这类问题。" +
51
+ "四类榜单:search 热搜榜(用户在搜)、watchlist 加自选榜(用户在持续跟踪)、browse 浏览榜(用户在看)、" +
52
+ "surge 飙升榜(关注度增速最快,不代表绝对热度高)。前三个按绝对关注量排,surge 按增速排,含义不同。" +
53
+ "支持 A股/ETF/板块概念/港股/可转债,用户没说品种默认 A股;说「刚刚/现在/这一小时」用 1h,否则 24h。" +
54
+ "榜单反映平安证券用户行为,不代表全市场,也不是涨幅排名。需要某只标的的行情/K线/资金流向请用行情工具。",
55
+ inputSchema: {
56
+ board: z
57
+ .enum(["search", "watchlist", "browse", "surge"])
58
+ .describe("榜单类型:search 热搜榜、watchlist 加自选榜、browse 浏览榜、surge 飙升榜(关注度增速)"),
59
+ category: z
60
+ .enum(["a_stock", "etf", "sector", "hk_stock", "convertible_bond"])
61
+ .default("a_stock")
62
+ .describe("品种:a_stock A股(默认)、etf、sector 板块概念、hk_stock 港股、convertible_bond 可转债"),
63
+ time_range: z
64
+ .enum(["1h", "24h"])
65
+ .default("24h")
66
+ .describe("统计时间窗:24h(默认)或 1h。用户说「刚刚/现在/这一小时」时用 1h"),
67
+ size: z.number().int().min(1).max(100).default(10).describe("返回条数,1~100,默认 10"),
68
+ },
69
+ }, async ({ board, category, time_range, size }) => guard(async () => {
70
+ const b = BOARDS[board];
71
+ const c = CATEGORIES[(category ?? "a_stock")];
72
+ const t = TIME_RANGES[(time_range ?? "24h")];
73
+ if (!b || !c || !t)
74
+ throw new ToolError("参数取值不在允许范围内,请按 schema 中的枚举值传参。");
75
+ const data = await call(EP, PATH, { moduleCode: b.code, typeCode: c.code, timeRange: t.code, page: 1, size: size ?? 10 }, apiKey());
76
+ return withDisclaimer(format(data, board, category, time_range));
77
+ }));
78
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "pingan-securities-mcp",
3
+ "version": "1.0.0",
4
+ "description": "平安证券 MCP Server — 热股风向标(热搜/加自选/浏览/飙升榜),持续扩展中",
5
+ "keywords": [
6
+ "mcp",
7
+ "modelcontextprotocol",
8
+ "model-context-protocol",
9
+ "stock",
10
+ "a-share",
11
+ "hk-stock",
12
+ "etf",
13
+ "market-data",
14
+ "finance",
15
+ "pingan",
16
+ "平安证券",
17
+ "热股",
18
+ "热搜榜"
19
+ ],
20
+ "license": "MIT",
21
+ "author": "Ping An Securities",
22
+ "type": "module",
23
+ "bin": {
24
+ "pingan-securities-mcp": "dist/index.js"
25
+ },
26
+ "main": "dist/index.js",
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc && chmod +x dist/index.js",
37
+ "prepublishOnly": "npm run build && npm run smoke",
38
+ "start": "node dist/index.js",
39
+ "smoke": "node scripts/smoke.mjs"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.30.0",
43
+ "zod": "^4.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.20.1",
47
+ "typescript": "^5.6.0"
48
+ }
49
+ }