@pipeworx/mcp-china-exchange-data 0.1.2 → 0.1.3
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 +2 -2
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +92 -23
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Live market data straight from the two Chinese mainland exchanges — SZSE (Shenzhen) and SSE (Shanghai), one pack because they are a single jurisdiction. Fills what `@pipeworx/china-stocks` doesn't cover: industry/sector turnover ranking, index daily/weekly bars, a full-market board snapshot for screens, and company bulletins (公告) by code.
|
|
4
4
|
|
|
5
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1764+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
6
6
|
|
|
7
7
|
板块 主力资金 净流入 指数 日线 公告 — 深交所 上交所 A股行情数据接口
|
|
8
8
|
|
|
@@ -81,7 +81,7 @@ directly, instead of just this one's:
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Both URLs reach the same gateway and the same
|
|
84
|
+
Both URLs reach the same gateway and the same 1764+ data sources. The
|
|
85
85
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
86
86
|
reaches all of them from either one.
|
|
87
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-china-exchange-data",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "China Exchange Data MCP (SZSE + SSE). Keyless.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-8195c53163250126a605b218ef1356b0b73857bc1a28595cb321f7eff3f25320",
|
|
30
|
+
"sourceCommit": "0ce6905893fe8da0d285be2f05754a4200d67371"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pipeworx-io/china-exchange-data",
|
|
4
4
|
"title": "China Exchange Data",
|
|
5
5
|
"description": "China Exchange Data MCP (SZSE + SSE). Keyless.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.3",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/china-exchange-data",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-china-exchange-data",
|
package/src/index.ts
CHANGED
|
@@ -687,14 +687,23 @@ function collapse(s: string): string {
|
|
|
687
687
|
* CATALOGID=<id> — used here for CATALOGID=1804_gptj_merge_after (股票行业
|
|
688
688
|
* 统计, industry turnover statistics; requires txtQueryDate).
|
|
689
689
|
* - SZSE quote history: www.szse.cn/api/market/ssjjhq/getHistoryData?
|
|
690
|
-
* cycleType=<32|33>&marketId=1&code=<code> — daily(32)/weekly(33)
|
|
691
|
-
* bars for any SZSE-listed code OR SZSE index (399xxx).
|
|
690
|
+
* cycleType=<32|33|37>&marketId=1&code=<code> — daily(32)/weekly(33)/
|
|
691
|
+
* annual(37) OHLC bars for any SZSE-listed code OR SZSE index (399xxx).
|
|
692
|
+
* No begin/end window param — always returns full available history
|
|
693
|
+
* (confirmed back to 1991 for 399xxx indices), sliced client-side to the
|
|
694
|
+
* requested count. (cycleType 34/35/36 = monthly/quarterly/semi-annual,
|
|
695
|
+
* probed but not exposed as tool params — not asked for.) Row shape
|
|
692
696
|
* (empirically confirmed, not documented by SZSE):
|
|
693
|
-
* [date, open, close, low, high, change, change_pct, volume, amount]
|
|
697
|
+
* [date, open, close, low, high, change, change_pct, volume, amount] —
|
|
698
|
+
* change/change_pct are period-over-period (e.g. for cycleType=37, this
|
|
699
|
+
* year's close vs last year's close), computed by SZSE itself.
|
|
694
700
|
* - SSE public quote system: yunhq.sse.com.cn:32041/v1/sh1/... (plain HTTP
|
|
695
|
-
* only — the TLS port is not reachable from here). /dayk/<code
|
|
696
|
-
*
|
|
697
|
-
*
|
|
701
|
+
* only — the TLS port is not reachable from here). /dayk/<code>?period=
|
|
702
|
+
* day|week|year gives OHLCV bars for any SSE code OR SSE index
|
|
703
|
+
* (000xxx/999xxx); period=year confirmed live back to the 1990s. Row =
|
|
704
|
+
* [date(YYYYMMDD), open, high, low, close, volume, amount] — no
|
|
705
|
+
* change/change_pct field, so ashares_index_bars computes it here from
|
|
706
|
+
* consecutive closes (fetches one extra bar as the prior-close anchor).
|
|
698
707
|
* /list/exchange/equity gives the full SSE equity board as one paged call
|
|
699
708
|
* (~2,358 rows): [code, name, last, change_pct, volume, amount] — a
|
|
700
709
|
* real-time/latest-session snapshot, not per-symbol OHLC, and not
|
|
@@ -753,6 +762,24 @@ function round2(v: number | null): number | null {
|
|
|
753
762
|
function todayISO(): string {
|
|
754
763
|
return new Date().toISOString().slice(0, 10);
|
|
755
764
|
}
|
|
765
|
+
/** True if `dateStr` (YYYY-MM-DD) is the exchange's in-progress CURRENT year,
|
|
766
|
+
* not yet a closed calendar year — both exchanges' annual-bar endpoints append
|
|
767
|
+
* a running bar for the current year dated "today" rather than year-end until
|
|
768
|
+
* the year actually closes (confirmed live 2026-10-10: the newest annual bar
|
|
769
|
+
* for 000001/399001 was dated 2026-10-09, not 2026-12-3x). Treating that as a
|
|
770
|
+
* completed "2026 annual close" would silently push a 6-year request to
|
|
771
|
+
* 2021-2026 instead of 2020-2025 — so `years` mode drops it. Once the
|
|
772
|
+
* calendar reaches the exchanges' actual year-end trading days (~Dec 29-31)
|
|
773
|
+
* the bar IS the real close and is kept. */
|
|
774
|
+
function isPartialCurrentYear(dateStr: string): boolean {
|
|
775
|
+
const today = todayISO();
|
|
776
|
+
if (dateStr.slice(0, 4) !== today.slice(0, 4)) return false;
|
|
777
|
+
return !(today.slice(5, 7) === '12' && Number(today.slice(8, 10)) >= 28);
|
|
778
|
+
}
|
|
779
|
+
function dropTrailingPartialYear<T extends { date: string }>(bars: T[]): T[] {
|
|
780
|
+
if (bars.length === 0) return bars;
|
|
781
|
+
return isPartialCurrentYear(bars[bars.length - 1].date) ? bars.slice(0, -1) : bars;
|
|
782
|
+
}
|
|
756
783
|
function stripJsonp(text: string): unknown {
|
|
757
784
|
const m = text.match(/^[^(]*\(([\s\S]*)\)\s*;?\s*$/);
|
|
758
785
|
const body = m ? m[1] : text;
|
|
@@ -821,13 +848,14 @@ const tools: McpToolExport['tools'] = [
|
|
|
821
848
|
{
|
|
822
849
|
name: 'ashares_index_bars',
|
|
823
850
|
description:
|
|
824
|
-
'
|
|
851
|
+
'上证/深证指数日线周线年线 — Daily, weekly, or annual OHLC + volume + turnover bars for a major China A-share index: 上证指数 Shanghai Composite (000001), 上证50 (000016), 沪深300 CSI 300 (000300), 科创50 STAR50 (000688) from SSE; 深证成指 Shenzhen Component (399001), 创业板指 ChiNext (399006) from SZSE. Weekly history goes back up to 6 years (312 bars); `years` returns one bar per calendar year (close + year-over-year pct change) for multi-year questions like "5 years of annual closes for CSI 300". Answers "上证指数000001最近10个交易日OHLC", "Shanghai Composite last 10 days", "深证成指周线", "ChiNext index history", "CSI 300 annual close 2020-2025". Source: SSE yunhq public quote system (000xxx/999xxx indices) + SZSE ssjjhq history API (399xxx indices), both keyless.',
|
|
825
852
|
inputSchema: {
|
|
826
853
|
type: 'object' as const,
|
|
827
854
|
properties: {
|
|
828
855
|
index: { type: 'string', description: 'Index code: 000001 (上证指数), 000016 (上证50), 000300 (沪深300), 000688 (科创50), 399001 (深证成指), 399006 (创业板指). Default 000001.' },
|
|
829
|
-
days: { type: 'number', description: 'Number of daily bars to return, 1-250 (default 10). Ignored if weeks is set.' },
|
|
830
|
-
weeks: { type: 'number', description: 'Number of weekly bars to return, 1-
|
|
856
|
+
days: { type: 'number', description: 'Number of daily bars to return, 1-250 (default 10). Ignored if weeks or years is set.' },
|
|
857
|
+
weeks: { type: 'number', description: 'Number of weekly bars to return, 1-312 (up to ~6 years). Overrides days when set; ignored if years is set.' },
|
|
858
|
+
years: { type: 'number', description: 'Number of annual bars to return, 1-40 — one bar per calendar year with close and year-over-year pct_change. Overrides weeks and days when set. E.g. years:6 for 2020-2025 inclusive.' },
|
|
831
859
|
},
|
|
832
860
|
},
|
|
833
861
|
},
|
|
@@ -904,13 +932,27 @@ async function sectorFlows(args: Record<string, unknown>) {
|
|
|
904
932
|
// ── ashares_index_bars ──────────────────────────────────────────────
|
|
905
933
|
async function indexBars(args: Record<string, unknown>) {
|
|
906
934
|
const code = typeof args.index === 'string' && args.index.trim() ? args.index.trim() : '000001';
|
|
907
|
-
|
|
908
|
-
|
|
935
|
+
// years > weeks > days. Weekly ceiling raised 104 -> 312 (~6y); years is new
|
|
936
|
+
// (both exchanges expose a native annual cycle — confirmed live 2026-10-10:
|
|
937
|
+
// SZSE cycleType=37, SSE yunhq period=year — so this is a real upstream bar
|
|
938
|
+
// size, not a client-side aggregation).
|
|
939
|
+
const years = args.years != null ? Math.min(Math.max(Number(args.years), 1), 40) : null;
|
|
940
|
+
const weeks = years == null && args.weeks != null ? Math.min(Math.max(Number(args.weeks), 1), 312) : null;
|
|
941
|
+
const days = years == null && weeks == null ? Math.min(Math.max(Number(args.days ?? 10), 1), 250) : null;
|
|
942
|
+
const want = (years ?? weeks ?? days) as number;
|
|
943
|
+
const period = years != null ? 'annual' : weeks != null ? 'weekly' : 'daily';
|
|
909
944
|
const exch = indexExchange(code);
|
|
910
945
|
const name = exch === 'sse' ? SSE_INDICES[code] : SZSE_INDICES[code];
|
|
911
946
|
|
|
912
947
|
if (exch === 'szse') {
|
|
913
|
-
|
|
948
|
+
// SZSE cycleType: 32 daily, 33 weekly, 37 annual (confirmed live; 34/35/36
|
|
949
|
+
// are monthly/quarterly/semi-annual, not used here). getHistoryData has no
|
|
950
|
+
// begin/end window param — it always returns full available history and
|
|
951
|
+
// we slice client-side, which is also how the weekly ceiling raise works
|
|
952
|
+
// with no upstream change. change/change_pct in the row are already
|
|
953
|
+
// period-over-period (confirmed: 2020 annual change = 2020 close - 2019
|
|
954
|
+
// close for 399001), so no recomputation needed here.
|
|
955
|
+
const cycleType = years != null ? 37 : weeks != null ? 33 : 32;
|
|
914
956
|
const params = new URLSearchParams({ random: String(Math.random()), cycleType: String(cycleType), marketId: '1', code });
|
|
915
957
|
const res = await pwFetch(`${SZSE_HISTORY}?${params}`, { headers: { Referer: SZSE_REFERER } }, 'SZSE');
|
|
916
958
|
if (!res.ok) throw await httpError(res, 'SZSE');
|
|
@@ -918,36 +960,63 @@ async function indexBars(args: Record<string, unknown>) {
|
|
|
918
960
|
if (body.code !== '0' || !body.data?.picupdata) {
|
|
919
961
|
return { index: code, name: name ?? body.data?.name ?? null, found: false, message: `No SZSE history for index ${code}.` };
|
|
920
962
|
}
|
|
921
|
-
|
|
922
|
-
|
|
963
|
+
// For annual, fetch a small margin so dropping the in-progress current
|
|
964
|
+
// year still leaves `want` closed years (full history is always returned
|
|
965
|
+
// here regardless, so this costs nothing extra upstream).
|
|
966
|
+
let bars = body.data.picupdata.slice(-(want + (years != null ? 2 : 0))).map((r) => szseRow(r));
|
|
967
|
+
if (years != null) bars = dropTrailingPartialYear(bars);
|
|
968
|
+
bars = bars.slice(-want);
|
|
923
969
|
return {
|
|
924
970
|
index: code,
|
|
925
971
|
name: name ?? body.data.name ?? null,
|
|
926
|
-
period
|
|
972
|
+
period,
|
|
927
973
|
bar_count: bars.length,
|
|
928
974
|
bars,
|
|
929
|
-
source:
|
|
975
|
+
source: years != null
|
|
976
|
+
? 'SZSE ssjjhq getHistoryData (cycleType=37 annual, marketId=1) — one bar per calendar year; change/change_pct are SZSE-computed year-over-year close deltas'
|
|
977
|
+
: 'SZSE ssjjhq getHistoryData (cycleType=32 daily / 33 weekly, marketId=1) — undocumented row order confirmed empirically as [date, open, close, low, high, change, change_pct, volume, amount]',
|
|
930
978
|
};
|
|
931
979
|
}
|
|
932
980
|
|
|
933
|
-
// SSE via yunhq: /v1/sh1/dayk/<code>?begin=-N&end=-1&period=day
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
981
|
+
// SSE via yunhq: /v1/sh1/dayk/<code>?begin=-N&end=-1&period=day|week|year
|
|
982
|
+
// (period=year confirmed live 2026-10-10 — e.g. 000001 has 37 annual bars
|
|
983
|
+
// back to the 1990s). Two things to compensate for, both confirmed live:
|
|
984
|
+
// (a) yunhq's "begin=-N" is off by one — it returns N-1 bars, not N (this
|
|
985
|
+
// IS the original bug symptom: weeks:104 returned bar_count 103).
|
|
986
|
+
// (b) yunhq's row has no change/change_pct field, so we compute it here
|
|
987
|
+
// from consecutive closes, which needs one extra prior-close anchor.
|
|
988
|
+
// For annual we also need one more so dropping the in-progress current
|
|
989
|
+
// year still leaves `want` closed years.
|
|
990
|
+
const upstreamPeriod = years != null ? 'year' : weeks != null ? 'week' : 'day';
|
|
991
|
+
const offByOne = 1; // (a)
|
|
992
|
+
const anchorExtra = 1; // (b)
|
|
993
|
+
const partialYearExtra = years != null ? 1 : 0;
|
|
994
|
+
const requestN = want + offByOne + anchorExtra + partialYearExtra;
|
|
995
|
+
const url = `${SSE_YUNHQ}/v1/sh1/dayk/${encodeURIComponent(code)}?begin=-${requestN}&end=-1&period=${upstreamPeriod}`;
|
|
937
996
|
const res = await pwFetch(url, {}, 'SSE');
|
|
938
997
|
if (!res.ok) throw await httpError(res, 'SSE');
|
|
939
998
|
const body = (await res.json()) as { code?: string; kline?: unknown[][] };
|
|
940
999
|
if (!body.kline || body.kline.length === 0) {
|
|
941
1000
|
return { index: code, name: name ?? null, found: false, message: `No SSE history for index ${code}.` };
|
|
942
1001
|
}
|
|
943
|
-
const
|
|
1002
|
+
const parsed = body.kline.map((r) => sseRow(r));
|
|
1003
|
+
let withChange = parsed.map((r, i) => {
|
|
1004
|
+
const prevClose = i > 0 ? parsed[i - 1].close : null;
|
|
1005
|
+
const change = prevClose != null && r.close != null ? round2(r.close - prevClose) : null;
|
|
1006
|
+
const change_pct = prevClose != null && r.close != null && prevClose !== 0 ? round2(((r.close - prevClose) / prevClose) * 100) : null;
|
|
1007
|
+
return { ...r, change, change_pct };
|
|
1008
|
+
});
|
|
1009
|
+
if (years != null) withChange = dropTrailingPartialYear(withChange);
|
|
1010
|
+
const bars = withChange.length > want ? withChange.slice(-want) : withChange;
|
|
944
1011
|
return {
|
|
945
1012
|
index: code,
|
|
946
1013
|
name: name ?? null,
|
|
947
|
-
period
|
|
1014
|
+
period,
|
|
948
1015
|
bar_count: bars.length,
|
|
949
1016
|
bars,
|
|
950
|
-
source:
|
|
1017
|
+
source: years != null
|
|
1018
|
+
? 'SSE yunhq public quote system /v1/sh1/dayk period=year — one bar per calendar year; change/change_pct computed here (close vs prior year close) since SSE does not return them'
|
|
1019
|
+
: 'SSE yunhq public quote system /v1/sh1/dayk (row = [date, open, high, low, close, volume, amount]; change/change_pct computed here from consecutive closes)',
|
|
951
1020
|
};
|
|
952
1021
|
}
|
|
953
1022
|
|
package/src/server.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
11
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-china-exchange-data', version: '0.1.
|
|
12
|
+
{ name: '@pipeworx/mcp-china-exchange-data', version: '0.1.3' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|